diff --git a/README.md b/README.md index 4c4ed40..1f4291c 100644 --- a/README.md +++ b/README.md @@ -260,12 +260,13 @@ Open: - `POST /api/v1/resource-restores/{taskId}/apply` - `GET /api/v1/resource-restores/latest` -恢复任务创建接口只完成验签、解密、展开和计划生成;前端只做一次“确认恢复”,确认后先调用创建接口,返回 `READY_TO_APPLY` 后立即调用 apply 接口进入停机恢复。TMS 重启后前端可调用 latest 接口展示最近一次恢复时间、结果和失败原因。 +恢复任务创建接口只完成验签、解密、展开和计划生成;前端只做一次“确认恢复”,确认后先调用创建接口,返回 `READY_TO_APPLY` 后立即调用 apply 接口进入停机恢复。也就是用户确认后调用 apply 接口进入停机恢复,不再做第二次确认。TMS 重启后前端可调用 latest 接口展示最近一次恢复时间、结果和失败原因。 前后端字段、状态枚举和联调流程见 `docs/openapi/resource-backup-restore-frontend-integration.md`。 当前实现边界: - 资源备份包输出为 `.tmsbak`,包内包含 `manifest.json`、`envelope.json`、`payload.enc`、`signature.sig` +- `payload.enc` 加解密使用 PCIe 密码卡 SDF 会话密钥流程:备份时通过 `SDF_GenerateKeyWithIPK_ECC` 生成内部 ECC 公钥包裹的会话密钥并用 `SDF_Encrypt` 加密 payload;恢复时通过 `SDF_ImportKeyWithISK_ECC` 导入包裹会话密钥并用 `SDF_Decrypt` 解密 payload。`envelope.json` 保持 `version=1`,只记录 `payloadAlg`、`ivBase64` 和 `wrappedSessionKeyBase64`,`keyIndex/keyBits/algId` 由系统固定约定。 - 当前第一版备份/恢复只覆盖最小资源集: - `TMS_CONFIG` - `TMS_DB` @@ -278,7 +279,7 @@ Open: - `USER_KEY`(从实体证书 `KeyEntity.keyIdx` 采集用户密钥备份) - `MQ_REPLAY` - 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload -- 恢复任务创建时 Java 完成验签、PCIe 解密、payload 展开、用户密钥恢复和恢复路径白名单校验 +- 恢复任务创建时 Java 完成验签、PCIe/SDF 解密、payload 展开、用户密钥恢复和恢复路径白名单校验 - Java 会生成 shell 友好的 `restore-files.tsv`、`restore-databases.tsv`、`restore-mq.tsv` - 创建恢复任务后启动 `apply-resource-backup.sh --work-dir `,由 shell 执行停服务、文件覆盖、SQL 导入、MQ replay、起服务和健康检查 - Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`、`restore-stop-commands.tsv`、`restore-start-commands.tsv`;停启服务命令来自 `tms.backup.restore-stop-commands` / `tms.backup.restore-start-commands` @@ -298,7 +299,7 @@ Open: - `db/.sql`(标准/间参收发器包内存在时,导回同名机构库) - `TMS_DB` 备份默认通过 `tms.backup.tms-db-excluded-tables` 排除角色、授权、会话和审计表,恢复时不会覆盖新机器安全状态 - `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本 -- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置 +- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置;健康检查会按配置重试等待 TMS 真正启动完成 关键配置: - `tms.backup.output-dir` @@ -319,6 +320,8 @@ Open: - `tms.backup.cmep-database-name` - `tms.backup.health-check-url` - `tms.backup.health-check-timeout-seconds` +- `tms.backup.health-check-max-wait-seconds` +- `tms.backup.health-check-retry-interval-seconds` 恢复前置条件: - LMK、IK、数据存储加密密钥对、数据真实性保护密钥对需先通过 UKey 恢复 diff --git a/docs/openapi/resource-backup-restore-frontend-integration.md b/docs/openapi/resource-backup-restore-frontend-integration.md index d5b85c7..9cec71c 100644 --- a/docs/openapi/resource-backup-restore-frontend-integration.md +++ b/docs/openapi/resource-backup-restore-frontend-integration.md @@ -329,6 +329,7 @@ Content-Disposition: attachment; filename="RBKP-20260429-160000-ABCDEF.tmsbak" - `precheckPassed` 是后端聚合结果,当前等价于 `keysetMatched=true`、`signatureValid=true`、`compatible=true` 且 `blockingIssues` 为空 - `payloadDecrypted=false` 是正常值,预检阶段不解密完整 payload - 真正解密、展开 payload、路径白名单校验发生在创建恢复任务阶段 +- `payload.enc` 的实际加解密由后端通过 PCIe 密码卡 SDF 会话密钥流程完成,前端不需要传递 `keyIndex/keyBits/algId` ### 4.8 创建恢复任务 @@ -380,7 +381,7 @@ Content-Disposition: attachment; filename="RBKP-20260429-160000-ABCDEF.tmsbak" - `status=READY_TO_APPLY`:立即调用 apply,不再弹确认框 - 其它状态:展示异常提示,不调用 apply -后端在该阶段会完成验签、解密、payload 展开、用户密钥恢复、路径白名单校验和恢复计划文件生成,但不会停止 TMS。 +后端在该阶段会完成验签、PCIe/SDF 解密、payload 展开、用户密钥恢复、路径白名单校验和恢复计划文件生成,但不会停止 TMS。 ### 4.9 执行恢复任务 diff --git a/scripts/resource-restore/apply-resource-backup.sh b/scripts/resource-restore/apply-resource-backup.sh index 1e83797..a172ffa 100755 --- a/scripts/resource-restore/apply-resource-backup.sh +++ b/scripts/resource-restore/apply-resource-backup.sh @@ -161,12 +161,37 @@ health_check() { } command -v curl >/dev/null 2>&1 || fail "curl command not found for health check" "HEALTH_CHECK" local timeout="${HEALTH_CHECK_TIMEOUT_SECONDS:-5}" - local response - response="$(curl -fsS --max-time "${timeout}" "${url}")" || fail "health check request failed" "HEALTH_CHECK" - if [[ "${response}" != *"UP"* ]]; then - fail "health check did not return UP" "HEALTH_CHECK" - fi - log "health check passed" + local max_wait="${HEALTH_CHECK_MAX_WAIT_SECONDS:-180}" + local interval="${HEALTH_CHECK_RETRY_INTERVAL_SECONDS:-5}" + [[ "${max_wait}" =~ ^[0-9]+$ ]] || max_wait=180 + [[ "${interval}" =~ ^[0-9]+$ ]] || interval=5 + local start_time + start_time="$(date +%s)" + local attempt=1 + local response="" + local curl_output + while true; do + curl_output="$(curl -fsS --max-time "${timeout}" "${url}" 2>&1)" && response="${curl_output}" || { + log "[WARN] health check attempt ${attempt} failed: ${curl_output}" + response="" + } + if [[ "${response}" == *"UP"* ]]; then + log "health check passed after ${attempt} attempt(s)" + return 0 + fi + if [[ -n "${response}" ]]; then + log "[WARN] health check attempt ${attempt} did not return UP: ${response}" + fi + local now + now="$(date +%s)" + if (( now - start_time >= max_wait )); then + fail "health check request failed after ${max_wait}s" "HEALTH_CHECK" + fi + if (( interval > 0 )); then + sleep "${interval}" + fi + attempt=$((attempt + 1)) + done } mkdir -p "${WORK_DIR}" diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestRequest.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestRequest.java new file mode 100644 index 0000000..da10df2 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestRequest.java @@ -0,0 +1,68 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyCryptoSelfTestRequest { + + private int keyIndex = 1; + private int keyBits = 128; + private int algId; + private byte[] iv; + private byte[] data; + private int encOutBufferSize = 4096; + private int decOutBufferSize = 4096; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public int getEncOutBufferSize() { + return encOutBufferSize; + } + + public void setEncOutBufferSize(int encOutBufferSize) { + this.encOutBufferSize = encOutBufferSize; + } + + public int getDecOutBufferSize() { + return decOutBufferSize; + } + + public void setDecOutBufferSize(int decOutBufferSize) { + this.decOutBufferSize = decOutBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestResult.java new file mode 100644 index 0000000..7a8e6dd --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyCryptoSelfTestResult.java @@ -0,0 +1,77 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyCryptoSelfTestResult { + + private int keyIndex; + private int keyBits; + private int algId; + private byte[] iv; + private byte[] wrappedSessionKey; + private byte[] encryptedData; + private byte[] decryptedData; + private boolean matched; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getWrappedSessionKey() { + return wrappedSessionKey; + } + + public void setWrappedSessionKey(byte[] wrappedSessionKey) { + this.wrappedSessionKey = wrappedSessionKey; + } + + public byte[] getEncryptedData() { + return encryptedData; + } + + public void setEncryptedData(byte[] encryptedData) { + this.encryptedData = encryptedData; + } + + public byte[] getDecryptedData() { + return decryptedData; + } + + public void setDecryptedData(byte[] decryptedData) { + this.decryptedData = decryptedData; + } + + public boolean isMatched() { + return matched; + } + + public void setMatched(boolean matched) { + this.matched = matched; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptRequest.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptRequest.java new file mode 100644 index 0000000..1495bc9 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptRequest.java @@ -0,0 +1,59 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyDecryptRequest { + + private int keyIndex = 1; + private int algId; + private byte[] iv; + private byte[] wrappedSessionKey; + private byte[] encryptedData; + private int outBufferSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getWrappedSessionKey() { + return wrappedSessionKey; + } + + public void setWrappedSessionKey(byte[] wrappedSessionKey) { + this.wrappedSessionKey = wrappedSessionKey; + } + + public byte[] getEncryptedData() { + return encryptedData; + } + + public void setEncryptedData(byte[] encryptedData) { + this.encryptedData = encryptedData; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptResult.java new file mode 100644 index 0000000..6d350eb --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyDecryptResult.java @@ -0,0 +1,14 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyDecryptResult { + + private byte[] decryptedData; + + public byte[] getDecryptedData() { + return decryptedData; + } + + public void setDecryptedData(byte[] decryptedData) { + this.decryptedData = decryptedData; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptRequest.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptRequest.java new file mode 100644 index 0000000..b475698 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptRequest.java @@ -0,0 +1,59 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyEncryptRequest { + + private int keyIndex = 1; + private int keyBits = 128; + private int algId; + private byte[] iv; + private byte[] data; + private int outBufferSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptResult.java new file mode 100644 index 0000000..f06c462 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/EccSessionKeyEncryptResult.java @@ -0,0 +1,59 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +public class EccSessionKeyEncryptResult { + + private int keyIndex; + private int keyBits; + private int algId; + private byte[] iv; + private byte[] wrappedSessionKey; + private byte[] encryptedData; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public byte[] getIv() { + return iv; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getWrappedSessionKey() { + return wrappedSessionKey; + } + + public void setWrappedSessionKey(byte[] wrappedSessionKey) { + this.wrappedSessionKey = wrappedSessionKey; + } + + public byte[] getEncryptedData() { + return encryptedData; + } + + public void setEncryptedData(byte[] encryptedData) { + this.encryptedData = encryptedData; + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java index 603c81c..9a9d971 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java @@ -9,6 +9,7 @@ import com.sun.jna.Structure; import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.PointerByReference; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.function.Supplier; @@ -22,6 +23,8 @@ public class JnaPcieCryptoService implements PcieCryptoService { private static final int IK_KEY_TYPE_AUTH = 2; private static final int HMAC_MAX_LENGTH = 64; private static final int ECC_CIPHER_PREFIX_LENGTH = (EccRefPublicKey.MAX_LEN * 2) + 32 + Integer.BYTES; + private static final int SYMMETRIC_BLOCK_SIZE = 16; + private static final int SDF_SYMMETRIC_CHUNK_SIZE = 4096; private static final String DEFAULT_PRIVATE_KEY_ACCESS_PASSWORD = "88888888"; private static final byte[] DEFAULT_SM2_USER_ID = "1234567812345678".getBytes(StandardCharsets.UTF_8); @@ -1010,6 +1013,166 @@ public class JnaPcieCryptoService implements PcieCryptoService { }); } + @Override + public EccSessionKeyEncryptResult encryptWithInternalEccSessionKey(EccSessionKeyEncryptRequest request) { + EccSessionKeyEncryptRequest req = requireRequest("request", request); + int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex()); + int keyBits = requirePositive("keyBits", req.getKeyBits()); + int algId = req.getAlgId() > 0 ? requireValidAlgId("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + byte[] data = requireBytes("data", req.getData()); + int outBufferSize = requirePositive("outBufferSize", req.getOutBufferSize()); + + return sessionTemplate.withSession("SDF_GenerateKeyWithIPK_ECC+SDF_Encrypt", (lib, deviceHandle, sessionHandle) -> { + byte[] baseIv = resolveSymmetricIv(lib, sessionHandle, algId, req.getIv()); + byte[] encryptInput = requiresPkcs7Padding(algId) ? applyPkcs7Padding(data, 16) : data; + EccCipher wrappedSessionKey = new EccCipher(); + PointerByReference generatedHandleRef = new PointerByReference(); + sessionTemplate.ensureSuccess( + "SDF_GenerateKeyWithIPK_ECC", + lib.SDF_GenerateKeyWithIPK_ECC(sessionHandle, keyIndex, keyBits, wrappedSessionKey, generatedHandleRef) + ); + Pointer generatedHandle = requireKeyHandle("SDF_GenerateKeyWithIPK_ECC", generatedHandleRef.getValue()); + try { + EccSessionKeyEncryptResult result = new EccSessionKeyEncryptResult(); + result.setKeyIndex(keyIndex); + result.setKeyBits(keyBits); + result.setAlgId(algId); + result.setIv(copyNullable(baseIv)); + result.setWrappedSessionKey(toEccCipherBytes("SDF_GenerateKeyWithIPK_ECC", wrappedSessionKey)); + result.setEncryptedData(encryptSymmetricInChunks(lib, sessionHandle, generatedHandle, + algId, baseIv, encryptInput, Math.max(outBufferSize, encryptInput.length))); + return result; + } finally { + destroyKeyHandle(lib, sessionHandle, generatedHandle); + } + }); + } + + @Override + public EccSessionKeyDecryptResult decryptWithInternalEccSessionKey(EccSessionKeyDecryptRequest request) { + EccSessionKeyDecryptRequest req = requireRequest("request", request); + int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex()); + int algId = req.getAlgId() > 0 ? requireValidAlgId("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + EccCipher wrappedSessionKey = toEccCipher("wrappedSessionKey", req.getWrappedSessionKey()); + byte[] encryptedData = requireBytes("encryptedData", req.getEncryptedData()); + int outBufferSize = requirePositive("outBufferSize", req.getOutBufferSize()); + + return sessionTemplate.withSession("SDF_ImportKeyWithISK_ECC+SDF_Decrypt", (lib, deviceHandle, sessionHandle) -> { + byte[] password = DEFAULT_PRIVATE_KEY_ACCESS_PASSWORD.getBytes(StandardCharsets.UTF_8); + boolean privateAccessOpened = false; + Pointer importedHandle = null; + try { + sessionTemplate.ensureSuccess( + "SDF_GetPrivateKeyAccessRight", + lib.SDF_GetPrivateKeyAccessRight(sessionHandle, keyIndex, password, password.length) + ); + privateAccessOpened = true; + + PointerByReference importedHandleRef = new PointerByReference(); + sessionTemplate.ensureSuccess( + "SDF_ImportKeyWithISK_ECC", + lib.SDF_ImportKeyWithISK_ECC(sessionHandle, keyIndex, wrappedSessionKey, importedHandleRef) + ); + importedHandle = requireKeyHandle("SDF_ImportKeyWithISK_ECC", importedHandleRef.getValue()); + + byte[] decryptedOutput = decryptSymmetricInChunks(lib, sessionHandle, importedHandle, + algId, req.getIv(), encryptedData, Math.max(outBufferSize, encryptedData.length)); + EccSessionKeyDecryptResult result = new EccSessionKeyDecryptResult(); + result.setDecryptedData(requiresPkcs7Padding(algId) ? removePkcs7Padding(decryptedOutput, 16) : decryptedOutput); + return result; + } finally { + try { + destroyKeyHandle(lib, sessionHandle, importedHandle); + } finally { + if (privateAccessOpened) { + sessionTemplate.ensureSuccess( + "SDF_ReleasePrivateKeyAccessRight", + lib.SDF_ReleasePrivateKeyAccessRight(sessionHandle, keyIndex) + ); + } + wipe(password); + } + } + }); + } + + @Override + public EccSessionKeyCryptoSelfTestResult eccSessionKeyEncryptDecryptSelfTest(EccSessionKeyCryptoSelfTestRequest request) { + EccSessionKeyCryptoSelfTestRequest req = requireRequest("request", request); + int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex()); + int keyBits = requirePositive("keyBits", req.getKeyBits()); + int algId = req.getAlgId() > 0 ? requireValidAlgId("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + byte[] data = requireBytes("data", req.getData()); + int encOutBufferSize = requirePositive("encOutBufferSize", req.getEncOutBufferSize()); + int decOutBufferSize = requirePositive("decOutBufferSize", req.getDecOutBufferSize()); + + return sessionTemplate.withSession("SDF_GenerateKeyWithIPK_ECC+self-test", (lib, deviceHandle, sessionHandle) -> { + byte[] baseIv = resolveSymmetricIv(lib, sessionHandle, algId, req.getIv()); + byte[] encryptInput = requiresPkcs7Padding(algId) ? applyPkcs7Padding(data, 16) : data; + EccCipher wrappedSessionKey = new EccCipher(); + PointerByReference generatedHandleRef = new PointerByReference(); + sessionTemplate.ensureSuccess( + "SDF_GenerateKeyWithIPK_ECC", + lib.SDF_GenerateKeyWithIPK_ECC(sessionHandle, keyIndex, keyBits, wrappedSessionKey, generatedHandleRef) + ); + Pointer generatedHandle = requireKeyHandle("SDF_GenerateKeyWithIPK_ECC", generatedHandleRef.getValue()); + byte[] wrappedSessionKeyBytes = toEccCipherBytes("SDF_GenerateKeyWithIPK_ECC", wrappedSessionKey); + + byte[] encryptedData; + try { + encryptedData = encryptSymmetricInChunks(lib, sessionHandle, generatedHandle, + algId, baseIv, encryptInput, Math.max(encOutBufferSize, encryptInput.length)); + } finally { + destroyKeyHandle(lib, sessionHandle, generatedHandle); + } + + byte[] password = DEFAULT_PRIVATE_KEY_ACCESS_PASSWORD.getBytes(StandardCharsets.UTF_8); + boolean privateAccessOpened = false; + Pointer importedHandle = null; + try { + sessionTemplate.ensureSuccess( + "SDF_GetPrivateKeyAccessRight", + lib.SDF_GetPrivateKeyAccessRight(sessionHandle, keyIndex, password, password.length) + ); + privateAccessOpened = true; + + PointerByReference importedHandleRef = new PointerByReference(); + sessionTemplate.ensureSuccess( + "SDF_ImportKeyWithISK_ECC", + lib.SDF_ImportKeyWithISK_ECC(sessionHandle, keyIndex, wrappedSessionKey, importedHandleRef) + ); + importedHandle = requireKeyHandle("SDF_ImportKeyWithISK_ECC", importedHandleRef.getValue()); + + byte[] decryptedOutput = decryptSymmetricInChunks(lib, sessionHandle, importedHandle, + algId, baseIv, encryptedData, Math.max(decOutBufferSize, encryptedData.length)); + byte[] decryptedData = requiresPkcs7Padding(algId) ? removePkcs7Padding(decryptedOutput, 16) : decryptedOutput; + + EccSessionKeyCryptoSelfTestResult result = new EccSessionKeyCryptoSelfTestResult(); + result.setKeyIndex(keyIndex); + result.setKeyBits(keyBits); + result.setAlgId(algId); + result.setIv(copyNullable(baseIv)); + result.setWrappedSessionKey(wrappedSessionKeyBytes); + result.setEncryptedData(encryptedData); + result.setDecryptedData(decryptedData); + result.setMatched(Arrays.equals(data, decryptedData)); + return result; + } finally { + try { + destroyKeyHandle(lib, sessionHandle, importedHandle); + } finally { + if (privateAccessOpened) { + sessionTemplate.ensureSuccess( + "SDF_ReleasePrivateKeyAccessRight", + lib.SDF_ReleasePrivateKeyAccessRight(sessionHandle, keyIndex) + ); + } + wipe(password); + } + } + }); + } + @Override public BackupDataResult exchangeDigitEnvelopeBaseOnRsa(ExchangeDigitEnvelopeRsaRequest request) { ExchangeDigitEnvelopeRsaRequest req = requireRequest("request", request); @@ -1952,6 +2115,196 @@ public class JnaPcieCryptoService implements PcieCryptoService { } } + private byte[] encryptSymmetricInChunks( + PcieNativeLibrary lib, + Pointer sessionHandle, + Pointer keyHandle, + int algId, + byte[] iv, + byte[] data, + int outBufferSize + ) { + if (!shouldChunkSymmetricPayload(algId, data.length)) { + byte[] out = new byte[Math.max(outBufferSize, data.length)]; + IntByReference outLen = new IntByReference(out.length); + sessionTemplate.ensureSuccess( + "SDF_Encrypt", + lib.SDF_Encrypt(sessionHandle, keyHandle, algId, copyNullable(iv), + data, data.length, out, outLen) + ); + return toBackupResult("SDF_Encrypt", out, outLen.getValue()).getData(); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(data.length); + byte[] currentIv = copyNullable(iv); + int offset = 0; + while (offset < data.length) { + int chunkLength = nextSymmetricChunkLength(data.length - offset); + byte[] chunk = Arrays.copyOfRange(data, offset, offset + chunkLength); + byte[] out = new byte[chunkLength]; + IntByReference outLen = new IntByReference(out.length); + sessionTemplate.ensureSuccess( + "SDF_Encrypt", + lib.SDF_Encrypt(sessionHandle, keyHandle, algId, copyNullable(currentIv), + chunk, chunk.length, out, outLen) + ); + byte[] encryptedChunk = toBackupResult("SDF_Encrypt", out, outLen.getValue()).getData(); + output.write(encryptedChunk, 0, encryptedChunk.length); + currentIv = nextSymmetricIv(algId, currentIv, encryptedChunk); + offset += chunkLength; + } + return output.toByteArray(); + } + + private byte[] decryptSymmetricInChunks( + PcieNativeLibrary lib, + Pointer sessionHandle, + Pointer keyHandle, + int algId, + byte[] iv, + byte[] data, + int outBufferSize + ) { + if (!shouldChunkSymmetricPayload(algId, data.length)) { + byte[] out = new byte[Math.max(outBufferSize, data.length)]; + IntByReference outLen = new IntByReference(out.length); + sessionTemplate.ensureSuccess( + "SDF_Decrypt", + lib.SDF_Decrypt(sessionHandle, keyHandle, algId, copyNullable(iv), + data, data.length, out, outLen) + ); + return toBackupResult("SDF_Decrypt", out, outLen.getValue()).getData(); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(data.length); + byte[] currentIv = copyNullable(iv); + int offset = 0; + while (offset < data.length) { + int chunkLength = nextSymmetricChunkLength(data.length - offset); + byte[] chunk = Arrays.copyOfRange(data, offset, offset + chunkLength); + byte[] nextIv = nextSymmetricIv(algId, currentIv, chunk); + byte[] out = new byte[chunkLength]; + IntByReference outLen = new IntByReference(out.length); + sessionTemplate.ensureSuccess( + "SDF_Decrypt", + lib.SDF_Decrypt(sessionHandle, keyHandle, algId, copyNullable(currentIv), + chunk, chunk.length, out, outLen) + ); + byte[] decryptedChunk = toBackupResult("SDF_Decrypt", out, outLen.getValue()).getData(); + output.write(decryptedChunk, 0, decryptedChunk.length); + currentIv = nextIv; + offset += chunkLength; + } + return output.toByteArray(); + } + + private static boolean shouldChunkSymmetricPayload(int algId, int length) { + return length > SDF_SYMMETRIC_CHUNK_SIZE && supportsBlockAlignedChunking(algId); + } + + private static boolean supportsBlockAlignedChunking(int algId) { + return algId == Gm0018AlgorithmIds.SM1_ECB + || algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SSF33_ECB + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SM4_ECB + || algId == Gm0018AlgorithmIds.SM4_CBC; + } + + private static int nextSymmetricChunkLength(int remaining) { + if (remaining <= SDF_SYMMETRIC_CHUNK_SIZE) { + return remaining; + } + return SDF_SYMMETRIC_CHUNK_SIZE; + } + + private static byte[] nextSymmetricIv(int algId, byte[] currentIv, byte[] cipherChunk) { + if (!isCbcAlg(algId)) { + return copyNullable(currentIv); + } + if (cipherChunk == null || cipherChunk.length < SYMMETRIC_BLOCK_SIZE) { + throw new PcieCryptoException("SDF_Encrypt", -1, "CBC分块输出长度非法"); + } + return Arrays.copyOfRange(cipherChunk, cipherChunk.length - SYMMETRIC_BLOCK_SIZE, cipherChunk.length); + } + + private static boolean isCbcAlg(int algId) { + return algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SM4_CBC; + } + + private static Pointer requireKeyHandle(String apiName, Pointer keyHandle) { + if (keyHandle == null) { + throw new PcieCryptoException(apiName, -1, "原生接口返回空会话密钥句柄"); + } + return keyHandle; + } + + private byte[] resolveSymmetricIv(PcieNativeLibrary lib, Pointer sessionHandle, int algId, byte[] iv) { + if (iv != null && iv.length > 0) { + return Arrays.copyOf(iv, iv.length); + } + if (requiresSymmetricIv(algId)) { + byte[] randomIv = new byte[16]; + sessionTemplate.ensureSuccess( + "SDF_GenerateRandom", + lib.SDF_GenerateRandom(sessionHandle, randomIv.length, randomIv) + ); + return randomIv; + } + return null; + } + + private static boolean requiresSymmetricIv(int algId) { + return algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SM1_CFB + || algId == Gm0018AlgorithmIds.SM1_OFB + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SSF33_CFB + || algId == Gm0018AlgorithmIds.SSF33_OFB + || algId == Gm0018AlgorithmIds.SM4_CBC + || algId == Gm0018AlgorithmIds.SM4_CFB + || algId == Gm0018AlgorithmIds.SM4_OFB; + } + + private static boolean requiresPkcs7Padding(int algId) { + return algId == Gm0018AlgorithmIds.SM1_ECB + || algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SSF33_ECB + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SM4_ECB + || algId == Gm0018AlgorithmIds.SM4_CBC; + } + + private static byte[] applyPkcs7Padding(byte[] data, int blockSize) { + int paddingLength = blockSize - (data.length % blockSize); + if (paddingLength == 0) { + paddingLength = blockSize; + } + byte[] padded = Arrays.copyOf(data, data.length + paddingLength); + Arrays.fill(padded, data.length, padded.length, (byte) paddingLength); + return padded; + } + + private static byte[] removePkcs7Padding(byte[] data, int blockSize) { + if (data.length == 0 || data.length % blockSize != 0) { + throw new PcieCryptoException("SDF_Decrypt", -1, "解密后的填充数据长度非法"); + } + int paddingLength = data[data.length - 1] & 0xFF; + if (paddingLength < 1 || paddingLength > blockSize || paddingLength > data.length) { + throw new PcieCryptoException("SDF_Decrypt", -1, "解密后的填充数据非法"); + } + for (int i = data.length - paddingLength; i < data.length; i++) { + if ((data[i] & 0xFF) != paddingLength) { + throw new PcieCryptoException("SDF_Decrypt", -1, "解密后的填充数据非法"); + } + } + return Arrays.copyOf(data, data.length - paddingLength); + } + + private static byte[] copyNullable(byte[] value) { + return value == null ? null : Arrays.copyOf(value, value.length); + } + private static byte[] fileNameBytes(String fileName) { if (fileName == null || fileName.isBlank()) { throw new IllegalArgumentException("fileName不能为空"); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java index a917655..d8592f4 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java @@ -20,6 +20,12 @@ import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptResult; import com.cisd.tms.integration.crypto.pcie.model.EccExternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccExternalEncryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccExternalSignRequest; @@ -466,6 +472,71 @@ public class MockPcieCryptoService implements PcieCryptoService { requireNonEmptyBytes("encryptedKeyBlob", req.getEncryptedKeyBlob()); } + @Override + public EccSessionKeyEncryptResult encryptWithInternalEccSessionKey(EccSessionKeyEncryptRequest request) { + EccSessionKeyEncryptRequest req = requireRequest("request", request); + int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex()); + int keyBits = requirePositive("keyBits", req.getKeyBits()); + int algId = req.getAlgId() > 0 ? requirePositive("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + byte[] data = requireBytes("data", req.getData()); + byte[] iv = resolveMockIv(algId, req.getIv()); + byte[] encrypted = requiresPkcs7Padding(algId) ? applyPkcs7Padding(data, 16) : Arrays.copyOf(data, data.length); + for (int i = 0; i < encrypted.length; i++) { + encrypted[i] = (byte) (encrypted[i] ^ 0x5A); + } + + EccSessionKeyEncryptResult result = new EccSessionKeyEncryptResult(); + result.setKeyIndex(keyIndex); + result.setKeyBits(keyBits); + result.setAlgId(algId); + result.setIv(iv); + result.setWrappedSessionKey(mockBackup(132, 0x50).getData()); + result.setEncryptedData(encrypted); + return result; + } + + @Override + public EccSessionKeyDecryptResult decryptWithInternalEccSessionKey(EccSessionKeyDecryptRequest request) { + EccSessionKeyDecryptRequest req = requireRequest("request", request); + requireNonNegative("keyIndex", req.getKeyIndex()); + int algId = req.getAlgId() > 0 ? requirePositive("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + requireNonEmptyBytes("wrappedSessionKey", req.getWrappedSessionKey()); + byte[] encryptedData = requireBytes("encryptedData", req.getEncryptedData()); + byte[] decrypted = Arrays.copyOf(encryptedData, encryptedData.length); + for (int i = 0; i < decrypted.length; i++) { + decrypted[i] = (byte) (decrypted[i] ^ 0x5A); + } + EccSessionKeyDecryptResult result = new EccSessionKeyDecryptResult(); + result.setDecryptedData(requiresPkcs7Padding(algId) ? removePkcs7Padding(decrypted, 16) : decrypted); + return result; + } + + @Override + public EccSessionKeyCryptoSelfTestResult eccSessionKeyEncryptDecryptSelfTest(EccSessionKeyCryptoSelfTestRequest request) { + EccSessionKeyCryptoSelfTestRequest req = requireRequest("request", request); + int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex()); + int keyBits = requirePositive("keyBits", req.getKeyBits()); + int algId = req.getAlgId() > 0 ? requirePositive("algId", req.getAlgId()) : Gm0018AlgorithmIds.SM4_CBC; + byte[] data = requireBytes("data", req.getData()); + byte[] iv = resolveMockIv(algId, req.getIv()); + byte[] encryptInput = requiresPkcs7Padding(algId) ? applyPkcs7Padding(data, 16) : data; + byte[] encrypted = Arrays.copyOf(encryptInput, encryptInput.length); + for (int i = 0; i < encrypted.length; i++) { + encrypted[i] = (byte) (encrypted[i] ^ 0x5A); + } + + EccSessionKeyCryptoSelfTestResult result = new EccSessionKeyCryptoSelfTestResult(); + result.setKeyIndex(keyIndex); + result.setKeyBits(keyBits); + result.setAlgId(algId); + result.setIv(iv); + result.setWrappedSessionKey(mockBackup(132, 0x50).getData()); + result.setEncryptedData(encrypted); + result.setDecryptedData(Arrays.copyOf(data, data.length)); + result.setMatched(true); + return result; + } + @Override public BackupDataResult exchangeDigitEnvelopeBaseOnRsa(ExchangeDigitEnvelopeRsaRequest request) { ExchangeDigitEnvelopeRsaRequest req = requireRequest("request", request); @@ -938,6 +1009,63 @@ public class MockPcieCryptoService implements PcieCryptoService { return result; } + private byte[] resolveMockIv(int algId, byte[] iv) { + if (iv != null && iv.length > 0) { + return Arrays.copyOf(iv, iv.length); + } + if (requiresSymmetricIv(algId)) { + return generateRandom(16); + } + return null; + } + + private static boolean requiresSymmetricIv(int algId) { + return algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SM1_CFB + || algId == Gm0018AlgorithmIds.SM1_OFB + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SSF33_CFB + || algId == Gm0018AlgorithmIds.SSF33_OFB + || algId == Gm0018AlgorithmIds.SM4_CBC + || algId == Gm0018AlgorithmIds.SM4_CFB + || algId == Gm0018AlgorithmIds.SM4_OFB; + } + + private static boolean requiresPkcs7Padding(int algId) { + return algId == Gm0018AlgorithmIds.SM1_ECB + || algId == Gm0018AlgorithmIds.SM1_CBC + || algId == Gm0018AlgorithmIds.SSF33_ECB + || algId == Gm0018AlgorithmIds.SSF33_CBC + || algId == Gm0018AlgorithmIds.SM4_ECB + || algId == Gm0018AlgorithmIds.SM4_CBC; + } + + private static byte[] applyPkcs7Padding(byte[] data, int blockSize) { + int paddingLength = blockSize - (data.length % blockSize); + if (paddingLength == 0) { + paddingLength = blockSize; + } + byte[] padded = Arrays.copyOf(data, data.length + paddingLength); + Arrays.fill(padded, data.length, padded.length, (byte) paddingLength); + return padded; + } + + private static byte[] removePkcs7Padding(byte[] data, int blockSize) { + if (data.length == 0 || data.length % blockSize != 0) { + throw new IllegalArgumentException("解密后的填充数据长度非法"); + } + int paddingLength = data[data.length - 1] & 0xFF; + if (paddingLength < 1 || paddingLength > blockSize || paddingLength > data.length) { + throw new IllegalArgumentException("解密后的填充数据非法"); + } + for (int i = data.length - paddingLength; i < data.length; i++) { + if ((data[i] & 0xFF) != paddingLength) { + throw new IllegalArgumentException("解密后的填充数据非法"); + } + } + return Arrays.copyOf(data, data.length - paddingLength); + } + private static int requireUserKeyType(int keyType) { if (keyType != PcieUserKeyType.SIGN.getCode() && keyType != PcieUserKeyType.ENC.getCode()) { throw new IllegalArgumentException("keyType必须为0(SIGN)或1(ENC)"); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java index f71984a..aaff2b6 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java @@ -10,6 +10,12 @@ import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptResult; import com.cisd.tms.integration.crypto.pcie.model.EccExternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccExternalEncryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccExternalSignRequest; @@ -341,6 +347,21 @@ public interface PcieCryptoService { */ void importKeyWithIskEcc(ImportKeyWithIskEccRequest request); + /** + * 生成内部 ECC 公钥包裹的会话密钥,使用原会话密钥句柄加密数据,再导入包裹密钥并解密数据。 + */ + EccSessionKeyCryptoSelfTestResult eccSessionKeyEncryptDecryptSelfTest(EccSessionKeyCryptoSelfTestRequest request); + + /** + * 使用内部 ECC 公钥生成会话密钥,并通过 SDF_Encrypt 加密数据。 + */ + EccSessionKeyEncryptResult encryptWithInternalEccSessionKey(EccSessionKeyEncryptRequest request); + + /** + * 使用内部 ECC 私钥导入包裹会话密钥,并通过 SDF_Decrypt 解密数据。 + */ + EccSessionKeyDecryptResult decryptWithInternalEccSessionKey(EccSessionKeyDecryptRequest request); + /** * 执行基于 RSA 的数字信封交换。 */ diff --git a/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java b/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java index 54a2607..5f0ec90 100644 --- a/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java +++ b/src/main/java/com/cisd/tms/modules/backup/config/ResourceBackupProperties.java @@ -29,6 +29,8 @@ public class ResourceBackupProperties { private String cmepDatabaseName = "CMEP"; private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health"; private int healthCheckTimeoutSeconds = 5; + private int healthCheckMaxWaitSeconds = 180; + private int healthCheckRetryIntervalSeconds = 5; private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh"; private List restoreStopCommands = List.of(); private List restoreStartCommands = List.of(); @@ -144,6 +146,22 @@ public class ResourceBackupProperties { this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds; } + public int getHealthCheckMaxWaitSeconds() { + return healthCheckMaxWaitSeconds; + } + + public void setHealthCheckMaxWaitSeconds(int healthCheckMaxWaitSeconds) { + this.healthCheckMaxWaitSeconds = healthCheckMaxWaitSeconds; + } + + public int getHealthCheckRetryIntervalSeconds() { + return healthCheckRetryIntervalSeconds; + } + + public void setHealthCheckRetryIntervalSeconds(int healthCheckRetryIntervalSeconds) { + this.healthCheckRetryIntervalSeconds = healthCheckRetryIntervalSeconds; + } + public String getRestoreApplyScriptPath() { return restoreApplyScriptPath; } diff --git a/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java index 89c43ae..8e5859e 100644 --- a/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceImpl.java @@ -1,31 +1,22 @@ package com.cisd.tms.modules.backup.crypto; +import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; import com.cisd.tms.integration.crypto.pcie.model.*; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import java.security.Security; -import java.util.Arrays; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.crypto.digests.SM3Digest; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.stereotype.Service; @Service public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCryptoService { - private static final String PROVIDER = "BC"; + private static final int PAYLOAD_SESSION_KEY_BITS = 128; + private static final int PAYLOAD_ALG_ID = Gm0018AlgorithmIds.SM4_CBC; + private static final String PAYLOAD_ALG_NAME = "SDF_SM4_CBC"; private final PcieCryptoService pcieCryptoService; - static { - if (Security.getProvider(PROVIDER) == null) { - Security.addProvider(new BouncyCastleProvider()); - } - } - public ResourceEnvelopeCryptoServiceImpl(PcieCryptoService pcieCryptoService) { this.pcieCryptoService = pcieCryptoService; } @@ -35,38 +26,30 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto if (payloadBytes == null) { throw new IllegalArgumentException("payloadBytes不能为空"); } - // 资源包正文使用随机 SM4 DEK 加密,再由用户密钥把 DEK 封装成 envelope。 - // 这样大 payload 仍走对称加密,ECC 只负责保护一次性数据密钥。 - byte[] sm4Key = randomBytes(16); - byte[] iv = randomBytes(16); try { - Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv)); - - // 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。 - EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest(); + // 资源包正文加密走 SDF 会话密钥流程:内部 ECC 公钥生成会话密钥,SDF_Encrypt 加密 payload。 + EccSessionKeyEncryptRequest encryptRequest = new EccSessionKeyEncryptRequest(); encryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX); - encryptRequest.setData(sm4Key); - BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest); + encryptRequest.setKeyBits(PAYLOAD_SESSION_KEY_BITS); + encryptRequest.setAlgId(PAYLOAD_ALG_ID); + encryptRequest.setData(payloadBytes); + encryptRequest.setOutBufferSize(payloadBytes.length + 16); + EccSessionKeyEncryptResult encryptResult = pcieCryptoService.encryptWithInternalEccSessionKey(encryptRequest); Map envelope = new LinkedHashMap<>(); envelope.put("version", 1); - envelope.put("payloadAlg", "SM4-CBC"); - envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC"); - envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv)); - envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek))); - envelope.put("dekLength", sm4Key.length); + envelope.put("payloadAlg", PAYLOAD_ALG_NAME); + envelope.put("ivBase64", Base64.getEncoder().encodeToString(nullToEmpty(encryptResult.getIv()))); + envelope.put("wrappedSessionKeyBase64", Base64.getEncoder().encodeToString(nullToEmpty(encryptResult.getWrappedSessionKey()))); ResourceSealedPayload sealedPayload = new ResourceSealedPayload(); - sealedPayload.setCipherBytes(cipher.doFinal(payloadBytes)); + sealedPayload.setCipherBytes(nullToEmpty(encryptResult.getEncryptedData())); sealedPayload.setEnvelope(envelope); sealedPayload.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT); sealedPayload.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT); return sealedPayload; } catch (Exception ex) { - throw new IllegalStateException("资源备份载荷加封失败", ex); - } finally { - wipe(sm4Key); + throw new IllegalStateException("资源备份载荷加封失败:" + rootCauseMessage(ex), ex); } } @@ -112,30 +95,28 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto throw new IllegalArgumentException("signature不能为空"); } verify(dataToVerify, signature); - - byte[] wrappedDek = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedDekBase64"))); - int dekLength = parseInt(envelope.get("dekLength"), 16); - // 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。 - UserKeyDecryptRequest decryptRequest = new UserKeyDecryptRequest(); - decryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX); - decryptRequest.setCipherBlob(wrappedDek); - byte[] sm4Key = normalizeAndWipe(pcieCryptoService.userKeyDecrypt(decryptRequest)); - byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64"))); - try { - Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv)); - return cipher.doFinal(cipherBytes); - } catch (Exception ex) { - throw new IllegalStateException("资源备份载荷解封失败", ex); - } finally { - wipe(sm4Key); + if (!Integer.valueOf(1).equals(envelope.get("version"))) { + throw new IllegalArgumentException("envelope版本不支持"); + } + if (!PAYLOAD_ALG_NAME.equals(String.valueOf(envelope.get("payloadAlg")))) { + throw new IllegalArgumentException("payload加密算法不支持"); } - } - private static byte[] randomBytes(int size) { - byte[] bytes = new byte[size]; - new java.security.SecureRandom().nextBytes(bytes); - return bytes; + byte[] wrappedSessionKey = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedSessionKeyBase64"))); + // 先验签,再导入会话密钥,最后 SDF_Decrypt 解 payload;顺序固定,避免篡改包先进入明文处理路径。 + EccSessionKeyDecryptRequest decryptRequest = new EccSessionKeyDecryptRequest(); + decryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX); + decryptRequest.setAlgId(PAYLOAD_ALG_ID); + decryptRequest.setWrappedSessionKey(wrappedSessionKey); + decryptRequest.setEncryptedData(cipherBytes); + decryptRequest.setIv(Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")))); + decryptRequest.setOutBufferSize(cipherBytes.length); + try { + EccSessionKeyDecryptResult decryptResult = pcieCryptoService.decryptWithInternalEccSessionKey(decryptRequest); + return nullToEmpty(decryptResult.getDecryptedData()); + } catch (Exception ex) { + throw new IllegalStateException("资源备份载荷解封失败:" + rootCauseMessage(ex), ex); + } } private static byte[] sm3(byte[] data) { @@ -158,33 +139,16 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto return normalized; } - private static byte[] normalizeAndWipe(BackupDataResult result) { - try { - return normalize(result); - } finally { - if (result != null) { - wipe(result.getData()); - } - } + private static byte[] nullToEmpty(byte[] bytes) { + return bytes == null ? new byte[0] : bytes; } - private static int parseInt(Object value, int defaultValue) { - if (value == null) { - return defaultValue; - } - if (value instanceof Number number) { - return number.intValue(); - } - try { - return Integer.parseInt(String.valueOf(value)); - } catch (NumberFormatException ex) { - return defaultValue; - } - } - - private static void wipe(byte[] bytes) { - if (bytes != null) { - Arrays.fill(bytes, (byte) 0); + private static String rootCauseMessage(Throwable throwable) { + Throwable current = throwable; + while (current.getCause() != null) { + current = current.getCause(); } + String message = current.getMessage(); + return message == null || message.isBlank() ? current.getClass().getSimpleName() : message; } } diff --git a/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java b/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java index d13c65c..49c35f8 100644 --- a/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java +++ b/src/main/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncher.java @@ -20,19 +20,25 @@ public class LightweightRestoreApplierLauncher { private final String datasourceUrl; private final String datasourceUsername; private final String datasourcePassword; + private final String standardHomeDir; + private final String standardRunUser; public LightweightRestoreApplierLauncher( ResourceBackupProperties properties, RestoreCommandInvoker invoker, @Value("${spring.datasource.url:}") String datasourceUrl, @Value("${spring.datasource.username:}") String datasourceUsername, - @Value("${spring.datasource.password:}") String datasourcePassword + @Value("${spring.datasource.password:}") String datasourcePassword, + @Value("${tms.init.executor.standard-home-dir:/home/cmep4i}") String standardHomeDir, + @Value("${tms.init.executor.standard-run-user:cmep4i}") String standardRunUser ) { this.properties = properties; this.invoker = invoker; this.datasourceUrl = datasourceUrl; this.datasourceUsername = datasourceUsername; this.datasourcePassword = datasourcePassword; + this.standardHomeDir = standardHomeDir; + this.standardRunUser = standardRunUser; } public void launch(Path workDir) { @@ -115,7 +121,11 @@ public class LightweightRestoreApplierLauncher { values.put("DB_PASSWORD", trim(datasourcePassword)); values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl())); values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds())); + values.put("HEALTH_CHECK_MAX_WAIT_SECONDS", String.valueOf(properties.getHealthCheckMaxWaitSeconds())); + values.put("HEALTH_CHECK_RETRY_INTERVAL_SECONDS", String.valueOf(properties.getHealthCheckRetryIntervalSeconds())); values.put("RESTORE_APPLY_DELAY_SECONDS", "3"); + values.put("STANDARD_HOME_DIR", trim(standardHomeDir)); + values.put("STANDARD_RUN_USER", trim(standardRunUser)); StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n"); for (Map.Entry entry : values.entrySet()) { diff --git a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceBackupServiceImpl.java b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceBackupServiceImpl.java index e18bd9c..ae22042 100644 --- a/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceBackupServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/backup/service/impl/ResourceBackupServiceImpl.java @@ -114,7 +114,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService { } catch (RuntimeException ex) { entity.setStatus("FAILED"); entity.setErrorCode("PACKAGE_CREATE_FAILED"); - entity.setErrorMessage(ex.getMessage()); + entity.setErrorMessage(rootCauseAwareMessage(ex)); } entity.setFinishTime(LocalDateTime.now()); entity.setUpdateTime(entity.getFinishTime()); @@ -229,4 +229,20 @@ public class ResourceBackupServiceImpl implements ResourceBackupService { return "{}"; } } + + private static String rootCauseAwareMessage(Throwable throwable) { + String message = throwable.getMessage(); + Throwable current = throwable; + while (current.getCause() != null) { + current = current.getCause(); + } + String rootMessage = current.getMessage(); + if (rootMessage == null || rootMessage.isBlank() || rootMessage.equals(message)) { + return message == null || message.isBlank() ? current.getClass().getSimpleName() : message; + } + if (message == null || message.isBlank()) { + return rootMessage; + } + return message + ";原因:" + rootMessage; + } } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java index a7611b2..b41335b 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java @@ -19,6 +19,8 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestResult; import com.cisd.tms.integration.crypto.pcie.model.ExchangeDigitEnvelopeEccRequest; import com.cisd.tms.integration.crypto.pcie.model.ExchangeDigitEnvelopeRsaRequest; import com.cisd.tms.integration.crypto.pcie.model.ExportKekComponentRequest; @@ -401,6 +403,40 @@ public class CryptoCardController { )); } + @PostMapping("/session-key/ecc/internal-crypto-self-test") + @Operation(summary = "执行内部 ECC 会话密钥加解密自测", description = "使用索引 1 的内部 ECC 密钥生成并导入会话密钥,未传 IV 时使用密码卡随机数接口生成 IV,完成 SDF 加密和解密闭环。") + public ApiResponse> eccSessionKeyCryptoSelfTest( + @Valid @RequestBody CryptoEccSessionKeyCryptoSelfTestInternalRequest request + ) { + EccSessionKeyCryptoSelfTestRequest req = new EccSessionKeyCryptoSelfTestRequest(); + req.setKeyIndex(1); + req.setKeyBits(128); + req.setAlgId(Gm0018AlgorithmIds.SM4_CBC); + req.setData(request.getPlainText().getBytes(StandardCharsets.UTF_8)); + req.setEncOutBufferSize(4096); + req.setDecOutBufferSize(4096); + + EccSessionKeyCryptoSelfTestResult result = pcieCryptoService.eccSessionKeyEncryptDecryptSelfTest(req); + byte[] wrappedSessionKey = result.getWrappedSessionKey() == null ? new byte[0] : result.getWrappedSessionKey(); + byte[] encryptedData = result.getEncryptedData() == null ? new byte[0] : result.getEncryptedData(); + byte[] decryptedData = result.getDecryptedData() == null ? new byte[0] : result.getDecryptedData(); + + Map payload = new LinkedHashMap<>(); + payload.put("keyIndex", result.getKeyIndex()); + payload.put("keyBits", result.getKeyBits()); + payload.put("algId", result.getAlgId()); + payload.put("ivBase64", result.getIv() == null ? null : Base64.getEncoder().encodeToString(result.getIv())); + payload.put("wrappedSessionKeyLength", wrappedSessionKey.length); + payload.put("wrappedSessionKeyBase64", Base64.getEncoder().encodeToString(wrappedSessionKey)); + payload.put("encryptedDataLength", encryptedData.length); + payload.put("encryptedDataBase64", Base64.getEncoder().encodeToString(encryptedData)); + payload.put("decryptedLength", decryptedData.length); + payload.put("decryptedBase64", Base64.getEncoder().encodeToString(decryptedData)); + payload.put("decryptedText", new String(decryptedData, StandardCharsets.UTF_8)); + payload.put("matched", result.isMatched()); + return ApiResponse.success(payload); + } + @PostMapping("/device/init-identify") @Operation(summary = "初始化识别码", description = "使用旧 PIN 和新 PIN 初始化设备识别码。") public ApiResponse initIdentify(@Valid @RequestBody CryptoInitIdentifyInternalRequest request) { diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoEccSessionKeyCryptoSelfTestInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoEccSessionKeyCryptoSelfTestInternalRequest.java new file mode 100644 index 0000000..bdcc1de --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoEccSessionKeyCryptoSelfTestInternalRequest.java @@ -0,0 +1,17 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoEccSessionKeyCryptoSelfTestInternalRequest { + + @NotBlank + private String plainText; + + public String getPlainText() { + return plainText; + } + + public void setPlainText(String plainText) { + this.plainText = plainText; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 97234ff..46b12bb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -315,6 +315,10 @@ tms: health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8080/actuator/health} # 健康检查请求超时(秒)。 health-check-timeout-seconds: ${TMS_BACKUP_HEALTH_CHECK_TIMEOUT_SECONDS:5} + # 健康检查最大等待时间(秒);TMS 启动命令返回后,Spring Boot 仍可能需要一段时间才监听端口。 + health-check-max-wait-seconds: ${TMS_BACKUP_HEALTH_CHECK_MAX_WAIT_SECONDS:180} + # 健康检查重试间隔(秒)。 + health-check-retry-interval-seconds: ${TMS_BACKUP_HEALTH_CHECK_RETRY_INTERVAL_SECONDS:5} mk: init-identify: # SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。 diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java index 8e05b0f..0997ea6 100644 --- a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java @@ -14,6 +14,12 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptResult; import com.cisd.tms.integration.crypto.pcie.model.FirmwareInfoResult; import com.cisd.tms.integration.crypto.pcie.model.IkSignRequest; import com.cisd.tms.integration.crypto.pcie.model.IkVerifyRequest; @@ -27,8 +33,10 @@ import com.sun.jna.Pointer; import com.sun.jna.ptr.IntByReference; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -238,6 +246,288 @@ class JnaPcieCryptoServiceTest { Mockito.verify(sessionTemplate).withSession(Mockito.eq("SDFE_VerifyIK"), Mockito.any()); } + @Test + void shouldGenerateEncryptImportAndDecryptWithInternalEccSessionKey() { + PcieSessionTemplate sessionTemplate = Mockito.mock(PcieSessionTemplate.class); + CryptoCardProperties properties = new CryptoCardProperties(); + JnaPcieCryptoService service = new JnaPcieCryptoService(sessionTemplate, properties); + EccSessionKeyCryptoSelfTestRequest request = new EccSessionKeyCryptoSelfTestRequest(); + request.setKeyIndex(1); + request.setKeyBits(128); + request.setAlgId(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC); + request.setData("hello".getBytes(StandardCharsets.UTF_8)); + request.setEncOutBufferSize(64); + request.setDecOutBufferSize(64); + + Mockito.when(sessionTemplate.withSession(Mockito.eq("SDF_GenerateKeyWithIPK_ECC+self-test"), Mockito.any())) + .thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + PcieSessionTemplate.SessionCallback callback = + (PcieSessionTemplate.SessionCallback) invocation.getArgument(1); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + Pointer session = Pointer.createConstant(2); + Pointer generatedHandle = Pointer.createConstant(101); + Pointer importedHandle = Pointer.createConstant(202); + byte[] paddedPlain = new byte[] { + 0x68, 0x65, 0x6C, 0x6C, + 0x6F, 0x0B, 0x0B, 0x0B, + 0x0B, 0x0B, 0x0B, 0x0B, + 0x0B, 0x0B, 0x0B, 0x0B + }; + byte[] cipher = new byte[] { + 0x21, 0x43, 0x65, 0x01, + 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, + 0x0A, 0x0B, 0x0C, 0x0D + }; + byte[] generatedIv = new byte[] { + 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F + }; + Mockito.when(lib.SDF_GenerateRandom(Mockito.eq(session), Mockito.eq(16), Mockito.any(byte[].class))) + .thenAnswer(call -> { + byte[] out = call.getArgument(2); + System.arraycopy(generatedIv, 0, out, 0, generatedIv.length); + return 0; + }); + Mockito.when(lib.SDF_GenerateKeyWithIPK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.eq(128), + Mockito.any(EccCipher.class), Mockito.any())) + .thenAnswer(call -> { + EccCipher wrapped = call.getArgument(3); + wrapped.l = 3; + wrapped.c[0] = 0x01; + wrapped.c[1] = 0x02; + wrapped.c[2] = 0x03; + com.sun.jna.ptr.PointerByReference handleRef = call.getArgument(4); + handleRef.setValue(generatedHandle); + return 0; + }); + Mockito.when(lib.SDF_Encrypt(Mockito.eq(session), Mockito.eq(generatedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), Mockito.eq(16), Mockito.any(), Mockito.any())) + .thenAnswer(call -> { + byte[] out = call.getArgument(6); + System.arraycopy(cipher, 0, out, 0, cipher.length); + IntByReference outLen = call.getArgument(7); + outLen.setValue(cipher.length); + return 0; + }); + Mockito.when(lib.SDF_GetPrivateKeyAccessRight(Mockito.eq(session), Mockito.eq(1), Mockito.any(), Mockito.anyInt())) + .thenReturn(0); + Mockito.when(lib.SDF_ImportKeyWithISK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.any(EccCipher.class), Mockito.any())) + .thenAnswer(call -> { + com.sun.jna.ptr.PointerByReference handleRef = call.getArgument(3); + handleRef.setValue(importedHandle); + return 0; + }); + Mockito.when(lib.SDF_Decrypt(Mockito.eq(session), Mockito.eq(importedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), Mockito.eq(16), Mockito.any(), Mockito.any())) + .thenAnswer(call -> { + byte[] out = call.getArgument(6); + System.arraycopy(paddedPlain, 0, out, 0, paddedPlain.length); + IntByReference outLen = call.getArgument(7); + outLen.setValue(paddedPlain.length); + return 0; + }); + Mockito.when(lib.SDF_ReleasePrivateKeyAccessRight(Mockito.eq(session), Mockito.eq(1))).thenReturn(0); + Mockito.when(lib.SDF_DestroyKey(Mockito.eq(session), Mockito.any())).thenReturn(0); + + EccSessionKeyCryptoSelfTestResult result = callback.apply(lib, Pointer.createConstant(1), session); + + InOrder inOrder = Mockito.inOrder(lib); + inOrder.verify(lib).SDF_GenerateRandom(Mockito.eq(session), Mockito.eq(16), Mockito.any(byte[].class)); + inOrder.verify(lib).SDF_GenerateKeyWithIPK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.eq(128), + Mockito.any(EccCipher.class), Mockito.any()); + inOrder.verify(lib).SDF_Encrypt(Mockito.eq(session), Mockito.eq(generatedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), Mockito.eq(16), Mockito.any(), Mockito.any()); + inOrder.verify(lib).SDF_DestroyKey(session, generatedHandle); + inOrder.verify(lib).SDF_GetPrivateKeyAccessRight(Mockito.eq(session), Mockito.eq(1), Mockito.any(), Mockito.anyInt()); + inOrder.verify(lib).SDF_ImportKeyWithISK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.any(EccCipher.class), Mockito.any()); + inOrder.verify(lib).SDF_Decrypt(Mockito.eq(session), Mockito.eq(importedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), Mockito.eq(16), Mockito.any(), Mockito.any()); + inOrder.verify(lib).SDF_DestroyKey(session, importedHandle); + inOrder.verify(lib).SDF_ReleasePrivateKeyAccessRight(session, 1); + ArgumentCaptor encryptIvCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor encryptDataCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor decryptIvCaptor = ArgumentCaptor.forClass(byte[].class); + Mockito.verify(lib).SDF_Encrypt(Mockito.eq(session), Mockito.eq(generatedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + encryptIvCaptor.capture(), encryptDataCaptor.capture(), Mockito.eq(16), Mockito.any(), Mockito.any()); + Mockito.verify(lib).SDF_Decrypt(Mockito.eq(session), Mockito.eq(importedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + decryptIvCaptor.capture(), Mockito.any(), Mockito.eq(16), Mockito.any(), Mockito.any()); + Assertions.assertArrayEquals(generatedIv, encryptIvCaptor.getValue()); + Assertions.assertArrayEquals(generatedIv, decryptIvCaptor.getValue()); + Assertions.assertArrayEquals(paddedPlain, encryptDataCaptor.getValue()); + Assertions.assertArrayEquals(generatedIv, result.getIv()); + return result; + }); + + EccSessionKeyCryptoSelfTestResult result = service.eccSessionKeyEncryptDecryptSelfTest(request); + + Assertions.assertEquals(1, result.getKeyIndex()); + Assertions.assertEquals(128, result.getKeyBits()); + Assertions.assertArrayEquals(new byte[] { + 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F + }, result.getIv()); + Assertions.assertArrayEquals(new byte[] { + 0x21, 0x43, 0x65, 0x01, + 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, + 0x0A, 0x0B, 0x0C, 0x0D + }, result.getEncryptedData()); + Assertions.assertArrayEquals("hello".getBytes(StandardCharsets.UTF_8), result.getDecryptedData()); + Assertions.assertTrue(result.isMatched()); + } + + @Test + void shouldEncryptLargePayloadWithInternalEccSessionKeyInBlocks() { + PcieSessionTemplate sessionTemplate = Mockito.mock(PcieSessionTemplate.class); + CryptoCardProperties properties = new CryptoCardProperties(); + JnaPcieCryptoService service = new JnaPcieCryptoService(sessionTemplate, properties); + AtomicReference libRef = new AtomicReference<>(); + Pointer session = Pointer.createConstant(2); + Pointer generatedHandle = Pointer.createConstant(101); + EccSessionKeyEncryptRequest request = new EccSessionKeyEncryptRequest(); + request.setKeyIndex(1); + request.setKeyBits(128); + request.setAlgId(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC); + byte[] plain = new byte[5000]; + Arrays.fill(plain, (byte) 0x31); + request.setData(plain); + request.setOutBufferSize(plain.length + 16); + + Mockito.when(sessionTemplate.withSession(Mockito.eq("SDF_GenerateKeyWithIPK_ECC+SDF_Encrypt"), Mockito.any())) + .thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + PcieSessionTemplate.SessionCallback callback = + (PcieSessionTemplate.SessionCallback) invocation.getArgument(1); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + libRef.set(lib); + byte[] generatedIv = new byte[] { + 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F + }; + Mockito.when(lib.SDF_GenerateRandom(Mockito.eq(session), Mockito.eq(16), Mockito.any(byte[].class))) + .thenAnswer(call -> { + byte[] out = call.getArgument(2); + System.arraycopy(generatedIv, 0, out, 0, generatedIv.length); + return 0; + }); + Mockito.when(lib.SDF_GenerateKeyWithIPK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.eq(128), + Mockito.any(EccCipher.class), Mockito.any())) + .thenAnswer(call -> { + EccCipher wrapped = call.getArgument(3); + wrapped.l = 3; + wrapped.c[0] = 0x01; + wrapped.c[1] = 0x02; + wrapped.c[2] = 0x03; + com.sun.jna.ptr.PointerByReference handleRef = call.getArgument(4); + handleRef.setValue(generatedHandle); + return 0; + }); + Mockito.doAnswer(call -> { + int inputLength = call.getArgument(5); + if (inputLength > 4096) { + return 0x100001d; + } + byte[] input = call.getArgument(4); + byte[] out = call.getArgument(6); + System.arraycopy(input, 0, out, 0, inputLength); + IntByReference outLen = call.getArgument(7); + outLen.setValue(inputLength); + return 0; + }) + .when(lib).SDF_Encrypt(Mockito.any(), Mockito.any(), + Mockito.anyInt(), Mockito.any(), Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.any()); + Mockito.when(lib.SDF_DestroyKey(Mockito.eq(session), Mockito.eq(generatedHandle))).thenReturn(0); + return callback.apply(lib, Pointer.createConstant(1), session); + }); + + EccSessionKeyEncryptResult result = service.encryptWithInternalEccSessionKey(request); + + Assertions.assertEquals(5008, result.getEncryptedData().length); + ArgumentCaptor encryptLengthCaptor = ArgumentCaptor.forClass(Integer.class); + Mockito.verify(libRef.get(), Mockito.atLeast(2)).SDF_Encrypt(Mockito.eq(session), Mockito.eq(generatedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), encryptLengthCaptor.capture(), Mockito.any(), Mockito.any()); + Assertions.assertTrue(encryptLengthCaptor.getAllValues().stream().allMatch(length -> length <= 4096)); + } + + @Test + void shouldDecryptLargePayloadWithInternalEccSessionKeyInBlocks() { + PcieSessionTemplate sessionTemplate = Mockito.mock(PcieSessionTemplate.class); + CryptoCardProperties properties = new CryptoCardProperties(); + JnaPcieCryptoService service = new JnaPcieCryptoService(sessionTemplate, properties); + AtomicReference libRef = new AtomicReference<>(); + Pointer session = Pointer.createConstant(2); + Pointer importedHandle = Pointer.createConstant(202); + byte[] encryptedData = new byte[5008]; + Arrays.fill(encryptedData, (byte) 0x31); + Arrays.fill(encryptedData, encryptedData.length - 8, encryptedData.length, (byte) 0x08); + + EccSessionKeyDecryptRequest request = new EccSessionKeyDecryptRequest(); + request.setKeyIndex(1); + request.setAlgId(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC); + request.setWrappedSessionKey(new byte[new EccCipher().size()]); + request.setEncryptedData(encryptedData); + request.setIv(new byte[16]); + request.setOutBufferSize(encryptedData.length); + + Mockito.when(sessionTemplate.withSession(Mockito.eq("SDF_ImportKeyWithISK_ECC+SDF_Decrypt"), Mockito.any())) + .thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + PcieSessionTemplate.SessionCallback callback = + (PcieSessionTemplate.SessionCallback) invocation.getArgument(1); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + libRef.set(lib); + Mockito.when(lib.SDF_GetPrivateKeyAccessRight(Mockito.eq(session), Mockito.eq(1), Mockito.any(), Mockito.anyInt())) + .thenReturn(0); + Mockito.when(lib.SDF_ImportKeyWithISK_ECC(Mockito.eq(session), Mockito.eq(1), Mockito.any(EccCipher.class), Mockito.any())) + .thenAnswer(call -> { + com.sun.jna.ptr.PointerByReference handleRef = call.getArgument(3); + handleRef.setValue(importedHandle); + return 0; + }); + Mockito.doAnswer(call -> { + int inputLength = call.getArgument(5); + if (inputLength > 4096) { + return 0x100001d; + } + byte[] input = call.getArgument(4); + byte[] out = call.getArgument(6); + System.arraycopy(input, 0, out, 0, inputLength); + IntByReference outLen = call.getArgument(7); + outLen.setValue(inputLength); + return 0; + }) + .when(lib).SDF_Decrypt(Mockito.any(), Mockito.any(), + Mockito.anyInt(), Mockito.any(), Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.any()); + Mockito.when(lib.SDF_DestroyKey(Mockito.eq(session), Mockito.eq(importedHandle))).thenReturn(0); + Mockito.when(lib.SDF_ReleasePrivateKeyAccessRight(Mockito.eq(session), Mockito.eq(1))).thenReturn(0); + return callback.apply(lib, Pointer.createConstant(1), session); + }); + + EccSessionKeyDecryptResult result = service.decryptWithInternalEccSessionKey(request); + + Assertions.assertEquals(5000, result.getDecryptedData().length); + ArgumentCaptor decryptLengthCaptor = ArgumentCaptor.forClass(Integer.class); + Mockito.verify(libRef.get(), Mockito.atLeast(2)).SDF_Decrypt(Mockito.eq(session), Mockito.eq(importedHandle), + Mockito.eq(com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds.SM4_CBC), + Mockito.any(), Mockito.any(), decryptLengthCaptor.capture(), Mockito.any(), Mockito.any()); + Assertions.assertTrue(decryptLengthCaptor.getAllValues().stream().allMatch(length -> length <= 4096)); + } + @Test void shouldReadFirmwareInfoViaSdfeGetFirmwareInfo() { PcieSessionTemplate sessionTemplate = Mockito.mock(PcieSessionTemplate.class); diff --git a/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java index 33dd7c0..510a4d7 100644 --- a/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/ResourceBackupPropertiesTest.java @@ -28,6 +28,8 @@ class ResourceBackupPropertiesTest { Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_account")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_ukey_binding")); Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_auth_session")); + Assertions.assertEquals(180, properties.getHealthCheckMaxWaitSeconds()); + Assertions.assertEquals(5, properties.getHealthCheckRetryIntervalSeconds()); } @Test diff --git a/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java index dcc2010..e2d2619 100644 --- a/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/crypto/ResourceEnvelopeCryptoServiceTest.java @@ -1,8 +1,10 @@ package com.cisd.tms.modules.backup.crypto; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptResult; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; @@ -16,26 +18,30 @@ import org.mockito.Mockito; class ResourceEnvelopeCryptoServiceTest { @Test - void shouldWrapDekWithEncUserKeyAndSignWithAuthUserKey() { + void shouldEncryptPayloadWithSdfEccSessionKeyAndSignWithAuthUserKey() { PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); - AtomicReference capturedDek = new AtomicReference<>(); - AtomicReference decryptableDek = new AtomicReference<>(); - AtomicReference decryptResultDek = new AtomicReference<>(); - Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) + AtomicReference capturedPlainPayload = new AtomicReference<>(); + Mockito.when(pcieCryptoService.encryptWithInternalEccSessionKey(Mockito.any(EccSessionKeyEncryptRequest.class))) .thenAnswer(invocation -> { - EccInternalEncryptRequest request = invocation.getArgument(0); - capturedDek.set(request.getData()); - decryptableDek.set(Arrays.copyOf(request.getData(), request.getData().length)); - return backup(new byte[]{0x11, 0x22, 0x33}); + EccSessionKeyEncryptRequest request = invocation.getArgument(0); + capturedPlainPayload.set(request.getData()); + EccSessionKeyEncryptResult result = new EccSessionKeyEncryptResult(); + result.setKeyIndex(request.getKeyIndex()); + result.setKeyBits(request.getKeyBits()); + result.setAlgId(request.getAlgId()); + result.setIv(new byte[]{0x01, 0x02, 0x03, 0x04}); + result.setWrappedSessionKey(new byte[]{0x11, 0x22, 0x33}); + result.setEncryptedData(new byte[]{0x66, 0x77, 0x55}); + return result; }); Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any(UserKeySignRequest.class))) .thenReturn(backup(new byte[]{0x44, 0x55, 0x66})); - Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) + Mockito.when(pcieCryptoService.decryptWithInternalEccSessionKey(Mockito.any(EccSessionKeyDecryptRequest.class))) .thenAnswer(invocation -> { - byte[] decryptedDek = Arrays.copyOf(decryptableDek.get(), decryptableDek.get().length); - decryptResultDek.set(decryptedDek); - return backup(decryptedDek); + EccSessionKeyDecryptResult result = new EccSessionKeyDecryptResult(); + result.setDecryptedData(Arrays.copyOf(capturedPlainPayload.get(), capturedPlainPayload.get().length)); + return result; }); ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService); @@ -48,10 +54,23 @@ class ResourceEnvelopeCryptoServiceTest { Assertions.assertArrayEquals(payload, opened); Assertions.assertArrayEquals(new byte[]{0x44, 0x55, 0x66}, signature); + Assertions.assertEquals(1, sealed.getEnvelope().get("version")); + Assertions.assertEquals("SDF_SM4_CBC", sealed.getEnvelope().get("payloadAlg")); + Assertions.assertTrue(sealed.getEnvelope().containsKey("ivBase64")); + Assertions.assertTrue(sealed.getEnvelope().containsKey("wrappedSessionKeyBase64")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("wrappedDekBase64")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("dekLength")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("keyIndex")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("keyBits")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("algId")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("sessionKeyAlg")); + Assertions.assertFalse(sealed.getEnvelope().containsKey("sessionKeyImportAlg")); - ArgumentCaptor encryptCaptor = ArgumentCaptor.forClass(EccInternalEncryptRequest.class); - Mockito.verify(pcieCryptoService).sdfeInternalEncryptEcc(encryptCaptor.capture()); + ArgumentCaptor encryptCaptor = ArgumentCaptor.forClass(EccSessionKeyEncryptRequest.class); + Mockito.verify(pcieCryptoService).encryptWithInternalEccSessionKey(encryptCaptor.capture()); Assertions.assertEquals(1, encryptCaptor.getValue().getKeyIndex()); + Assertions.assertEquals(128, encryptCaptor.getValue().getKeyBits()); + Assertions.assertArrayEquals(payload, encryptCaptor.getValue().getData()); ArgumentCaptor signCaptor = ArgumentCaptor.forClass(UserKeySignRequest.class); Mockito.verify(pcieCryptoService).userKeySignWithSm3(signCaptor.capture()); @@ -61,11 +80,12 @@ class ResourceEnvelopeCryptoServiceTest { Mockito.verify(pcieCryptoService).eccInternalVerify(verifyCaptor.capture()); Assertions.assertEquals(1, verifyCaptor.getValue().getKeyIndex()); - ArgumentCaptor decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class); - Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture()); + ArgumentCaptor decryptCaptor = ArgumentCaptor.forClass(EccSessionKeyDecryptRequest.class); + Mockito.verify(pcieCryptoService).decryptWithInternalEccSessionKey(decryptCaptor.capture()); Assertions.assertEquals(1, decryptCaptor.getValue().getKeyIndex()); - Assertions.assertTrue(isAllZero(capturedDek.get()), "generated SM4 DEK should be wiped after sealing"); - Assertions.assertTrue(isAllZero(decryptResultDek.get()), "decrypted SM4 DEK result buffer should be wiped after unsealing"); + Assertions.assertArrayEquals(new byte[]{0x66, 0x77, 0x55}, decryptCaptor.getValue().getEncryptedData()); + Mockito.verify(pcieCryptoService, Mockito.never()).sdfeInternalEncryptEcc(Mockito.any()); + Mockito.verify(pcieCryptoService, Mockito.never()).sdfeInternalDecryptEcc(Mockito.any()); } private static BackupDataResult backup(byte[] data) { @@ -75,15 +95,4 @@ class ResourceEnvelopeCryptoServiceTest { return result; } - private static boolean isAllZero(byte[] bytes) { - if (bytes == null || bytes.length == 0) { - return false; - } - for (byte value : bytes) { - if (value != 0) { - return false; - } - } - return true; - } } diff --git a/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java index 14dd93b..9435d53 100644 --- a/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/packagex/ResourcePackageServiceTest.java @@ -4,8 +4,10 @@ import com.cisd.tms.modules.backup.config.ResourceBackupProperties; import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl; import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyDecryptResult; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyEncryptResult; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest; import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem; @@ -45,19 +47,7 @@ class ResourcePackageServiceTest { properties.setOutputDir(tempDir.toString()); Path payloadFile = writePayloadFile("resource/application.yml", "server.port=8080"); PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); - Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) - .thenAnswer(invocation -> { - EccInternalEncryptRequest request = invocation.getArgument(0); - return backup(request.getData()); - }); - Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any())) - .thenReturn(backup(new byte[]{0x11, 0x22, 0x33})); - Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) - .thenAnswer(invocation -> { - EccInternalDecryptRequest request = invocation.getArgument(0); - return backup(request.getCipherBlob()); - }); - Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); + mockEnvelopeCrypto(pcieCryptoService); ResourcePackageService service = new ResourcePackageServiceImpl( properties, new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService), @@ -177,13 +167,7 @@ class ResourcePackageServiceTest { Files.writeString(cmspDir.resolve("application-prd-node2.properties"), "spring.rabbitmq.username=user2"); Files.writeString(cmspDir.resolve("application-dev.properties"), "spring.rabbitmq.username=dev"); PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); - Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) - .thenAnswer(invocation -> backup(invocation.getArgument(0).getData())); - Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) - .thenAnswer(invocation -> backup(invocation.getArgument(0).getCipherBlob())); - Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any())) - .thenReturn(backup(new byte[]{0x11, 0x22, 0x33})); - Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); + mockEnvelopeCrypto(pcieCryptoService); ResourcePackageService service = new ResourcePackageServiceImpl( properties, new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService), @@ -224,13 +208,7 @@ class ResourcePackageServiceTest { ResourceBackupProperties properties = new ResourceBackupProperties(); properties.setOutputDir(tempDir.toString()); PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); - Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) - .thenAnswer(invocation -> backup(invocation.getArgument(0).getData())); - Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any())) - .thenReturn(backup(new byte[]{0x11, 0x22, 0x33})); - Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class))) - .thenAnswer(invocation -> backup(invocation.getArgument(0).getCipherBlob())); - Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); + mockEnvelopeCrypto(pcieCryptoService); Mockito.when(pcieCryptoService.backupUserKey(2, PcieUserKeyType.SIGN.getCode(), 4096)) .thenReturn(backup(new byte[]{0x02, 0x20})); Mockito.when(pcieCryptoService.backupUserKey(3, PcieUserKeyType.SIGN.getCode(), 4096)) @@ -284,6 +262,31 @@ class ResourcePackageServiceTest { return result; } + private static void mockEnvelopeCrypto(PcieCryptoService pcieCryptoService) { + Mockito.when(pcieCryptoService.encryptWithInternalEccSessionKey(Mockito.any(EccSessionKeyEncryptRequest.class))) + .thenAnswer(invocation -> { + EccSessionKeyEncryptRequest request = invocation.getArgument(0); + EccSessionKeyEncryptResult result = new EccSessionKeyEncryptResult(); + result.setKeyIndex(request.getKeyIndex()); + result.setKeyBits(request.getKeyBits()); + result.setAlgId(request.getAlgId()); + result.setIv(new byte[16]); + result.setWrappedSessionKey(new byte[]{0x11, 0x22, 0x33}); + result.setEncryptedData(request.getData()); + return result; + }); + Mockito.when(pcieCryptoService.decryptWithInternalEccSessionKey(Mockito.any(EccSessionKeyDecryptRequest.class))) + .thenAnswer(invocation -> { + EccSessionKeyDecryptRequest request = invocation.getArgument(0); + EccSessionKeyDecryptResult result = new EccSessionKeyDecryptResult(); + result.setDecryptedData(request.getEncryptedData()); + return result; + }); + Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any())) + .thenReturn(backup(new byte[]{0x11, 0x22, 0x33})); + Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); + } + private static InitTaskEntity sampleInitTask() { InitTaskEntity entity = new InitTaskEntity(); entity.setTaskId("INIT-20260408-000001"); diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java index 0fb80ff..3c72e3b 100644 --- a/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/restore/ApplyResourceBackupScriptContractTest.java @@ -128,6 +128,48 @@ class ApplyResourceBackupScriptContractTest { Assertions.assertTrue(log.contains("http://127.0.0.1:8080/actuator/health")); } + @Test + void shouldWaitForHealthCheckUntilServiceIsReady() throws Exception { + Path workDir = tempDir.resolve("work"); + Path payloadDir = workDir.resolve("payload"); + Files.createDirectories(payloadDir); + Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}"); + Path commandLog = tempDir.resolve("curl-retry.log"); + Path counter = tempDir.resolve("curl-count"); + Path binDir = tempDir.resolve("bin"); + Files.createDirectories(binDir); + Path curl = binDir.resolve("curl"); + Files.writeString(curl, """ + #!/usr/bin/env bash + count=0 + if [ -f "%s" ]; then + count="$(cat "%s")" + fi + count=$((count + 1)) + printf '%%s' "$count" > "%s" + printf 'attempt=%%s %%s\\n' "$count" "$*" >> "%s" + if [ "$count" -lt 3 ]; then + exit 7 + fi + printf '{"status":"UP"}' + """.formatted(counter, counter, counter, commandLog)); + curl.toFile().setExecutable(true); + + runScript(workDir, Map.of( + "HEALTH_CHECK_URL", "http://127.0.0.1:8080/actuator/health", + "HEALTH_CHECK_TIMEOUT_SECONDS", "1", + "HEALTH_CHECK_MAX_WAIT_SECONDS", "10", + "HEALTH_CHECK_RETRY_INTERVAL_SECONDS", "0", + "PATH", binDir + ":" + System.getenv("PATH") + )); + + String log = Files.readString(commandLog); + Assertions.assertTrue(log.contains("attempt=1")); + Assertions.assertTrue(log.contains("attempt=2")); + Assertions.assertTrue(log.contains("attempt=3")); + Assertions.assertTrue(Files.readString(workDir.resolve("status.json")).contains("\"status\":\"SUCCESS\"")); + } + @Test void shouldRunConfiguredStopAndStartCommandsInOrder() throws Exception { Path workDir = tempDir.resolve("work"); diff --git a/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java b/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java index cf3473b..088163a 100644 --- a/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/restore/LightweightRestoreApplierLauncherTest.java @@ -42,7 +42,9 @@ class LightweightRestoreApplierLauncherTest { invoker, "jdbc:mysql://db-host:4000/tms?useUnicode=true", "root", - "p'ass" + "p'ass", + "/home/cmep4i", + "cmep4i" ); Path workDir = tempDir.resolve("RRST-001"); @@ -65,6 +67,10 @@ class LightweightRestoreApplierLauncherTest { Assertions.assertTrue(env.contains("export DB_PORT='4000'")); Assertions.assertTrue(env.contains("export DB_USER='root'")); Assertions.assertTrue(env.contains("export DB_PASSWORD='p'\\''ass'")); + Assertions.assertTrue(env.contains("export HEALTH_CHECK_MAX_WAIT_SECONDS='180'")); + Assertions.assertTrue(env.contains("export HEALTH_CHECK_RETRY_INTERVAL_SECONDS='5'")); + Assertions.assertTrue(env.contains("export STANDARD_HOME_DIR='/home/cmep4i'")); + Assertions.assertTrue(env.contains("export STANDARD_RUN_USER='cmep4i'")); String stopCommands = Files.readString(workDir.toAbsolutePath().normalize().resolve("restore-stop-commands.tsv")); Assertions.assertTrue(stopCommands.contains("TMS\tfalse\t/home/tms/scripts/tms.sh stop")); diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourceBackupServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourceBackupServiceTest.java index 955594e..1dd389c 100644 --- a/src/test/java/com/cisd/tms/modules/backup/service/ResourceBackupServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/backup/service/ResourceBackupServiceTest.java @@ -122,6 +122,48 @@ class ResourceBackupServiceTest { Assertions.assertNull(saved.getPackagePath()); } + @Test + void shouldPersistPackageFailureWithRootCauseMessage() { + InitTaskRepository initTaskRepository = Mockito.mock(InitTaskRepository.class); + ResourceDiscoveryService resourceDiscoveryService = Mockito.mock(ResourceDiscoveryService.class); + ResourcePackageService resourcePackageService = Mockito.mock(ResourcePackageService.class); + ResourceBackupTaskRepository resourceBackupTaskRepository = Mockito.mock(ResourceBackupTaskRepository.class); + ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class); + + InitTaskEntity initTask = sampleInitTask(); + List discoveredItems = List.of(resource("TMS_CONFIG", "/home/tms/config", true)); + + Mockito.when(initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")).thenReturn(Optional.of(initTask)); + Mockito.when(resourceDiscoveryService.discover(initTask)).thenReturn(discoveredItems); + Mockito.when(resourcePackageService.createPackage( + Mockito.anyString(), + Mockito.anyString(), + Mockito.same(initTask), + Mockito.eq(discoveredItems), + Mockito.any(CreateResourceBackupRequest.class) + )).thenThrow(new IllegalStateException("资源备份载荷加封失败", + new RuntimeException("[SDF_Encrypt] 无效参数 (errorCode=16777245)"))); + + ResourceBackupService service = new ResourceBackupServiceImpl( + initTaskRepository, + resourceDiscoveryService, + resourcePackageService, + resourceBackupTaskRepository, + new ResourceTaskIdGenerator(), + admissionGuard + ); + + CreateResourceBackupResponse response = service.createBackupTask(sampleRequest()); + + Assertions.assertEquals("FAILED", response.getStatus()); + ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceBackupTaskEntity.class); + Mockito.verify(resourceBackupTaskRepository).update(captor.capture()); + ResourceBackupTaskEntity saved = captor.getValue(); + Assertions.assertEquals("PACKAGE_CREATE_FAILED", saved.getErrorCode()); + Assertions.assertTrue(saved.getErrorMessage().contains("资源备份载荷加封失败")); + Assertions.assertTrue(saved.getErrorMessage().contains("SDF_Encrypt")); + } + @Test void shouldPersistRunningTaskWithConfiguredKeyFingerprintsBeforePackaging() { InitTaskRepository initTaskRepository = Mockito.mock(InitTaskRepository.class); diff --git a/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckServiceTest.java b/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckServiceTest.java deleted file mode 100644 index 938cc61..0000000 --- a/src/test/java/com/cisd/tms/modules/backup/service/ResourcePrecheckServiceTest.java +++ /dev/null @@ -1,178 +0,0 @@ -//package com.cisd.tms.modules.backup.service; -// -//import com.cisd.tms.common.config.properties.CisdPresetProperties; -//import com.cisd.tms.common.exception.BizException; -//import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -//import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; -//import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -//import com.cisd.tms.modules.backup.config.ResourceBackupProperties; -//import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl; -//import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest; -//import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest; -//import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse; -//import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageResult; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageService; -//import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl; -//import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl; -//import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore; -//import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator; -//import com.cisd.tms.modules.file.entity.FileRecordEntity; -//import com.cisd.tms.modules.file.repository.FileRecordRepository; -//import com.cisd.tms.modules.init.entity.InitTaskEntity; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import java.nio.file.Path; -//import java.util.List; -//import java.util.Optional; -//import org.junit.jupiter.api.Assertions; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.io.TempDir; -//import org.mockito.Mockito; -// -//class ResourcePrecheckServiceTest { -// -// @TempDir -// Path tempDir; -// -// @Test -// void shouldOnlyVerifyHeaderWithoutDecryptingPayload() { -// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); -// ResourceBackupProperties backupProperties = new ResourceBackupProperties(); -// backupProperties.setOutputDir(tempDir.toString()); -// backupProperties.setStorageEncKeyIndex(7); -// backupProperties.setAuthenticitySignKeyIndex(9); -// PcieCryptoService pcieCryptoService = mockCrypto(); -// ResourcePackageService packageService = new ResourcePackageServiceImpl( -// backupProperties, -// new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties), -// new ObjectMapper() -// ); -// ResourcePackageResult packageResult = packageService.createPackage( -// "RBKP-TASK-20260414-000001", -// "RBKP-20260414-000001", -// sampleInitTask(), -// List.of(resource("TMS_CONFIG", "/home/tms/config")), -// new CreateResourceBackupRequest() -// ); -// FileRecordEntity fileRecordEntity = new FileRecordEntity(); -// fileRecordEntity.setFileId("FILE-20260414-000001"); -// fileRecordEntity.setStoragePath(packageResult.getPackagePath()); -// Mockito.when(fileRecordRepository.findByFileId("FILE-20260414-000001")).thenReturn(Optional.of(fileRecordEntity)); -// -// CisdPresetProperties presetProperties = new CisdPresetProperties(); -// presetProperties.setProductType("ENTERPRISE"); -// ResourcePrecheckService service = new ResourcePrecheckServiceImpl( -// fileRecordRepository, -// packageService, -// new InMemoryResourcePrecheckStateStore(), -// new ResourceTaskIdGenerator(), -// presetProperties, -// backupProperties -// ); -// -// ResourceRestorePrecheckResponse response = service.precheck(sampleRequest()); -// -// Assertions.assertTrue(response.getSignatureValid()); -// Assertions.assertTrue(response.getKeysetMatched()); -// Assertions.assertFalse(response.getPayloadDecrypted()); -// Assertions.assertInstanceOf(java.util.Map.class, response.getRestoreScopeSummary()); -// Assertions.assertTrue(response.getRestoreScopeSummary().toString().contains("resourceCount")); -// } -// -// @Test -// void shouldRejectRestoreWhenKeyFingerprintDoesNotMatch() { -// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); -// ResourceBackupProperties backupProperties = new ResourceBackupProperties(); -// backupProperties.setOutputDir(tempDir.toString()); -// backupProperties.setStorageEncKeyIndex(1); -// backupProperties.setAuthenticitySignKeyIndex(1); -// PcieCryptoService pcieCryptoService = mockCrypto(); -// ResourcePackageService packageService = new ResourcePackageServiceImpl( -// backupProperties, -// new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties), -// new ObjectMapper() -// ); -// ResourcePackageResult packageResult = packageService.createPackage( -// "RBKP-TASK-20260414-000002", -// "RBKP-20260414-000002", -// sampleInitTask(), -// List.of(resource("TMS_CONFIG", "/home/tms/config")), -// new CreateResourceBackupRequest() -// ); -// FileRecordEntity fileRecordEntity = new FileRecordEntity(); -// fileRecordEntity.setFileId("FILE-20260414-000002"); -// fileRecordEntity.setStoragePath(packageResult.getPackagePath()); -// Mockito.when(fileRecordRepository.findByFileId("FILE-20260414-000002")).thenReturn(Optional.of(fileRecordEntity)); -// -// CisdPresetProperties presetProperties = new CisdPresetProperties(); -// presetProperties.setProductType("ENTERPRISE"); -// ResourceBackupProperties mismatched = new ResourceBackupProperties(); -// mismatched.setStorageEncKeyIndex(3); -// mismatched.setAuthenticitySignKeyIndex(5); -// ResourcePrecheckService service = new ResourcePrecheckServiceImpl( -// fileRecordRepository, -// packageService, -// new InMemoryResourcePrecheckStateStore(), -// new ResourceTaskIdGenerator(), -// presetProperties, -// mismatched -// ); -// -// BizException exception = Assertions.assertThrows(BizException.class, () -> service.precheck(sampleMismatchRequest())); -// Assertions.assertTrue(exception.getMessage().contains("指纹")); -// Assertions.assertTrue(exception.getMessage().contains("不匹配")); -// } -// -// private ResourceRestorePrecheckRequest sampleRequest() { -// ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest(); -// request.setFileId("FILE-20260414-000001"); -// return request; -// } -// -// private ResourceRestorePrecheckRequest sampleMismatchRequest() { -// ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest(); -// request.setFileId("FILE-20260414-000002"); -// return request; -// } -// -// private static PcieCryptoService mockCrypto() { -// PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); -// Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class))) -// .thenAnswer(invocation -> { -// EccInternalEncryptRequest request = invocation.getArgument(0); -// return backup(request.getData()); -// }); -// Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())) -// .thenReturn(backup(new byte[]{0x21, 0x22, 0x23})); -// Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any()); -// return pcieCryptoService; -// } -// -// private static BackupDataResult backup(byte[] data) { -// BackupDataResult result = new BackupDataResult(); -// result.setData(data); -// result.setLength(data.length); -// return result; -// } -// -// private static InitTaskEntity sampleInitTask() { -// InitTaskEntity entity = new InitTaskEntity(); -// entity.setTaskId("INIT-20260408-000001"); -// entity.setTaskType("INIT"); -// entity.setStatus("SUCCESS"); -// entity.setProductType("ENTERPRISE"); -// entity.setMqType("RABBITMQ"); -// entity.setOrgCode("AAAABBBBXXX"); -// entity.setDeployMode("SINGLE"); -// return entity; -// } -// -// private static DiscoveredResourceItem resource(String code, String path) { -// DiscoveredResourceItem item = new DiscoveredResourceItem(); -// item.setResourceType("FILE"); -// item.setResourceCode(code); -// item.setResolvedPath(path); -// item.setRequired(true); -// return item; -// } -//} diff --git a/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java b/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java index 650abb7..e8db06e 100644 --- a/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java @@ -9,6 +9,8 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccSessionKeyCryptoSelfTestResult; import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest; import com.cisd.tms.integration.crypto.pcie.model.KeyStatusResult; import com.cisd.tms.integration.crypto.pcie.model.PrivateKeyAccessRequest; @@ -20,6 +22,7 @@ import com.cisd.tms.integration.crypto.pcie.model.UserKeySm2VerifyRequest; import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest; import com.cisd.tms.integration.crypto.pcie.model.UserKeyVerifyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoEccSessionKeyCryptoSelfTestInternalRequest; import org.mockito.ArgumentCaptor; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -31,6 +34,7 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -338,6 +342,65 @@ class CryptoCardControllerTest { assertArrayEquals(new byte[] {0x21, 0x43, 0x65}, captor.getValue().getCipherBlob()); } + @Test + void shouldRunEccSessionKeyCryptoSelfTestWithPlaintextInput() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + EccSessionKeyCryptoSelfTestResult result = new EccSessionKeyCryptoSelfTestResult(); + result.setKeyIndex(1); + result.setKeyBits(128); + result.setAlgId(Gm0018AlgorithmIds.SM4_CBC); + result.setIv(new byte[] {0x01, 0x02}); + result.setWrappedSessionKey(new byte[] {0x01, 0x02, 0x03}); + result.setEncryptedData(new byte[] {0x11, 0x22, 0x33, 0x44}); + result.setDecryptedData("hello".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + result.setMatched(true); + Mockito.when(pcieCryptoService.eccSessionKeyEncryptDecryptSelfTest(Mockito.any())).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/device/crypto/session-key/ecc/internal-crypto-self-test") + .contentType("application/json") + .content(""" + { + "plainText": "hello" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"keyIndex\":1"))) + .andExpect(content().string(containsString("\"keyBits\":128"))) + .andExpect(content().string(containsString("\"algId\":1026"))) + .andExpect(content().string(containsString("\"ivBase64\":\"AQI=\""))) + .andExpect(content().string(containsString("\"wrappedSessionKeyLength\":3"))) + .andExpect(content().string(containsString("\"wrappedSessionKeyBase64\":\"AQID\""))) + .andExpect(content().string(containsString("\"encryptedDataLength\":4"))) + .andExpect(content().string(containsString("\"encryptedDataBase64\":\"ESIzRA==\""))) + .andExpect(content().string(containsString("\"decryptedText\":\"hello\""))) + .andExpect(content().string(containsString("\"matched\":true"))); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(EccSessionKeyCryptoSelfTestRequest.class); + Mockito.verify(pcieCryptoService).eccSessionKeyEncryptDecryptSelfTest(captor.capture()); + assertEquals(1, captor.getValue().getKeyIndex()); + assertEquals(128, captor.getValue().getKeyBits()); + assertEquals(Gm0018AlgorithmIds.SM4_CBC, captor.getValue().getAlgId()); + assertEquals(4096, captor.getValue().getEncOutBufferSize()); + assertEquals(4096, captor.getValue().getDecOutBufferSize()); + assertNull(captor.getValue().getIv()); + assertArrayEquals("hello".getBytes(java.nio.charset.StandardCharsets.UTF_8), captor.getValue().getData()); + } + + @Test + void shouldExposeOnlyPlainTextOnEccSessionKeySelfTestRequest() { + String[] fieldNames = java.util.Arrays.stream( + CryptoEccSessionKeyCryptoSelfTestInternalRequest.class.getDeclaredFields()) + .map(java.lang.reflect.Field::getName) + .toArray(String[]::new); + + assertArrayEquals(new String[] {"plainText"}, fieldNames); + } + @Test void shouldSignUserKeyViaHighLevelEndpoint() throws Exception { PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);