fix:备份恢复密钥

This commit is contained in:
waner
2026-05-09 18:09:16 +08:00
parent 9ccb5147ef
commit 79d38cc566
28 changed files with 1543 additions and 332 deletions
+6 -3
View File
@@ -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 <workDir>`,由 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/<orgCode>.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 恢复
@@ -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 执行恢复任务
@@ -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}"
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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不能为空");
@@ -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)");
@@ -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 的数字信封交换。
*/
@@ -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<ConfiguredRestoreCommand> restoreStopCommands = List.of();
private List<ConfiguredRestoreCommand> 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;
}
@@ -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<String, Object> 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;
}
}
@@ -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<String, String> entry : values.entrySet()) {
@@ -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;
}
}
@@ -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<Map<String, Object>> 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<String, Object> 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<Void> initIdentify(@Valid @RequestBody CryptoInitIdentifyInternalRequest request) {
@@ -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;
}
}
+4
View File
@@ -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 编码);主密钥初始化接口会读取该值。
@@ -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<EccSessionKeyCryptoSelfTestResult> callback =
(PcieSessionTemplate.SessionCallback<EccSessionKeyCryptoSelfTestResult>) 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<byte[]> encryptIvCaptor = ArgumentCaptor.forClass(byte[].class);
ArgumentCaptor<byte[]> encryptDataCaptor = ArgumentCaptor.forClass(byte[].class);
ArgumentCaptor<byte[]> 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<PcieNativeLibrary> 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<EccSessionKeyEncryptResult> callback =
(PcieSessionTemplate.SessionCallback<EccSessionKeyEncryptResult>) 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<Integer> 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<PcieNativeLibrary> 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<EccSessionKeyDecryptResult> callback =
(PcieSessionTemplate.SessionCallback<EccSessionKeyDecryptResult>) 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<Integer> 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);
@@ -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
@@ -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<byte[]> capturedDek = new AtomicReference<>();
AtomicReference<byte[]> decryptableDek = new AtomicReference<>();
AtomicReference<byte[]> decryptResultDek = new AtomicReference<>();
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
AtomicReference<byte[]> 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<EccInternalEncryptRequest> encryptCaptor = ArgumentCaptor.forClass(EccInternalEncryptRequest.class);
Mockito.verify(pcieCryptoService).sdfeInternalEncryptEcc(encryptCaptor.capture());
ArgumentCaptor<EccSessionKeyEncryptRequest> 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<UserKeySignRequest> 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<EccInternalDecryptRequest> decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class);
Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture());
ArgumentCaptor<EccSessionKeyDecryptRequest> 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;
}
}
@@ -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.<EccInternalEncryptRequest>getArgument(0).getData()));
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
.thenAnswer(invocation -> backup(invocation.<EccInternalDecryptRequest>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.<EccInternalEncryptRequest>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.<EccInternalDecryptRequest>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");
@@ -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");
@@ -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"));
@@ -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<DiscoveredResourceItem> 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<ResourceBackupTaskEntity> 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);
@@ -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;
// }
//}
@@ -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<EccSessionKeyCryptoSelfTestRequest> 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);