diff --git a/src/main/java/com/cisd/tms/modules/device/controller/IpWhitelistController.java b/src/main/java/com/cisd/tms/modules/device/controller/IpWhitelistController.java index 9a17c08..ce876e5 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/IpWhitelistController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/IpWhitelistController.java @@ -28,6 +28,7 @@ public class IpWhitelistController { @PostMapping("/add") @Operation(summary = "添加IP白名单", description = "新增一条IP白名单记录") @ReplayProtected + @AuditedOperation(module = ModuleCode.DEVICE, action = ActionType.CREATE, summary = "添加IP白名单") public ApiResponse addWhitelist(@RequestBody IpWhitelistRequest req) { ipWhitelistService.addWhitelist(req); return ApiResponse.success(); diff --git a/src/main/java/com/cisd/tms/modules/log/dto/AuditLogReviewRequest.java b/src/main/java/com/cisd/tms/modules/log/dto/AuditLogReviewRequest.java index 1fdc8ad..b3febe8 100644 --- a/src/main/java/com/cisd/tms/modules/log/dto/AuditLogReviewRequest.java +++ b/src/main/java/com/cisd/tms/modules/log/dto/AuditLogReviewRequest.java @@ -8,8 +8,8 @@ import lombok.Data; @Schema(description = "日志审计请求参数") public class AuditLogReviewRequest { - @Schema(description = "审计管理员签名值", example = "MEUC...") - private String auditSign; +// @Schema(description = "审计管理员签名值", example = "MEUC...") +// private String auditSign; @Schema(description = "审计结果 (PASS/ REJECT / NEED_VERIFY)", example = "PASS") private AuditResult auditResult; @Schema(description = "审计意见", example = "审核通过") diff --git a/src/main/java/com/cisd/tms/modules/log/repository/OperationAuditLogRepository.java b/src/main/java/com/cisd/tms/modules/log/repository/OperationAuditLogRepository.java index 1ccaf63..6b6565b 100644 --- a/src/main/java/com/cisd/tms/modules/log/repository/OperationAuditLogRepository.java +++ b/src/main/java/com/cisd/tms/modules/log/repository/OperationAuditLogRepository.java @@ -11,6 +11,6 @@ public interface OperationAuditLogRepository { void add(OperationAuditLogEntity auditLogEntity); IPage findPage(OperationAuditLogPageRequest req); Optional findByLogId(String logId); - void update(OperationAuditLogEntity entity); - Optional> findByAuditStatus (String auditStatus); + void updateByLogId(OperationAuditLogEntity entity); + Optional> findAll (); } diff --git a/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java index f832504..740ca63 100644 --- a/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/log/repository/impl/OperationAuditLogRepositoryImpl.java @@ -1,7 +1,6 @@ package com.cisd.tms.modules.log.repository.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -70,16 +69,17 @@ public class OperationAuditLogRepositoryImpl implements OperationAuditLogReposit } @Override - public void update(OperationAuditLogEntity entity) { - operationAuditLogMapper.updateById(entity); + public void updateByLogId(OperationAuditLogEntity entity) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationAuditLogEntity::getLogId, entity.getLogId()); + operationAuditLogMapper.update(entity, wrapper); } @Override - public Optional> findByAuditStatus(String auditStatus) { - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("audit_status", auditStatus); - List logList = operationAuditLogMapper.selectList(queryWrapper); + public Optional> findAll() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + List logList = operationAuditLogMapper.selectList(wrapper); return Optional.ofNullable(logList); } } diff --git a/src/main/java/com/cisd/tms/modules/log/runner/BackupTaskStartupRunner.java b/src/main/java/com/cisd/tms/modules/log/runner/BackupTaskStartupRunner.java new file mode 100644 index 0000000..5eae805 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/log/runner/BackupTaskStartupRunner.java @@ -0,0 +1,64 @@ +//package com.cisd.tms.modules.log.runner; +// +//import com.cisd.tms.modules.log.dto.BackupConfigUpdateRequest; +//import com.cisd.tms.modules.log.entity.BackupConfigEntity; +//import com.cisd.tms.modules.log.repository.BackupConfigRepository; +//import com.cisd.tms.modules.log.service.AuditBackupService; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.CommandLineRunner; +//import org.springframework.stereotype.Component; +// +//import java.util.ArrayList; +//import java.util.List; +// +// +//@Component +//public class BackupTaskStartupRunner implements CommandLineRunner { +// +// private static final Logger log = LoggerFactory.getLogger(BackupTaskStartupRunner.class); +// +// @Autowired +// private BackupConfigRepository backupConfigRepository; +// +// @Autowired +// private AuditBackupService auditBackupService; +// +// @Override +// public void run(String... args) throws Exception { +// log.info("============= 系统启动:开始初始化审计日志备份定时任务 ============="); +// +// try { +// List configList = backupConfigRepository.selectAll().orElseGet(ArrayList::new); +// +// if (configList.isEmpty()) { +// log.info("未在数据库中找到备份策略配置,跳过任务初始化。"); +// return; +// } +// +// for (BackupConfigEntity config : configList) { +// // 仅当状态为启用 (1) 且 cron 表达式不为空时,才去开启任务 +// if (config.getEnable() != null && config.getEnable() == 1 +// && config.getCronExp() != null && !config.getCronExp().isEmpty()) { +// +// BackupConfigUpdateRequest req = new BackupConfigUpdateRequest(); +// req.setCronExp(config.getCronExp()); +// req.setEnable(config.getEnable()); +// req.setRetentionCount(config.getRetentionCount()); +// auditBackupService.updateAndRestartBackupTask( +// req +// ); +// log.info("成功加载并开启备份任务,配置ID: {}, Cron: {}", config.getId(), config.getCronExp()); +// } else { +// log.info("配置ID: {} 未启用或 Cron 为空,跳过。", config.getId()); +// } +// } +// +// log.info("============= 审计日志备份定时任务初始化完成 ============="); +// +// } catch (Exception e) { +// log.error("初始化审计日志备份任务发生异常: {}", e.getMessage(), e); +// } +// } +//} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java b/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java new file mode 100644 index 0000000..42596ba --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/log/service/AuditBackupService.java @@ -0,0 +1,248 @@ +//package com.cisd.tms.modules.log.service; +// +//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.log.dto.BackupConfigRequest; +//import com.cisd.tms.modules.log.entity.BackupConfigEntity; +//import com.cisd.tms.modules.log.entity.BackupRecordEntity; +//import com.cisd.tms.modules.log.entity.OperationAuditLogEntity; +//import com.cisd.tms.modules.log.enums.ActionType; +//import com.cisd.tms.modules.log.enums.OperationResult; +//import com.cisd.tms.modules.log.repository.BackupConfigRepository; +//import com.cisd.tms.modules.log.repository.BackupRecordRepository; +//import com.cisd.tms.modules.log.repository.OperationAuditLogRepository; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +//import org.springframework.scheduling.support.CronExpression; +//import org.springframework.scheduling.support.CronTrigger; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//import javax.crypto.Cipher; +//import javax.crypto.spec.IvParameterSpec; +//import javax.crypto.spec.SecretKeySpec; +//import java.io.File; +//import java.io.FileOutputStream; +//import java.io.IOException; +//import java.nio.charset.StandardCharsets; +//import java.text.SimpleDateFormat; +//import java.time.LocalDateTime; +//import java.time.format.DateTimeFormatter; +//import java.util.*; +//import java.util.concurrent.ScheduledFuture; +//import java.util.zip.ZipEntry; +//import java.util.zip.ZipOutputStream; +// +//@Service +//public class AuditBackupService { +// +// private static final Logger log = LoggerFactory.getLogger(AuditBackupService.class); +// +// @Autowired +// private BackupConfigRepository backupConfigRepository; +// +// @Autowired +// private ThreadPoolTaskScheduler taskScheduler; +// +// @Autowired +// private OperationAuditLogRepository operationAuditLogRepository; +// +// @Autowired +// private BackupRecordRepository backupRecordRepository; +// +// @Autowired +// private PcieCryptoService pcieCryptoService; +// private ScheduledFuture currentTaskFuture; +// private static final String PROVIDER = "BC"; +// +// @Transactional(rollbackFor = Exception.class) +// public void updateAndRestartBackupTask(BackupConfigRequest req) { +// String cronExp = trim(req.getCronExp()); +// if (!CronExpression.isValidExpression(req.getCronExp())) { +// throw new IllegalArgumentException("Cron表达式格式不正确,请重新输入"); +// } +// +// BackupConfigEntity updateEntity = new BackupConfigEntity(); +// updateEntity.setEnable(req.getEnable()); +// updateEntity.setCronExp(req.getCronExp()); +// if (req.getRetentionCount() != null) { +// updateEntity.setRetentionCount(req.getRetentionCount()); +// } +// +// backupConfigRepository.update(updateEntity); +// +// +// if (currentTaskFuture != null && !currentTaskFuture.isCancelled()) { +// currentTaskFuture.cancel(true); +// log.info("已关闭旧的备份定时任务"); +// } +// +// +// if (req.getEnable() == 1) { +// currentTaskFuture = taskScheduler.schedule( +// () -> executeBackup(), // 指向下面的备份方法 +// new CronTrigger(cronExp) +// ); +// log.info("已开启新的备份定时任务,Cron表达式: {}", cronExp); +// } +// } +// +// private void executeBackup() { +// log.info("开始执行审计日志定期备份任务"); +// +// String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); +// // 定义压缩包的名称和路径 +// String zipFileName = "audit_backup_" + timeStr + ".zip"; +// String zipFilePath = "./data/backup/" + zipFileName; +// +// File zipFile = new File(zipFilePath); +// zipFile.getParentFile().mkdirs(); // 确保目录存在 +// +// boolean isBackupSuccess = false; +// try (FileOutputStream fos = new FileOutputStream(zipFile); +// ZipOutputStream zos = new ZipOutputStream(fos)) { +// +// StringBuilder csvContent = new StringBuilder(); +// +// csvContent.append("日志ID,发生时间,操作角色,认证等级,业务模块,操作动作,操作结果,来源IP,操作摘要,失败原因,审计状态,审计结果,审计人,审计时间,防篡改摘要(SM3),签名值(SM2)\n"); +// +// List logList = operationAuditLogRepository.findAll().orElseGet(ArrayList::new); +// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +// +// for (OperationAuditLogEntity auditLog : logList) { +// String summary = escapeCSV(auditLog.getSummary()); +// String errorMsg = escapeCSV(auditLog.getErrorMessage()); +// ActionType actionType = auditLog.getActionType(); +// OperationResult result = auditLog.getOperationResult(); +// +// String occurredTime = auditLog.getOccurredAt() != null ? sdf.format(auditLog.getOccurredAt()) : ""; +// String auditedTime = auditLog.getAuditedAt() != null ? sdf.format(auditLog.getAuditedAt()) : ""; +// +// String line = String.format("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", +// auditLog.getLogId(), +// occurredTime, +// auditLog.getOperatorRoleCode(), +// auditLog.getOperatorAuthLevel(), +// auditLog.getModuleCode(), +// actionType, +// result, +// auditLog.getRemoteIp(), +// summary, +// errorMsg, +// auditLog.getAuditStatus(), +// auditLog.getAuditResult(), +// auditLog.getAuditedBy(), +// auditedTime, +// auditLog.getPayloadHash(), +// auditLog.getSignValue() +// ); +// csvContent.append(line); +// } +// +// byte[] orgDataBytes = csvContent.toString().getBytes(StandardCharsets.UTF_8); +// +// byte[] iv = randomBytes(16); +// byte[] sm4Key = randomBytes(16); +// +// Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER); +// cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv)); +// +// EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest(); +// encryptRequest.setKeyIndex(1); +// encryptRequest.setData(sm4Key); +// BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest); +// +// Map envelope = new LinkedHashMap<>(); +// envelope.put("version", 1); +// envelope.put("payloadAlg", "SM4-CBC"); +// envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC"); +// envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv)); +// envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek))); +// envelope.put("dekLength", sm4Key.length); +// +// +// byte[] encryptedData = cipher.doFinal(orgDataBytes); +// +// ZipEntry csvEntry = new ZipEntry("data.csv.enc"); +// zos.putNextEntry(csvEntry); +// +// zos.write(encryptedData); +// zos.closeEntry(); +// +// log.info("审计日志备份完成,共备份 {} 条", logList.size()); +// +// // 更新备份更新时间 +// BackupConfigEntity config = new BackupConfigEntity(); +// config.setLastBackupTime(LocalDateTime.now()); +// backupConfigRepository.saveOrUpdate(config); +// +// isBackupSuccess = true; +// +// } catch (IOException e) { +// log.error("写入 ZIP 压缩包发生异常: {}", e.getMessage(), e); +// } catch (Exception e) { +// log.error("执行备份任务发生异常: {}", e.getMessage(), e); +// } +// +// +// if (isBackupSuccess) { +// try { +// BackupRecordEntity record = new BackupRecordEntity() +// .setFileName(zipFileName) +// .setFilePath(zipFilePath) +// .setStatus("SUCCESS"); +// +// backupRecordRepository.add(record); +// +// log.info("备份记录已成功存入 tms_log_backup 表。"); +// } catch (Exception e) { +// log.error("保存备份记录到数据库失败: {}", e.getMessage(), e); +// } +// } +// } +// +// private String escapeCSV(String str) { +// if (str == null) { +// return ""; +// } +// +// boolean containSpecialChar = str.contains(",") +// || str.contains("\"") +// || str.contains("\n") +// || str.contains("\r"); +// +// if (containSpecialChar) { +// str = str.replace("\"", "\"\""); +// str = "\"" + str + "\""; +// } +// +// return str; +// } +// +// +// private static byte[] normalize(BackupDataResult result) { +// if (result == null || result.getData() == null) { +// return new byte[0]; +// } +// int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length +// ? result.getLength() +// : result.getData().length; +// byte[] normalized = new byte[safeLength]; +// System.arraycopy(result.getData(), 0, normalized, 0, safeLength); +// return normalized; +// } +// +// +// private static byte[] randomBytes(int size) { +// byte[] bytes = new byte[size]; +// new java.security.SecureRandom().nextBytes(bytes); +// return bytes; +// } +// +// private String trim(String str) { +// return str == null ? "" : str.trim(); +// } +//} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/log/service/OperationAuditService.java b/src/main/java/com/cisd/tms/modules/log/service/OperationAuditService.java index b4c9cf4..967b52b 100644 --- a/src/main/java/com/cisd/tms/modules/log/service/OperationAuditService.java +++ b/src/main/java/com/cisd/tms/modules/log/service/OperationAuditService.java @@ -3,7 +3,6 @@ package com.cisd.tms.modules.log.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.exception.BizException; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.log.annotation.AuditedOperation; import com.cisd.tms.modules.log.dto.*; @@ -21,7 +20,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Service; import org.springframework.web.method.HandlerMethod; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.Base64; import java.util.UUID; @@ -137,6 +135,7 @@ public class OperationAuditService { resp.setAuditedBy(entity.getAuditedBy()); resp.setRemoteIp(entity.getRemoteIp()); resp.setErrorMessage(entity.getErrorMessage()); + resp.setSignValue(entity.getSignValue()); resp.setOccurredAt(entity.getOccurredAt()); return resp; }); @@ -169,24 +168,32 @@ public class OperationAuditService { OperationAuditLogEntity existLog = operationAuditLogRepository.findByLogId(logId) .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "未找到该logId")); - String signValue = trim(req.getAuditSign()); - if (signValue.isEmpty()){ - throw new IllegalArgumentException("签名值不能为空"); - } - - //todo 验签 - EccInternalVerifyRequest VerifyRequest = new EccInternalVerifyRequest(); - // todo后续更改 - VerifyRequest.setKeyIndex(1); - VerifyRequest.setData(req.getAuditSign().getBytes(StandardCharsets.UTF_8)); - VerifyRequest.setSignature(decodeBlob(signValue, "signature is invalid")); - try { - pcieCryptoService.eccInternalVerify(VerifyRequest); - } catch (BizException ex) { - throw ex; - } catch (RuntimeException ex) { - throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed"); - } +// String signValue = trim(req.getAuditSign()); +// if (signValue.isEmpty()){ +// throw new IllegalArgumentException("签名值不能为空"); +// } +// +// String payload = auditSigner.buildAuditPayload(existLog); +// +// //计算SM3哈希 +// DigestRequest request = new DigestRequest(); +// request.setAlgId(Gm0018AlgorithmIds.SM3); +// request.setData(payload.getBytes(StandardCharsets.UTF_8)); +// BackupDataResult payloadHash = pcieCryptoService.digest(request); +// +// +// //todo 验签 +// UserKeyVerifyRequest userKeyVerifyRequest = new UserKeyVerifyRequest(); +// userKeyVerifyRequest.setKeyIndex(1); +// userKeyVerifyRequest.setData(payloadHash.getData()); +// userKeyVerifyRequest.setSignature(decodeBlob(signValue, "signature is invalid")); +// try { +// pcieCryptoService.userKeyVerifyWithSm3(userKeyVerifyRequest); +// } catch (BizException ex) { +// throw ex; +// } catch (RuntimeException ex) { +// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed"); +// } @@ -202,8 +209,7 @@ public class OperationAuditService { updateEntity.setAuditedBy(auditorUser); updateEntity.setAuditedAt(LocalDateTime.now()); - // 4. 执行更新 - operationAuditLogRepository.update(updateEntity); + operationAuditLogRepository.updateByLogId(updateEntity); log.info("日志复核完成, logId: {}, 审计人: {}, 结果: {}, {}", logId, auditorUser, req.getAuditResult(), req.getAuditComment()); } diff --git a/src/main/java/com/cisd/tms/modules/log/service/OperationAuditSigner.java b/src/main/java/com/cisd/tms/modules/log/service/OperationAuditSigner.java index 230b6c9..31ede9f 100644 --- a/src/main/java/com/cisd/tms/modules/log/service/OperationAuditSigner.java +++ b/src/main/java/com/cisd/tms/modules/log/service/OperationAuditSigner.java @@ -3,7 +3,7 @@ package com.cisd.tms.modules.log.service; import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; +import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.log.entity.OperationAuditLogEntity; import org.springframework.stereotype.Component; @@ -49,11 +49,11 @@ public class OperationAuditSigner { entity.setPayloadHash(Base64.getEncoder().encodeToString(payloadHash.getData())); - // 3. 计算 SM2 签名值 - EccInternalSignRequest signRequest = new EccInternalSignRequest(); - signRequest.setData(payloadHash.getData()); - signRequest.setKeyIndex(1); - BackupDataResult sign = pcieCryptoService.eccInternalSign(signRequest); + //计算 SM2 签名值 + UserKeySignRequest userKeySignRequest = new UserKeySignRequest(); + userKeySignRequest.setData(payloadHash.getData()); + userKeySignRequest.setKeyIndex(1); + BackupDataResult sign = pcieCryptoService.userKeySignWithSm3(userKeySignRequest ); entity.setSignValue(Base64.getEncoder().encodeToString(sign.getData())); } diff --git a/src/test/java/com/cisd/tms/modules/log/controller/OperationAuditControllerTest.java b/src/test/java/com/cisd/tms/modules/log/controller/OperationAuditControllerTest.java index a261e37..6a2d368 100644 --- a/src/test/java/com/cisd/tms/modules/log/controller/OperationAuditControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/log/controller/OperationAuditControllerTest.java @@ -46,7 +46,7 @@ class OperationAuditControllerTest { void testReviewLog_Success_WithAuditAdminRole() throws Exception { AuditLogReviewRequest req = new AuditLogReviewRequest(); req.setAuditResult(AuditResult.PASS); - req.setAuditSign("E1A2B3C4..."); +// req.setAuditSign("E1A2B3C4..."); Mockito.doNothing().when(auditService).reviewLog(eq("log-789"), any(), eq("AUDIT_ADMIN")); diff --git a/ukey-tool/pom.xml b/ukey-tool/pom.xml new file mode 100644 index 0000000..a1c296b --- /dev/null +++ b/ukey-tool/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.5.11 + + + com.cisd + ukey-tool + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + net.java.dev.jna + jna + 5.14.0 + + + + org.bouncycastle + bcprov-jdk18on + 1.81 + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + cisd-ukey-tool + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/ukey-tool/skflib/HidAPI.dll b/ukey-tool/skflib/HidAPI.dll new file mode 100644 index 0000000..d036094 Binary files /dev/null and b/ukey-tool/skflib/HidAPI.dll differ diff --git a/ukey-tool/skflib/MscAPI.dll b/ukey-tool/skflib/MscAPI.dll new file mode 100644 index 0000000..8a1cb93 Binary files /dev/null and b/ukey-tool/skflib/MscAPI.dll differ diff --git a/ukey-tool/skflib/SKF.dll b/ukey-tool/skflib/SKF.dll new file mode 100644 index 0000000..b43154e Binary files /dev/null and b/ukey-tool/skflib/SKF.dll differ diff --git a/ukey-tool/src/main/java/com/cisd/ukey/UkeyToolApplication.java b/ukey-tool/src/main/java/com/cisd/ukey/UkeyToolApplication.java new file mode 100644 index 0000000..51289b1 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/UkeyToolApplication.java @@ -0,0 +1,21 @@ +package com.cisd.ukey; + +import com.cisd.ukey.config.SkfProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(SkfProperties.class) +public class UkeyToolApplication { + + public static void main(String[] args) { + SpringApplication.run(UkeyToolApplication.class, args); + System.out.println(); + System.out.println("==========================================================="); + System.out.println("= UKEY 工具已启动成功 ="); + System.out.println("= 请保持此窗口打开,不要关闭,否则服务将停止。 ="); + System.out.println("==========================================================="); + System.out.println(); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/config/SkfProperties.java b/ukey-tool/src/main/java/com/cisd/ukey/config/SkfProperties.java new file mode 100644 index 0000000..ff6983b --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/config/SkfProperties.java @@ -0,0 +1,22 @@ +package com.cisd.ukey.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(prefix = "ukey.skf") +public class SkfProperties { + + private String vendorLibName = "SKF.dll"; + private String libDir = "skflib"; + private String applicationName = "cisd_app"; + private String containerName = "cisd_container"; + private String initAdminPin = "12345678"; + private String initUserPin = "12345678"; + private int adminPinRetryCount = 10; + private int userPinRetryCount = 10; + private int createFileRights = 0x000000FF; + private int signKeyAlgId = 0x00020100; + private String deviceAuthKey = "1234567812345678"; + +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponse.java b/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponse.java new file mode 100644 index 0000000..5b4210f --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponse.java @@ -0,0 +1,50 @@ +package com.cisd.ukey.controller; + +public class ApiResponse { + + private int code; + private T data; + private String msg; + + public ApiResponse() { + } + + public ApiResponse(int code, T data, String msg) { + this.code = code; + this.data = data; + this.msg = msg; + } + + public static ApiResponse success(T data) { + return new ApiResponse<>(0, data, "success"); + } + + public static ApiResponse fail(int code, String msg) { + int actualCode = code == 0 ? -1 : code; + return new ApiResponse<>(actualCode, null, msg); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponseAdvice.java b/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponseAdvice.java new file mode 100644 index 0000000..734287b --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/controller/ApiResponseAdvice.java @@ -0,0 +1,28 @@ +package com.cisd.ukey.controller; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +@RestControllerAdvice +public class ApiResponseAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (body instanceof ApiResponse) { + return body; + } + return ApiResponse.success(body); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfController.java b/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfController.java new file mode 100644 index 0000000..327056b --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfController.java @@ -0,0 +1,110 @@ +package com.cisd.ukey.controller; + +import com.cisd.ukey.service.SkfService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/skf") +public class SkfController { + + private final SkfService skfService; + + public SkfController(SkfService skfService) { + this.skfService = skfService; + } + + @GetMapping("/devices") + public List listDevices(@RequestParam(defaultValue = "true") boolean presentOnly) { + return skfService.listDevices(presentOnly); + } + + @PostMapping("/pin/verify") + public SkfService.PinVerifyResult verifyPin(@RequestBody PinVerifyRequest request) { + return skfService.verifyPin(request.deviceName(), request.pinType(), request.pin()); + } + + @PostMapping("/pin/change") + public SkfService.PinVerifyResult changePin(@RequestBody ChangePinRequest request) { + return skfService.changePin(request.deviceName(), request.pinType(), request.oldPin(), request.newPin()); + } + + @PostMapping("/pin/reset") + public SkfService.PinVerifyResult resetPin(@RequestBody ResetPinRequest request) { + return skfService.unblockPin(request.deviceName(), request.adminPin(), request.newUserPin()); + } + + @PostMapping("/file/list") + public List listFiles(@RequestBody DeviceRequest request) { + return skfService.listFiles(request.deviceName()); + } + + @PostMapping("/file/read") + public SkfService.FileContent readFile(@RequestBody ReadFileRequest request) { + return skfService.readFile(request.deviceName(), request.fileName()); + } + + @PostMapping("/file/write") + public SkfService.FileContent writeFile(@RequestBody WriteFileRequest request) { + return skfService.writeFile(request.deviceName(), request.fileName(), request.content()); + } + + @PostMapping("/file/delete") + public void deleteFile(@RequestBody DeleteFileRequest request) { + skfService.deleteFile(request.deviceName(), request.fileName()); + } + + @PostMapping("/app/delete") + public void deleteApplication(@RequestBody DeviceRequest request) { + skfService.deleteApplication(request.deviceName()); + } + + @PostMapping("/ecc/public-key/export") + public SkfService.EccPublicKeyResult exportPublicKey(@RequestBody DeviceRequest request, + @RequestParam(defaultValue = "true") boolean signFlag) { + return skfService.exportEccPublicKey(request.deviceName(), signFlag); + } + + @PostMapping("/ecc/sign") + public SkfService.EccSignatureResult sign(@RequestBody SignRequest request) { + return skfService.eccSign(request.deviceName(), request.plainText()); + } + + @PostMapping("/ecc/verify") + public SkfService.VerifyResult verify(@RequestBody VerifyRequest request) { + return skfService.eccVerify(request.deviceName(), request.publicKey(), request.plainText(), request.signatureBase64()); + } + + public record DeviceRequest(String deviceName) { + } + + public record PinVerifyRequest(String deviceName, int pinType, String pin) { + } + + public record ChangePinRequest(String deviceName, int pinType, String oldPin, String newPin) { + } + + public record ResetPinRequest(String deviceName, String adminPin, String newUserPin) { + } + + public record ReadFileRequest(String deviceName, String fileName) { + } + + public record WriteFileRequest(String deviceName, String fileName, String content) { + } + + public record DeleteFileRequest(String deviceName, String fileName) { + } + + public record SignRequest(String deviceName, String plainText) { + } + + public record VerifyRequest(String deviceName, String publicKey, String plainText, String signatureBase64) { + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfExceptionHandler.java b/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfExceptionHandler.java new file mode 100644 index 0000000..f2b0533 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/controller/SkfExceptionHandler.java @@ -0,0 +1,26 @@ +package com.cisd.ukey.controller; + +import com.cisd.ukey.exception.SkfErrorCodeMessageResolver; +import com.cisd.ukey.exception.SkfException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class SkfExceptionHandler { + + @ExceptionHandler(SkfException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleSkfException(SkfException ex) { + String zhMessage = SkfErrorCodeMessageResolver.resolve(ex.getCode()); + String message = zhMessage == null ? ex.getMessage() : ex.getMessage() + "(" + zhMessage + ")"; + return ApiResponse.fail(ex.getCode(), message); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception ex) { + return ApiResponse.fail(-1, ex.getMessage()); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfErrorCodeMessageResolver.java b/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfErrorCodeMessageResolver.java new file mode 100644 index 0000000..aee8884 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfErrorCodeMessageResolver.java @@ -0,0 +1,82 @@ +package com.cisd.ukey.exception; + +import java.util.Map; + +public final class SkfErrorCodeMessageResolver { + + private static final Map CODE_MESSAGE_MAP = Map.ofEntries( + Map.entry(0x00000000, "成功"), + Map.entry(0x0A000001, "失败"), + Map.entry(0x0A000002, "异常错误"), + Map.entry(0x0A000003, "不支持的服务"), + Map.entry(0x0A000004, "文件操作错误"), + Map.entry(0x0A000005, "无效的句柄"), + Map.entry(0x0A000006, "无效的参数"), + Map.entry(0x0A000007, "读文件错误"), + Map.entry(0x0A000008, "写文件错误"), + Map.entry(0x0A000009, "名称长度错误"), + Map.entry(0x0A00000A, "密钥用途错误"), + Map.entry(0x0A00000B, "模的长度错误"), + Map.entry(0x0A00000C, "未初始化"), + Map.entry(0x0A00000D, "对象错误"), + Map.entry(0x0A00000E, "内存错误"), + Map.entry(0x0A00000F, "超时"), + Map.entry(0x0A000010, "输入数据长度错误"), + Map.entry(0x0A000011, "输入数据错误"), + Map.entry(0x0A000012, "生成随机数错误"), + Map.entry(0x0A000013, "HASH对象错误"), + Map.entry(0x0A000014, "HASH运算错误"), + Map.entry(0x0A000015, "产生RSA密钥错误"), + Map.entry(0x0A000016, "RSA密钥模长错误"), + Map.entry(0x0A000017, "CSP服务导入公钥错误"), + Map.entry(0x0A000018, "RSA加密错误"), + Map.entry(0x0A000019, "RSA解密错误"), + Map.entry(0x0A00001A, "HASH值不相等"), + Map.entry(0x0A00001B, "密钥未发现"), + Map.entry(0x0A00001C, "证书未发现"), + Map.entry(0x0A00001D, "对象未导出"), + Map.entry(0x0A00001E, "解密填充错误"), + Map.entry(0x0A00001F, "MAC长度错误"), + Map.entry(0x0A000020, "缓冲区不足"), + Map.entry(0x0A000021, "密钥类型错误"), + Map.entry(0x0A000022, "无事件错误"), + Map.entry(0x0A000023, "设备已移除"), + Map.entry(0x0A000024, "PIN不正确"), + Map.entry(0x0A000025, "PIN被锁死"), + Map.entry(0x0A000026, "PIN无效"), + Map.entry(0x0A000027, "PIN长度错误"), + Map.entry(0x0A000028, "用户已经登录"), + Map.entry(0x0A000029, "没有初始化用户口令"), + Map.entry(0x0A00002A, "PIN类型错误"), + Map.entry(0x0A00002B, "应用名称无效"), + Map.entry(0x0A00002C, "应用已经存在"), + Map.entry(0x0A00002D, "用户没有登录"), + Map.entry(0x0A00002E, "应用不存在"), + Map.entry(0x0A00002F, "文件已经存在"), + Map.entry(0x0A000030, "空间不足"), + Map.entry(0x0A000031, "文件不存在"), + Map.entry(0x0A000032, "已达到最大可管理容器数"), + Map.entry(0x0A000033, "密钥已被锁住"), + Map.entry(0x0A000034, "证书未被使用"), + Map.entry(0x0A000035, "无效容器"), + Map.entry(0x0A000036, "容器不存在"), + Map.entry(0x0A000037, "容器已存在"), + Map.entry(0x0A000038, "证书已经被使用"), + Map.entry(0x0A000039, "密钥未被使用"), + Map.entry(0x0A00003A, "文件操作权限错误"), + Map.entry(0x0A00003B, "设备未认证"), + Map.entry(0x0A00003C, "请重新插拔卡"), + Map.entry(0x0A00003D, "应用处于非活跃状态"), + Map.entry(0x0A00003E, "应用处于打开状态"), + Map.entry(0x0A000040, "传入的固件数据无效(格式错误)"), + Map.entry(0x0A000041, "传入的固件数据的签名值无效(验签失败)"), + Map.entry(0x0A0000A4, "MF不存在") + ); + + private SkfErrorCodeMessageResolver() { + } + + public static String resolve(int code) { + return CODE_MESSAGE_MAP.get(code); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfException.java b/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfException.java new file mode 100644 index 0000000..fff3ec0 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/exception/SkfException.java @@ -0,0 +1,27 @@ +package com.cisd.ukey.exception; + +public class SkfException extends RuntimeException { + + private final String apiName; + private final int code; + + public SkfException(String apiName, int code, String message) { + super(message); + this.apiName = apiName; + this.code = code; + } + + public SkfException(String apiName, String message, Throwable cause) { + super(message, cause); + this.apiName = apiName; + this.code = -1; + } + + public String getApiName() { + return apiName; + } + + public int getCode() { + return code; + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/BlockCipherParam.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/BlockCipherParam.java new file mode 100644 index 0000000..f5c132a --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/BlockCipherParam.java @@ -0,0 +1,12 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({"iv", "ivLen", "paddingType", "feedBitLen"}) +public class BlockCipherParam extends Structure { + + public byte[] iv = new byte[32]; + public int ivLen; + public int paddingType; + public int feedBitLen; +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/DevInfo.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/DevInfo.java new file mode 100644 index 0000000..97e14a0 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/DevInfo.java @@ -0,0 +1,37 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({ + "version", "manufacturer", "issuer", "label", "serialNumber", + "hwVersion", "firmwareVersion", "algSymCap", "algAsymCap", + "algHashCap", "devAuthAlgId", "totalSpace", "freeSpace", + "maxEccBufferSize", "maxBufferSize", "reserved" +}) +public class DevInfo extends Structure { + + public DevInfo() { + setAlignType(ALIGN_NONE); + } + + public Version version; + public byte[] manufacturer = new byte[64]; + public byte[] issuer = new byte[64]; + public byte[] label = new byte[32]; + public byte[] serialNumber = new byte[32]; + public Version hwVersion; + public Version firmwareVersion; + public int algSymCap; + public int algAsymCap; + public int algHashCap; + public int devAuthAlgId; + public int totalSpace; + public int freeSpace; + public int maxEccBufferSize; + public int maxBufferSize; + public byte[] reserved = new byte[64]; + + public long devAuthAlgIdUnsigned() { + return Integer.toUnsignedLong(devAuthAlgId); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/EccPublicKeyBlob.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/EccPublicKeyBlob.java new file mode 100644 index 0000000..db12641 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/EccPublicKeyBlob.java @@ -0,0 +1,19 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Pointer; +import com.sun.jna.Structure; + +@Structure.FieldOrder({"bitLen", "xCoordinate", "yCoordinate"}) +public class EccPublicKeyBlob extends Structure { + + public int bitLen; + public byte[] xCoordinate = new byte[64]; + public byte[] yCoordinate = new byte[64]; + + public EccPublicKeyBlob() { + } + + public EccPublicKeyBlob(Pointer pointer) { + super(pointer); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/EccSignatureBlob.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/EccSignatureBlob.java new file mode 100644 index 0000000..63d990e --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/EccSignatureBlob.java @@ -0,0 +1,10 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({"r", "s"}) +public class EccSignatureBlob extends Structure { + + public byte[] r = new byte[64]; + public byte[] s = new byte[64]; +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/FileAttribute.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/FileAttribute.java new file mode 100644 index 0000000..d14e3fb --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/FileAttribute.java @@ -0,0 +1,12 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({"fileName", "fileSize", "readRights", "writeRights"}) +public class FileAttribute extends Structure { + + public byte[] fileName = new byte[32]; + public int fileSize; + public int readRights; + public int writeRights; +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/SkfLibrary.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/SkfLibrary.java new file mode 100644 index 0000000..94f0988 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/SkfLibrary.java @@ -0,0 +1,74 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import com.sun.jna.win32.StdCallLibrary; + +public interface SkfLibrary extends StdCallLibrary { + + int SKF_EnumDev(int present, Pointer nameList, IntByReference size); + + int SKF_ConnectDev(byte[] name, PointerByReference devHandle); + + int SKF_DisConnectDev(Pointer devHandle); + + int SKF_GetDevInfo(Pointer devHandle, DevInfo devInfo); + + int SKF_GenRandom(Pointer devHandle, byte[] random, int randomLen); + + int SKF_DevAuth(Pointer devHandle, byte[] authData, int len); + + int SKF_OpenApplication(Pointer devHandle, byte[] appName, PointerByReference applicationHandle); + + int SKF_CreateApplication(Pointer devHandle, byte[] appName, byte[] adminPin, int adminPinRetryCount, + byte[] userPin, int userPinRetryCount, int createFileRights, + PointerByReference applicationHandle); + + int SKF_DeleteApplication(Pointer devHandle, byte[] appName); + + int SKF_CloseApplication(Pointer applicationHandle); + + int SKF_VerifyPIN(Pointer applicationHandle, int pinType, byte[] pin, IntByReference retryCount); + + int SKF_ChangePIN(Pointer applicationHandle, int pinType, byte[] oldPin, byte[] newPin, IntByReference retryCount); + + int SKF_UnblockPIN(Pointer applicationHandle, byte[] adminPin, byte[] newUserPin, IntByReference retryCount); + + int SKF_GetPINInfo(Pointer applicationHandle, int pinType, IntByReference maxRetryCount, + IntByReference remainRetryCount, IntByReference defaultPin); + + int SKF_CreateFile(Pointer applicationHandle, byte[] fileName, int fileSize, int readRights, int writeRights); + + int SKF_DeleteFile(Pointer applicationHandle, byte[] fileName); + + int SKF_EnumFiles(Pointer applicationHandle, Pointer fileList, IntByReference size); + + int SKF_GetFileInfo(Pointer applicationHandle, byte[] fileName, FileAttribute fileAttribute); + + int SKF_ReadFile(Pointer applicationHandle, byte[] fileName, int offset, int size, Pointer outData, IntByReference outLen); + + int SKF_WriteFile(Pointer applicationHandle, byte[] fileName, int offset, byte[] data, int size); + + int SKF_OpenContainer(Pointer applicationHandle, byte[] containerName, PointerByReference containerHandle); + + int SKF_CreateContainer(Pointer applicationHandle, byte[] containerName, PointerByReference containerHandle); + + int SKF_GenECCKeyPair(Pointer containerHandle, int algId, EccPublicKeyBlob publicKeyBlob); + + int SKF_CloseContainer(Pointer containerHandle); + + int SKF_ECCSignData(Pointer containerHandle, byte[] data, int dataLen, EccSignatureBlob signature); + + int SKF_ECCVerify(Pointer devHandle, EccPublicKeyBlob publicKey, byte[] data, int dataLen, EccSignatureBlob signature); + + int SKF_ExportPublicKey(Pointer containerHandle, int signFlag, Pointer blob, IntByReference blobLen); + + int SKF_SetSymmKey(Pointer devHandle, byte[] key, int algId, PointerByReference keyHandle); + + int SKF_EncryptInit(Pointer keyHandle, BlockCipherParam encryptParam); + + int SKF_Encrypt(Pointer keyHandle, byte[] data, int dataLen, byte[] encryptedData, IntByReference encryptedLen); + + int SKF_CloseHandle(Pointer handle); +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/jna/Version.java b/ukey-tool/src/main/java/com/cisd/ukey/jna/Version.java new file mode 100644 index 0000000..50bef94 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/jna/Version.java @@ -0,0 +1,14 @@ +package com.cisd.ukey.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({"major", "minor"}) +public class Version extends Structure { + + public Version() { + setAlignType(ALIGN_NONE); + } + + public byte major; + public byte minor; +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/service/BCECUtils.java b/ukey-tool/src/main/java/com/cisd/ukey/service/BCECUtils.java new file mode 100644 index 0000000..8266f75 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/service/BCECUtils.java @@ -0,0 +1,428 @@ +package com.cisd.ukey.service; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X962Parameters; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9ECPoint; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.generators.ECKeyPairGenerator; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyGenerationParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * 这个工具类的方法,也适用于其他基于BC库的ECC算法 + */ +public class BCECUtils { + private static final String ALGO_NAME_EC = "EC"; + private static final String PEM_STRING_PUBLIC = "PUBLIC KEY"; + private static final String PEM_STRING_ECPRIVATEKEY = "EC PRIVATE KEY"; + + + public static final SM2P256V1Curve CURVE = new SM2P256V1Curve(); + public final static BigInteger SM2_ECC_P = CURVE.getQ(); + public final static BigInteger SM2_ECC_A = CURVE.getA().toBigInteger(); + public final static BigInteger SM2_ECC_B = CURVE.getB().toBigInteger(); + public final static BigInteger SM2_ECC_N = CURVE.getOrder(); + public final static BigInteger SM2_ECC_H = CURVE.getCofactor(); + public final static BigInteger SM2_ECC_GX = new BigInteger( + "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16); + public final static BigInteger SM2_ECC_GY = new BigInteger( + "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16); + public static final ECPoint G_POINT = CURVE.createPoint(SM2_ECC_GX, SM2_ECC_GY); + public static final ECDomainParameters DOMAIN_PARAMS = new ECDomainParameters(CURVE, G_POINT, + SM2_ECC_N, SM2_ECC_H); + + /** + * 生成ECC密钥对 + * + * @return ECC密钥对 + */ + public static AsymmetricCipherKeyPair generateKeyPairParameter( + ECDomainParameters domainParameters, SecureRandom random) { + ECKeyGenerationParameters keyGenerationParams = new ECKeyGenerationParameters(domainParameters, + random); + ECKeyPairGenerator keyGen = new ECKeyPairGenerator(); + keyGen.init(keyGenerationParams); + return keyGen.generateKeyPair(); + } + + public static KeyPair generateKeyPair(ECDomainParameters domainParameters, SecureRandom random) + throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + ECParameterSpec parameterSpec = new ECParameterSpec(domainParameters.getCurve(), domainParameters.getG(), + domainParameters.getN(), domainParameters.getH()); + kpg.initialize(parameterSpec, random); + return kpg.generateKeyPair(); + } + + public static int getCurveLength(ECKeyParameters ecKey) { + return getCurveLength(ecKey.getParameters()); + } + + public static int getCurveLength(ECDomainParameters domainParams) { + return (domainParams.getCurve().getFieldSize() + 7) / 8; + } + + public static byte[] fixToCurveLengthBytes(int curveLength, byte[] src) { + if (src.length == curveLength) { + return src; + } + + byte[] result = new byte[curveLength]; + if (src.length > curveLength) { + System.arraycopy(src, src.length - result.length, result, 0, result.length); + } else { + System.arraycopy(src, 0, result, result.length - src.length, src.length); + } + return result; + } + + + + public static String getHexPubKey(BCECPublicKey publicKey) { + byte[] x = publicKey.getQ().getXCoord().getEncoded(); + byte[] y = publicKey.getQ().getYCoord().getEncoded(); + return Hex.toHexString(x) + Hex.toHexString(y); + } + + + + /** + * 根据EC私钥构造EC公钥 + * + * @param priKey ECC私钥参数对象 + * @return + */ + public static ECPublicKeyParameters buildECPublicKeyByPrivateKey(ECPrivateKeyParameters priKey) { + ECDomainParameters domainParameters = priKey.getParameters(); + ECPoint q = new FixedPointCombMultiplier().multiply(domainParameters.getG(), priKey.getD()); + return new ECPublicKeyParameters(q, domainParameters); + } + + + public static ECPublicKeyParameters createECPublicKeyParameters(byte[] xBytes, byte[] yBytes) { + return createECPublicKeyParameters(xBytes, yBytes, CURVE, DOMAIN_PARAMS); + } + + /** + * @param xBytes 十六进制形式的公钥x分量,如果是SM2算法,应该是32字节 + * @param yBytes 十六进制形式的公钥y分量,如果是SM2算法,应该是32字节 + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + byte[] xBytes, byte[] yBytes, ECCurve curve, ECDomainParameters domainParameters) { + final byte uncompressedFlag = 0x04; + int curveLength = getCurveLength(domainParameters); + xBytes = fixToCurveLengthBytes(curveLength, xBytes); + yBytes = fixToCurveLengthBytes(curveLength, yBytes); + byte[] encodedPubKey = new byte[1 + xBytes.length + yBytes.length]; + encodedPubKey[0] = uncompressedFlag; + System.arraycopy(xBytes, 0, encodedPubKey, 1, xBytes.length); + System.arraycopy(yBytes, 0, encodedPubKey, 1 + xBytes.length, yBytes.length); + return new ECPublicKeyParameters(curve.decodePoint(encodedPubKey), domainParameters); + } + + public static ECPrivateKeyParameters convertPrivateKeyToParameters(BCECPrivateKey ecPriKey) { + ECParameterSpec parameterSpec = ecPriKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + return new ECPrivateKeyParameters(ecPriKey.getD(), domainParameters); + } + + public static ECPublicKeyParameters convertPublicKeyToParameters(BCECPublicKey ecPubKey) { + ECParameterSpec parameterSpec = ecPubKey.getParameters(); + ECDomainParameters domainParameters = new ECDomainParameters(parameterSpec.getCurve(), parameterSpec.getG(), + parameterSpec.getN(), parameterSpec.getH()); + return new ECPublicKeyParameters(ecPubKey.getQ(), domainParameters); + } + + public static BCECPublicKey createPublicKeyFromSubjectPublicKeyInfo(SubjectPublicKeyInfo subPubInfo) + throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeySpecException, IOException { + return BCECUtils.convertX509ToECPublicKey(subPubInfo.toASN1Primitive().getEncoded(ASN1Encoding.DER)); + } + + /** + * 将ECC私钥转换为PKCS8标准的字节流 + * + * @param priKey + * @param pubKey 可以为空,但是如果为空的话得到的结果OpenSSL可能解析不了 + * @return + */ + public static byte[] convertECPrivateKeyToPKCS8( + ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) { + ECDomainParameters domainParams = priKey.getParameters(); + ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(), + domainParams.getN(), domainParams.getH()); + BCECPublicKey publicKey = null; + if (pubKey != null) { + publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec, + BouncyCastleProvider.CONFIGURATION); + } + BCECPrivateKey privateKey = new BCECPrivateKey(ALGO_NAME_EC, priKey, publicKey, + spec, BouncyCastleProvider.CONFIGURATION); + return privateKey.getEncoded(); + } + + /** + * 将PKCS8标准的私钥字节流转换为私钥对象 + * + * @param pkcs8Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + */ + public static BCECPrivateKey convertPKCS8ToECPrivateKey(byte[] pkcs8Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException { + PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(pkcs8Key); + KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + return (BCECPrivateKey) kf.generatePrivate(peks); + } + + /** + * 将PKCS8标准的私钥字节流转换为PEM + * + * @param encodedKey + * @return + * @throws IOException + */ + public static String convertECPrivateKeyPKCS8ToPEM(byte[] encodedKey) throws IOException { + return convertEncodedDataToPEM(PEM_STRING_ECPRIVATEKEY, encodedKey); + } + + /** + * 将PEM格式的私钥转换为PKCS8标准字节流 + * + * @param pemString + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeyPEMToPKCS8(String pemString) throws IOException { + return convertPEMToEncodedData(pemString); + } + + /** + * 将ECC私钥转换为SEC1标准的字节流 + * openssl d2i_ECPrivateKey函数要求的DER编码的私钥也是SEC1标准的, + * 这个工具函数的主要目的就是为了能生成一个openssl可以直接“识别”的ECC私钥. + * 相对RSA私钥的PKCS1标准,ECC私钥的标准为SEC1 + * + * @param priKey + * @param pubKey + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeyToSEC1( + ECPrivateKeyParameters priKey, ECPublicKeyParameters pubKey) throws IOException { + byte[] pkcs8Bytes = convertECPrivateKeyToPKCS8(priKey, pubKey); + PrivateKeyInfo pki = PrivateKeyInfo.getInstance(pkcs8Bytes); + ASN1Encodable encodable = pki.parsePrivateKey(); + ASN1Primitive primitive = encodable.toASN1Primitive(); + byte[] sec1Bytes = primitive.getEncoded(); + return sec1Bytes; + } + + + /** + * 将ECC公钥对象转换为X509标准的字节流 + * + * @param pubKey + * @return + */ + public static byte[] convertECPublicKeyToX509(ECPublicKeyParameters pubKey) { + ECDomainParameters domainParams = pubKey.getParameters(); + ECParameterSpec spec = new ECParameterSpec(domainParams.getCurve(), domainParams.getG(), + domainParams.getN(), domainParams.getH()); + BCECPublicKey publicKey = new BCECPublicKey(ALGO_NAME_EC, pubKey, spec, + BouncyCastleProvider.CONFIGURATION); + return publicKey.getEncoded(); + } + + /** + * 将X509标准的公钥字节流转为公钥对象 + * + * @param x509Bytes + * @return + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws InvalidKeySpecException + */ + public static BCECPublicKey convertX509ToECPublicKey(byte[] x509Bytes) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec eks = new X509EncodedKeySpec(x509Bytes); + KeyFactory kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME); + return (BCECPublicKey) kf.generatePublic(eks); + } + + /** + * 将X509标准的公钥字节流转为PEM + * + * @param encodedKey + * @return + * @throws IOException + */ + public static String convertECPublicKeyX509ToPEM(byte[] encodedKey) throws IOException { + return convertEncodedDataToPEM(PEM_STRING_PUBLIC, encodedKey); + } + + /** + * 将PEM格式的公钥转为X509标准的字节流 + * + * @param pemString + * @return + * @throws IOException + */ + public static byte[] convertECPublicKeyPEMToX509(String pemString) throws IOException { + return convertPEMToEncodedData(pemString); + } + + /** + * copy from BC + * + * @param genSpec + * @return + */ + public static X9ECParameters getDomainParametersFromGenSpec(ECGenParameterSpec genSpec) { + return getDomainParametersFromName(genSpec.getName()); + } + + /** + * copy from BC + * + * @param curveName + * @return + */ + public static X9ECParameters getDomainParametersFromName(String curveName) { + X9ECParameters domainParameters; + try { + if (curveName.charAt(0) >= '0' && curveName.charAt(0) <= '2') { + ASN1ObjectIdentifier oidID = new ASN1ObjectIdentifier(curveName); + domainParameters = ECUtil.getNamedCurveByOid(oidID); + } else { + if (curveName.indexOf(' ') > 0) { + curveName = curveName.substring(curveName.indexOf(' ') + 1); + domainParameters = ECUtil.getNamedCurveByName(curveName); + } else { + domainParameters = ECUtil.getNamedCurveByName(curveName); + } + } + } catch (IllegalArgumentException ex) { + domainParameters = ECUtil.getNamedCurveByName(curveName); + } + return domainParameters; + } + + /** + * copy from BC + * + * @param ecSpec + * @param withCompression + * @return + */ + public static X962Parameters getDomainParametersFromName( + java.security.spec.ECParameterSpec ecSpec, boolean withCompression) { + X962Parameters params; + + if (ecSpec instanceof ECNamedCurveSpec) { + ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec) ecSpec).getName()); + if (curveOid == null) { + curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec) ecSpec).getName()); + } + params = new X962Parameters(curveOid); + } else if (ecSpec == null) { + params = new X962Parameters(DERNull.INSTANCE); + } else { + ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve()); + + X9ECParameters ecP = new X9ECParameters( + curve, + new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), withCompression), + ecSpec.getOrder(), + BigInteger.valueOf(ecSpec.getCofactor()), + ecSpec.getCurve().getSeed()); + + //// 如果是1.62或更低版本的bcprov-jdk15on应该使用以下这段代码,因为高版本的EC5Util.convertPoint没有向下兼容 + /* + X9ECParameters ecP = new X9ECParameters( + curve, + EC5Util.convertPoint(curve, ecSpec.getGenerator(), withCompression), + ecSpec.getOrder(), + BigInteger.valueOf(ecSpec.getCofactor()), + ecSpec.getCurve().getSeed()); + */ + + params = new X962Parameters(ecP); + } + + return params; + } + + private static String convertEncodedDataToPEM(String type, byte[] encodedData) throws IOException { + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + PemWriter pWrt = new PemWriter(new OutputStreamWriter(bOut)); + try { + PemObject pemObj = new PemObject(type, encodedData); + pWrt.writeObject(pemObj); + } finally { + pWrt.close(); + } + return new String(bOut.toByteArray()); + } + + private static byte[] convertPEMToEncodedData(String pemString) throws IOException { + ByteArrayInputStream bIn = new ByteArrayInputStream(pemString.getBytes()); + PemReader pRdr = new PemReader(new InputStreamReader(bIn)); + try { + PemObject pemObject = pRdr.readPemObject(); + return pemObject.getContent(); + } finally { + pRdr.close(); + } + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/service/SM2PreprocessSigner.java b/ukey-tool/src/main/java/com/cisd/ukey/service/SM2PreprocessSigner.java new file mode 100644 index 0000000..85f601b --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/service/SM2PreprocessSigner.java @@ -0,0 +1,294 @@ +package com.cisd.ukey.service; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.DSAKCalculator; +import org.bouncycastle.crypto.signers.RandomDSAKCalculator; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECConstants; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECMultiplier; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * 有的国密需求是用户可以自己做预处理,签名验签只是对预处理的结果进行签名和验签 + */ +public class SM2PreprocessSigner implements ECConstants { + private static final int DIGEST_LENGTH = 32; // bytes + + private final DSAKCalculator kCalculator = new RandomDSAKCalculator(); + private Digest digest = null; + + private ECDomainParameters ecParams; + private ECPoint pubPoint; + private ECKeyParameters ecKey; + private byte[] userID; + + /** + * 初始化 + * + * @param forSigning true表示用于签名,false表示用于验签 + * @param param + */ + public void init(boolean forSigning, CipherParameters param) { + init(forSigning, new SM3Digest(), param); + } + + /** + * 初始化 + * + * @param forSigning true表示用于签名,false表示用于验签 + * @param digest SM2算法的话,一般是采用SM3摘要算法 + * @param param + * @throws RuntimeException + */ + public void init(boolean forSigning, Digest digest, CipherParameters param) throws RuntimeException { + CipherParameters baseParam; + + if (digest.getDigestSize() != DIGEST_LENGTH) { + throw new RuntimeException("Digest size must be " + DIGEST_LENGTH); + } + this.digest = digest; + + if (param instanceof ParametersWithID) { + baseParam = ((ParametersWithID) param).getParameters(); + userID = ((ParametersWithID) param).getID(); + } else { + baseParam = param; + userID = Hex.decode("31323334353637383132333435363738"); // the default value + } + + if (forSigning) { + if (baseParam instanceof ParametersWithRandom) { + ParametersWithRandom rParam = (ParametersWithRandom) baseParam; + + ecKey = (ECKeyParameters) rParam.getParameters(); + ecParams = ecKey.getParameters(); + kCalculator.init(ecParams.getN(), rParam.getRandom()); + } else { + ecKey = (ECKeyParameters) baseParam; + ecParams = ecKey.getParameters(); + kCalculator.init(ecParams.getN(), CryptoServicesRegistrar.getSecureRandom()); + } + pubPoint = createBasePointMultiplier().multiply(ecParams.getG(), ((ECPrivateKeyParameters) ecKey).getD()).normalize(); + } else { + ecKey = (ECKeyParameters) baseParam; + ecParams = ecKey.getParameters(); + pubPoint = ((ECPublicKeyParameters) ecKey).getQ(); + } + } + + /** + * 预处理,辅助方法 + * ZA=H256(ENT LA ∥ IDA ∥ a ∥ b ∥ xG ∥yG ∥ xA ∥ yA)。 + * M=ZA ∥ M; + * e = Hv(M) + * + * @return + */ + public byte[] preprocess(byte[] m) { + return preprocess(m, 0, m.length); + } + + + public byte[] preprocess(byte[] m, int off, int len) { + byte[] z = getZ(userID); + digest.update(z, 0, z.length); + digest.update(m, off, len); + byte[] eHash = new byte[DIGEST_LENGTH]; + digest.doFinal(eHash, 0); + return eHash; + } + + public boolean verifySignature(byte[] eHash, byte[] signature) { + try { + BigInteger[] rs = derDecode(signature); + if (rs != null) { + return verifySignature(eHash, rs[0], rs[1]); + } + } catch (IOException e) { + } + + return false; + } + + public void reset() { + digest.reset(); + } + + public byte[] generateSignature(byte[] eHash) throws CryptoException { + BigInteger n = ecParams.getN(); + BigInteger e = calculateE(eHash); + BigInteger d = ((ECPrivateKeyParameters) ecKey).getD(); + + BigInteger r, s; + + ECMultiplier basePointMultiplier = createBasePointMultiplier(); + + // 5.2.1 Draft RFC: SM2 Public Key Algorithms + do // generate s + { + BigInteger k; + do // generate r + { + // A3 + k = kCalculator.nextK(); + + // A4 + ECPoint p = basePointMultiplier.multiply(ecParams.getG(), k).normalize(); + + // A5 + r = e.add(p.getAffineXCoord().toBigInteger()).mod(n); + } + while (r.equals(ZERO) || r.add(k).equals(n)); + + // A6 + BigInteger dPlus1ModN = d.add(ONE).modInverse(n); + + s = k.subtract(r.multiply(d)).mod(n); + s = dPlus1ModN.multiply(s).mod(n); + } + while (s.equals(ZERO)); + + // A7 + try { + return derEncode(r, s); + } catch (IOException ex) { + throw new CryptoException("unable to encode signature: " + ex.getMessage(), ex); + } + } + + private boolean verifySignature(byte[] eHash, BigInteger r, BigInteger s) { + BigInteger n = ecParams.getN(); + + // 5.3.1 Draft RFC: SM2 Public Key Algorithms + // B1 + if (r.compareTo(ONE) < 0 || r.compareTo(n) >= 0) { + return false; + } + + // B2 + if (s.compareTo(ONE) < 0 || s.compareTo(n) >= 0) { + return false; + } + + // B3 eHash + + // B4 + BigInteger e = calculateE(eHash); + + // B5 + BigInteger t = r.add(s).mod(n); + if (t.equals(ZERO)) { + return false; + } + + // B6 + ECPoint q = ((ECPublicKeyParameters) ecKey).getQ(); + ECPoint x1y1 = ECAlgorithms.sumOfTwoMultiplies(ecParams.getG(), s, q, t).normalize(); + if (x1y1.isInfinity()) { + return false; + } + + // B7 + BigInteger expectedR = e.add(x1y1.getAffineXCoord().toBigInteger()).mod(n); + + return expectedR.equals(r); + } + + private byte[] digestDoFinal() { + byte[] result = new byte[digest.getDigestSize()]; + digest.doFinal(result, 0); + + reset(); + + return result; + } + + private byte[] getZ(byte[] userID) { + digest.reset(); + + addUserID(digest, userID); + + addFieldElement(digest, ecParams.getCurve().getA()); + addFieldElement(digest, ecParams.getCurve().getB()); + addFieldElement(digest, ecParams.getG().getAffineXCoord()); + addFieldElement(digest, ecParams.getG().getAffineYCoord()); + addFieldElement(digest, pubPoint.getAffineXCoord()); + addFieldElement(digest, pubPoint.getAffineYCoord()); + + byte[] result = new byte[digest.getDigestSize()]; + + digest.doFinal(result, 0); + + return result; + } + + private void addUserID(Digest digest, byte[] userID) { + int len = userID.length * 8; + digest.update((byte) (len >> 8 & 0xFF)); + digest.update((byte) (len & 0xFF)); + digest.update(userID, 0, userID.length); + } + + private void addFieldElement(Digest digest, ECFieldElement v) { + byte[] p = v.getEncoded(); + digest.update(p, 0, p.length); + } + + protected ECMultiplier createBasePointMultiplier() { + return new FixedPointCombMultiplier(); + } + + protected BigInteger calculateE(byte[] message) { + return new BigInteger(1, message); + } + + protected BigInteger[] derDecode(byte[] encoding) + throws IOException { + ASN1Sequence seq = ASN1Sequence.getInstance(ASN1Primitive.fromByteArray(encoding)); + if (seq.size() != 2) { + return null; + } + + BigInteger r = ASN1Integer.getInstance(seq.getObjectAt(0)).getValue(); + BigInteger s = ASN1Integer.getInstance(seq.getObjectAt(1)).getValue(); + + byte[] expectedEncoding = derEncode(r, s); + if (!Arrays.constantTimeAreEqual(expectedEncoding, encoding)) { + return null; + } + + return new BigInteger[]{r, s}; + } + + protected byte[] derEncode(BigInteger r, BigInteger s) + throws IOException { + + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + return new DERSequence(v).getEncoded(ASN1Encoding.DER); + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/service/SkfService.java b/ukey-tool/src/main/java/com/cisd/ukey/service/SkfService.java new file mode 100644 index 0000000..0f0a355 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/service/SkfService.java @@ -0,0 +1,306 @@ +package com.cisd.ukey.service; + +import com.cisd.ukey.config.SkfProperties; +import com.cisd.ukey.jna.DevInfo; +import com.cisd.ukey.jna.EccPublicKeyBlob; +import com.cisd.ukey.jna.EccSignatureBlob; +import com.cisd.ukey.jna.FileAttribute; +import com.cisd.ukey.jna.SkfLibrary; +import com.sun.jna.Memory; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.bouncycastle.crypto.CipherParameters; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +@Service +public class SkfService { + + public static final int ADMIN_TYPE = 0; + public static final int USER_TYPE = 1; + private static final int SAR_APPLICATION_NOT_EXISTS = 0x0A00002E; + private static final int SAR_FILE_ALREADY_EXIST = 0x0A00002F; + private static final int SAR_FILE_NOT_EXIST = 0x0A000031; + private static final int FILE_ACCESS_ANYONE = 0x000000FF; + + private final SkfSessionTemplate sessionTemplate; + private final SkfProperties properties; + + public SkfService(SkfSessionTemplate sessionTemplate, SkfProperties properties) { + this.sessionTemplate = sessionTemplate; + this.properties = properties; + } + + public List listDevices(boolean presentOnly) { + SkfLibrary lib = sessionTemplate.library(); + IntByReference size = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_EnumDev", lib.SKF_EnumDev(presentOnly ? 1 : 0, Pointer.NULL, size)); + Memory buffer = new Memory(size.getValue()); + sessionTemplate.ensureSuccess("SKF_EnumDev", lib.SKF_EnumDev(presentOnly ? 1 : 0, buffer, size)); + List result = new ArrayList<>(); + for (String deviceName : parseMultiString(buffer.getByteArray(0, size.getValue()))) { + result.add(sessionTemplate.withDevice("listDevices", deviceName, (nativeLib, devHandle) -> { + DevInfo devInfo = new DevInfo(); + sessionTemplate.ensureSuccess("SKF_GetDevInfo", nativeLib.SKF_GetDevInfo(devHandle, devInfo)); + devInfo.read(); + return new DeviceInfo( + deviceName, + trimCString(devInfo.serialNumber), + trimCString(devInfo.label), + trimCString(devInfo.manufacturer), + trimCString(devInfo.issuer) + ); + })); + } + return result; + } + + public PinVerifyResult verifyPin(String deviceName, int pinType, String pin) { + return sessionTemplate.withApplication("verifyPin", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + IntByReference retryCount = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_VerifyPIN", + lib.SKF_VerifyPIN(appHandle, pinType, sessionTemplate.cString(pin), retryCount)); + return pinInfo(lib, appHandle, pinType, retryCount.getValue()); + }); + } + + public PinVerifyResult changePin(String deviceName, int pinType, String oldPin, String newPin) { + return sessionTemplate.withApplication("changePin", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + IntByReference retryCount = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_ChangePIN", + lib.SKF_ChangePIN(appHandle, pinType, sessionTemplate.cString(oldPin), sessionTemplate.cString(newPin), retryCount)); + return pinInfo(lib, appHandle, pinType, retryCount.getValue()); + }); + } + + public PinVerifyResult unblockPin(String deviceName, String adminPin, String newUserPin) { + verifyPin(deviceName, ADMIN_TYPE, adminPin); + return sessionTemplate.withApplication("resetPin", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + IntByReference retryCount = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_UnblockPIN", + lib.SKF_UnblockPIN(appHandle, sessionTemplate.cString(adminPin), sessionTemplate.cString(newUserPin), retryCount)); + return pinInfo(lib, appHandle, USER_TYPE, retryCount.getValue()); + }); + } + + public List listFiles(String deviceName) { + return sessionTemplate.withApplication("listFiles", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + IntByReference size = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_EnumFiles", lib.SKF_EnumFiles(appHandle, Pointer.NULL, size)); + Memory buffer = new Memory(size.getValue()); + sessionTemplate.ensureSuccess("SKF_EnumFiles", lib.SKF_EnumFiles(appHandle, buffer, size)); + List result = new ArrayList<>(); + for (String fileName : parseMultiString(buffer.getByteArray(0, size.getValue()))) { + FileAttribute attribute = new FileAttribute(); + sessionTemplate.ensureSuccess("SKF_GetFileInfo", + lib.SKF_GetFileInfo(appHandle, sessionTemplate.cString(fileName), attribute)); + attribute.read(); + result.add(new FileInfo(fileName, attribute.fileSize, attribute.readRights, attribute.writeRights)); + } + return result; + }); + } + + public FileContent readFile(String deviceName, String fileName) { + return sessionTemplate.withApplication("readFile", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + FileAttribute attribute = new FileAttribute(); + sessionTemplate.ensureSuccess("SKF_GetFileInfo", + lib.SKF_GetFileInfo(appHandle, sessionTemplate.cString(fileName), attribute)); + attribute.read(); + + int readSize = attribute.fileSize; + if (readSize <= 0) { + return new FileContent(fileName, 0, ""); + } + + Memory buffer = new Memory(readSize); + IntByReference outLen = new IntByReference(readSize); + sessionTemplate.ensureSuccess("SKF_ReadFile", + lib.SKF_ReadFile(appHandle, sessionTemplate.cString(fileName), 0, readSize, buffer, outLen)); + byte[] data = buffer.getByteArray(0, outLen.getValue()); + return new FileContent(fileName, outLen.getValue(), new String(data, StandardCharsets.UTF_8)); + }); + } + + public FileContent writeFile(String deviceName, String fileName, String content) { + byte[] data = content.getBytes(StandardCharsets.UTF_8); + return sessionTemplate.withApplication("writeFile", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + FileAttribute attribute = new FileAttribute(); + int infoRc = lib.SKF_GetFileInfo(appHandle, sessionTemplate.cString(fileName), attribute); + if (infoRc == 0) { + throw new com.cisd.ukey.exception.SkfException("SKF_WriteFile", SAR_FILE_ALREADY_EXIST, "文件已被写入"); + } + if (infoRc != SAR_FILE_NOT_EXIST) { + sessionTemplate.ensureSuccess("SKF_GetFileInfo", infoRc); + } + + sessionTemplate.ensureSuccess("SKF_CreateFile", + lib.SKF_CreateFile(appHandle, sessionTemplate.cString(fileName), data.length, FILE_ACCESS_ANYONE, FILE_ACCESS_ANYONE)); + sessionTemplate.ensureSuccess("SKF_WriteFile", + lib.SKF_WriteFile(appHandle, sessionTemplate.cString(fileName), 0, data, data.length)); + return new FileContent(fileName, data.length, content); + }); + } + + public void deleteFile(String deviceName, String fileName) { + sessionTemplate.withApplication("deleteFile", deviceName, properties.getApplicationName(), (lib, devHandle, appHandle) -> { + sessionTemplate.ensureSuccess("SKF_DeleteFile", lib.SKF_DeleteFile(appHandle, sessionTemplate.cString(fileName))); + return null; + }); + } + + public void deleteApplication(String deviceName) { + sessionTemplate.withAuthenticatedDevice("deleteApplication", deviceName, (lib, devHandle) -> { + int rc = lib.SKF_DeleteApplication(devHandle, sessionTemplate.cString(properties.getApplicationName())); + if (rc != 0 && rc != SAR_APPLICATION_NOT_EXISTS) { + sessionTemplate.ensureSuccess("SKF_DeleteApplication", rc); + } + return null; + }); + } + + public EccPublicKeyResult exportEccPublicKey(String deviceName, boolean signFlag) { + return sessionTemplate.withContainer("exportEccPublicKey", deviceName, properties.getApplicationName(), properties.getContainerName(), + (lib, devHandle, appHandle, containerHandle) -> { + IntByReference blobLen = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_ExportPublicKey", + lib.SKF_ExportPublicKey(containerHandle, signFlag ? 1 : 0, Pointer.NULL, blobLen)); + Memory blob = new Memory(blobLen.getValue()); + sessionTemplate.ensureSuccess("SKF_ExportPublicKey", + lib.SKF_ExportPublicKey(containerHandle, signFlag ? 1 : 0, blob, blobLen)); + byte[] raw = blob.getByteArray(0, blobLen.getValue()); + return new EccPublicKeyResult( + signFlag, + Base64.getEncoder().encodeToString(raw) + ); + }); + } + + public EccSignatureResult eccSign(String deviceName, String plainText) { + EccPublicKeyResult eccPublicKeyResult = exportEccPublicKey(deviceName, true); + byte[] data = preprocessForSm2Sign(eccPublicKeyResult.pubKey, plainText); + return sessionTemplate.withContainer("eccSign", deviceName, properties.getApplicationName(), properties.getContainerName(), + (lib, devHandle, appHandle, containerHandle) -> { + EccSignatureBlob signature = new EccSignatureBlob(); + sessionTemplate.ensureSuccess("SKF_ECCSignData", + lib.SKF_ECCSignData(containerHandle, data, data.length, signature)); + signature.read(); + return new EccSignatureResult( + Base64.getEncoder().encodeToString(signature.getPointer().getByteArray(0, signature.size())), + toHex(signature.r), + toHex(signature.s) + ); + }); + } + + public VerifyResult eccVerify(String deviceName, String pubKey, String plainText, String signatureBase64) { + byte[] data = preprocessForSm2Sign(pubKey, plainText); + byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64); + byte[] pubkeyBytes = Base64.getDecoder().decode(pubKey); + return sessionTemplate.withContainer("eccVerify", deviceName, properties.getApplicationName(), properties.getContainerName(), + (lib, devHandle, appHandle, containerHandle) -> { + EccPublicKeyBlob publicKey = new EccPublicKeyBlob(); + publicKey.getPointer().write(0, pubkeyBytes, 0, Math.min(pubkeyBytes.length, publicKey.size())); + publicKey.read(); + + EccSignatureBlob signature = new EccSignatureBlob(); + signature.write(); + signature.getPointer().write(0, signatureBytes, 0, Math.min(signatureBytes.length, signature.size())); + signature.read(); + + sessionTemplate.ensureSuccess("SKF_ECCVerify", + lib.SKF_ECCVerify(devHandle, publicKey, data, data.length, signature)); + return new VerifyResult(true); + }); + } + + private byte[] preprocessForSm2Sign(String publicKey, String plainText) { + byte[] source = plainText.getBytes(StandardCharsets.UTF_8); + byte[] pubBytes = Base64.getDecoder().decode(publicKey); + byte[] x = Arrays.copyOfRange(pubBytes, 36, 68); + byte[] y = Arrays.copyOfRange(pubBytes, 100, 132); + + CipherParameters parameters = BCECUtils.createECPublicKeyParameters(x, y); + SM2PreprocessSigner signer = new SM2PreprocessSigner(); + signer.init(false, parameters); + byte[] preHash = signer.preprocess(source); + return preHash; + } + + private PinVerifyResult pinInfo(SkfLibrary lib, Pointer applicationHandle, int pinType, int retryCountFromCall) { + IntByReference maxRetryCount = new IntByReference(); + IntByReference remainRetryCount = new IntByReference(); + IntByReference defaultPin = new IntByReference(); + sessionTemplate.ensureSuccess("SKF_GetPINInfo", + lib.SKF_GetPINInfo(applicationHandle, pinType, maxRetryCount, remainRetryCount, defaultPin)); + return new PinVerifyResult(pinType, maxRetryCount.getValue(), remainRetryCount.getValue(), defaultPin.getValue() != 0, retryCountFromCall); + } + + private List parseMultiString(byte[] bytes) { + List result = new ArrayList<>(); + int start = 0; + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] != 0) { + continue; + } + if (i == start) { + break; + } + result.add(new String(bytes, start, i - start, StandardCharsets.UTF_8)); + start = i + 1; + } + return result; + } + + private String trimCString(byte[] bytes) { + int len = 0; + while (len < bytes.length && bytes[len] != 0) { + len++; + } + return new String(bytes, 0, len, StandardCharsets.UTF_8).trim(); + } + + private String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte value : bytes) { + builder.append(String.format("%02X", value)); + } + return builder.toString(); + } + + private byte[] fromHex(String hex, int maxSize) { + byte[] result = new byte[maxSize]; + int usable = Math.min(hex.length() / 2, maxSize); + for (int i = 0; i < usable; i++) { + int index = i * 2; + result[i] = (byte) Integer.parseInt(hex.substring(index, index + 2), 16); + } + return result; + } + + public record DeviceInfo(String deviceName, String serialNumber, String label, String manufacturer, String issuer) { + } + + public record PinVerifyResult(int pinType, int maxRetryCount, int remainRetryCount, boolean defaultPin, int retryCountFromCall) { + } + + public record FileInfo(String fileName, int fileSize, int readRights, int writeRights) { + } + + public record FileContent(String fileName, int size, String content) { + } + + public record EccPublicKeyResult(boolean signFlag, String pubKey) { + } + + public record EccSignatureResult(String signatureBase64, String rHex, String sHex) { + } + + public record VerifyResult(boolean success) { + } +} diff --git a/ukey-tool/src/main/java/com/cisd/ukey/service/SkfSessionTemplate.java b/ukey-tool/src/main/java/com/cisd/ukey/service/SkfSessionTemplate.java new file mode 100644 index 0000000..112c919 --- /dev/null +++ b/ukey-tool/src/main/java/com/cisd/ukey/service/SkfSessionTemplate.java @@ -0,0 +1,297 @@ +package com.cisd.ukey.service; + +import com.cisd.ukey.config.SkfProperties; +import com.cisd.ukey.exception.SkfException; +import com.cisd.ukey.jna.BlockCipherParam; +import com.cisd.ukey.jna.DevInfo; +import com.cisd.ukey.jna.EccPublicKeyBlob; +import com.cisd.ukey.jna.SkfLibrary; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLibrary; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +@Component +public class SkfSessionTemplate { + + private static final int SAR_APPLICATION_NOT_EXISTS = 0x0A00002E; + private static final int SAR_CONTAINER_NOT_EXISTS = 0x0A000036; + private static final int SGD_SM1 = 0x00000100; + private static final int SGD_SM4 = 0x00000400; + private static final int SGD_AES128 = 0x00000800; + private static final int SGD_AES192 = 0x00000810; + private static final int SGD_AES256 = 0x00000820; + private static final int SGD_DES = 0x00001000; + private static final int SGD_D3DES = 0x00001010; + private static final int SGD_T3DES = 0x00001020; + private static final int SGD_SM7 = 0x00002000; + + @FunctionalInterface + public interface DeviceCallback { + T apply(SkfLibrary lib, Pointer devHandle); + } + + @FunctionalInterface + public interface ApplicationCallback { + T apply(SkfLibrary lib, Pointer devHandle, Pointer applicationHandle); + } + + @FunctionalInterface + public interface ContainerCallback { + T apply(SkfLibrary lib, Pointer devHandle, Pointer applicationHandle, Pointer containerHandle); + } + + private final Object lock = new Object(); + private final SkfProperties properties; + private volatile SkfLibrary library; + + public SkfSessionTemplate(SkfProperties properties) { + this.properties = properties; + } + + public T withDevice(String apiName, String deviceName, DeviceCallback callback) { + synchronized (lock) { + SkfLibrary lib = ensureLibraryLoaded(); + Pointer devHandle = connectDevice(apiName, lib, deviceName); + try { + return callback.apply(lib, devHandle); + } finally { + disconnectQuietly(lib, devHandle); + } + } + } + + public T withApplication(String apiName, String deviceName, String applicationName, ApplicationCallback callback) { + return withDevice(apiName, deviceName, (lib, devHandle) -> { + Pointer applicationHandle = ensureApplication(apiName, lib, devHandle, applicationName); + try { + return callback.apply(lib, devHandle, applicationHandle); + } finally { + closeApplicationQuietly(lib, applicationHandle); + } + }); + } + + public T withAuthenticatedDevice(String apiName, String deviceName, DeviceCallback callback) { + return withDevice(apiName, deviceName, (lib, devHandle) -> { + authenticateDevice(apiName, lib, devHandle); + return callback.apply(lib, devHandle); + }); + } + + public T withContainer(String apiName, String deviceName, String applicationName, String containerName, + ContainerCallback callback) { + return withApplication(apiName, deviceName, applicationName, (lib, devHandle, applicationHandle) -> { + Pointer containerHandle = ensureContainer(apiName, lib, applicationHandle, containerName); + try { + return callback.apply(lib, devHandle, applicationHandle, containerHandle); + } finally { + closeContainerQuietly(lib, containerHandle); + } + }); + } + + public SkfLibrary library() { + return ensureLibraryLoaded(); + } + + public void ensureSuccess(String apiName, int retCode) { + if (retCode != 0) { + throw new SkfException(apiName, retCode, apiName + " failed, ret=0x" + Integer.toHexString(retCode).toUpperCase()); + } + } + + public byte[] cString(String value) { + return (value + "\0").getBytes(StandardCharsets.UTF_8); + } + + private SkfLibrary ensureLibraryLoaded() { + SkfLibrary cached = library; + if (cached != null) { + return cached; + } + synchronized (this) { + if (library != null) { + return library; + } + Map options = new HashMap<>(); + options.put(Library.OPTION_STRING_ENCODING, "UTF-8"); + try { + Path extractDir = Path.of(properties.getLibDir()).toAbsolutePath().normalize(); + Path libPath = extractDir.resolve(properties.getVendorLibName()).toAbsolutePath().normalize(); + NativeLibrary.addSearchPath(stripExtension(properties.getVendorLibName()), extractDir.toString()); + library = Native.load(libPath.toString(), SkfLibrary.class, options); + } catch (UnsatisfiedLinkError ex) { + throw new SkfException("Native.load", "load SKF native library failed", ex); + } + return library; + } + } + + private String stripExtension(String fileName) { + int index = fileName.lastIndexOf('.'); + return index > 0 ? fileName.substring(0, index) : fileName; + } + + private Pointer connectDevice(String apiName, SkfLibrary lib, String deviceName) { + PointerByReference ref = new PointerByReference(); + ensureSuccess(apiName + ":SKF_ConnectDev", lib.SKF_ConnectDev(cString(deviceName), ref)); + return ref.getValue(); + } + + private Pointer openApplication(String apiName, SkfLibrary lib, Pointer devHandle, String applicationName) { + PointerByReference ref = new PointerByReference(); + ensureSuccess(apiName + ":SKF_OpenApplication", lib.SKF_OpenApplication(devHandle, cString(applicationName), ref)); + return ref.getValue(); + } + + private Pointer ensureApplication(String apiName, SkfLibrary lib, Pointer devHandle, String applicationName) { + PointerByReference ref = new PointerByReference(); + int openRc = lib.SKF_OpenApplication(devHandle, cString(applicationName), ref); + if (openRc == 0) { + return ref.getValue(); + } + if (openRc != SAR_APPLICATION_NOT_EXISTS) { + ensureSuccess(apiName + ":SKF_OpenApplication", openRc); + } + + authenticateDevice(apiName, lib, devHandle); + int createRc = lib.SKF_CreateApplication( + devHandle, + cString(applicationName), + cString(properties.getInitAdminPin()), + properties.getAdminPinRetryCount(), + cString(properties.getInitUserPin()), + properties.getUserPinRetryCount(), + properties.getCreateFileRights(), + ref + ); + ensureSuccess(apiName + ":SKF_CreateApplication", createRc); + return ref.getValue(); + } + + private Pointer openContainer(String apiName, SkfLibrary lib, Pointer applicationHandle, String containerName) { + PointerByReference ref = new PointerByReference(); + ensureSuccess(apiName + ":SKF_OpenContainer", lib.SKF_OpenContainer(applicationHandle, cString(containerName), ref)); + return ref.getValue(); + } + + private Pointer ensureContainer(String apiName, SkfLibrary lib, Pointer applicationHandle, String containerName) { + PointerByReference ref = new PointerByReference(); + int openRc = lib.SKF_OpenContainer(applicationHandle, cString(containerName), ref); + if (openRc == 0) { + return ref.getValue(); + } + if (openRc != SAR_CONTAINER_NOT_EXISTS) { + ensureSuccess(apiName + ":SKF_OpenContainer", openRc); + } + + int createRc = lib.SKF_CreateContainer(applicationHandle, cString(containerName), ref); + ensureSuccess(apiName + ":SKF_CreateContainer", createRc); + EccPublicKeyBlob publicKeyBlob = new EccPublicKeyBlob(); + ensureSuccess(apiName + ":SKF_GenECCKeyPair", + lib.SKF_GenECCKeyPair(ref.getValue(), properties.getSignKeyAlgId(), publicKeyBlob)); + return ref.getValue(); + } + + private void authenticateDevice(String apiName, SkfLibrary lib, Pointer devHandle) { + DevInfo devInfo = new DevInfo(); + ensureSuccess(apiName + ":SKF_GetDevInfo", lib.SKF_GetDevInfo(devHandle, devInfo)); + devInfo.read(); + + byte[] random = new byte[16]; + ensureSuccess(apiName + ":SKF_GenRandom", lib.SKF_GenRandom(devHandle, random, random.length)); + + PointerByReference keyHandleRef = new PointerByReference(); + byte[] authKey = normalizeAuthKey(properties.getDeviceAuthKey(), devInfo.devAuthAlgId); + ensureSuccess(apiName + ":SKF_SetSymmKey", + lib.SKF_SetSymmKey(devHandle, authKey, devInfo.devAuthAlgId, keyHandleRef)); + + Pointer keyHandle = keyHandleRef.getValue(); + try { + BlockCipherParam cipherParam = new BlockCipherParam(); + cipherParam.ivLen = 0; + cipherParam.paddingType = 0; + cipherParam.feedBitLen = 0; + cipherParam.write(); + + ensureSuccess(apiName + ":SKF_EncryptInit", lib.SKF_EncryptInit(keyHandle, cipherParam)); + + byte[] encrypted = new byte[random.length + 32]; + IntByReference encryptedLen = new IntByReference(encrypted.length); + ensureSuccess(apiName + ":SKF_Encrypt", + lib.SKF_Encrypt(keyHandle, random, random.length, encrypted, encryptedLen)); + + byte[] authData = new byte[encryptedLen.getValue()]; + System.arraycopy(encrypted, 0, authData, 0, encryptedLen.getValue()); + ensureSuccess(apiName + ":SKF_DevAuth", lib.SKF_DevAuth(devHandle, authData, authData.length)); + } finally { + closeHandleQuietly(lib, keyHandle); + } + } + + private byte[] normalizeAuthKey(String configuredKey, int algId) { + byte[] raw = configuredKey.getBytes(StandardCharsets.UTF_8); + if (raw.length == 0) { + throw new SkfException("SKF_DevAuth", -1, "ukey.skf.device-auth-key must not be blank"); + } + int keyLen = requiredKeyLength(algId); + if (raw.length == keyLen) { + return raw; + } + byte[] normalized = new byte[keyLen]; + for (int i = 0; i < keyLen; i++) { + normalized[i] = raw[i % raw.length]; + } + return normalized; + } + + private int requiredKeyLength(int algId) { + int family = algId & 0xFFFFFFF0; + if (family == SGD_AES256) { + return 32; + } + if (family == SGD_AES192 || family == SGD_T3DES) { + return 24; + } + if (family == SGD_SM4 || family == SGD_AES128 || family == SGD_SM1 || family == SGD_SM7 || family == SGD_D3DES) { + return 16; + } + if (family == SGD_DES) { + return 8; + } + return 16; + } + + private void disconnectQuietly(SkfLibrary lib, Pointer devHandle) { + if (devHandle != null) { + lib.SKF_DisConnectDev(devHandle); + } + } + + private void closeApplicationQuietly(SkfLibrary lib, Pointer applicationHandle) { + if (applicationHandle != null) { + lib.SKF_CloseApplication(applicationHandle); + } + } + + private void closeContainerQuietly(SkfLibrary lib, Pointer containerHandle) { + if (containerHandle != null) { + lib.SKF_CloseContainer(containerHandle); + } + } + + private void closeHandleQuietly(SkfLibrary lib, Pointer handle) { + if (handle != null) { + lib.SKF_CloseHandle(handle); + } + } +} diff --git a/ukey-tool/src/main/resources/application.yml b/ukey-tool/src/main/resources/application.yml new file mode 100644 index 0000000..be66e84 --- /dev/null +++ b/ukey-tool/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 8690 + +ukey: + skf: + create-file-rights: 255 + sign-key-alg-id: 131328 +spring: + main: + banner-mode: "off" \ No newline at end of file