实现 BCSdfApiService 中 sm4 加解密接口

This commit is contained in:
Cheney 2024-11-14 15:31:11 +08:00
parent b500a4e9cc
commit a98659f3a9
10 changed files with 482 additions and 17 deletions

View File

@ -0,0 +1,36 @@
package com.sunyard.chsm.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 算法的轮模式
* @author Cheney
*/
@Getter
@AllArgsConstructor
public enum AlgMode {
ECB("ECB", "ECB"),
CBC( "CBC", "CBC"),
;
private final String code;
private final String desc;
public static AlgMode of(String code) {
if (code == null || code.trim().isEmpty()) {
return null;
}
return Arrays.stream(AlgMode.values())
.filter(it -> Objects.equals(it.getCode(), code))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,34 @@
package com.sunyard.chsm.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 数据的填充模式
* @author Cheney
*/
@Getter
@AllArgsConstructor
public enum Padding {
NOPadding("NoPadding", "NoPadding"),
PCKS5Padding( "PKCS5Padding", "PKCS5Padding"),
;
private final String code;
private final String desc;
public static Padding of(String code) {
if (code == null || code.trim().isEmpty()) {
return null;
}
return Arrays.stream(Padding.values())
.filter(it -> Objects.equals(it.getCode(), code))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,61 @@
package com.sunyard.chsm.sdf;
import com.sunyard.chsm.enums.AlgMode;
import com.sunyard.chsm.enums.KeyAlg;
import com.sunyard.chsm.enums.Padding;
import java.util.HashMap;
import java.util.Map;
import static com.sunyard.chsm.utils.gm.BCSM4Utils.DEFAULT_KEY_SIZE;
/**
* 实现 SdfApiService 接口中的通用逻辑
* 处理参数缺失时的默认参数确保下层入参没有 null
* @author Cheney
*/
public abstract class AbstractSdfApiService implements SdfApiService {
private static final Map<KeyAlg, Integer> algKeyLen = new HashMap<>();
static {
algKeyLen.put( KeyAlg.SM4, DEFAULT_KEY_SIZE );
}
/**
* 处理算法的默认密钥长度
* @param alg 算法只支持对称算法
* 密钥长度根据 alg 指定的算法自动选择
* @return 密钥
*/
@Override
public byte[] genSymKey(KeyAlg alg) {
return genSymKey( alg, algKeyLen.get( alg ) );
}
/**
* 对称加密
* 内部实现和解密共用逻辑
* @param alg 算法只支持对称算法
* @param key 密钥值明文
* @param data 原始数据
*/
@Override
public byte[] symEncrypt(KeyAlg alg, byte[] key, byte[] data){
return symEncrypt( alg, AlgMode.ECB, Padding.PCKS5Padding, key, data );
}
/**
* 对称解密
* 内部实现和加密共用逻辑
* @param alg 算法只支持对称算法
* @param key 密钥值明文
* @param data 密文数据
*/
@Override
public byte[] symDecrypt(KeyAlg alg, byte[] key, byte[] data) {
return symDecrypt( alg, AlgMode.ECB, Padding.PCKS5Padding, key, data );
}
}

View File

@ -1,29 +1,50 @@
package com.sunyard.chsm.sdf;
import com.sunyard.chsm.enums.AlgMode;
import com.sunyard.chsm.enums.KeyAlg;
import com.sunyard.chsm.enums.KeyCategory;
import com.sunyard.chsm.enums.Padding;
import com.sunyard.chsm.sdf.model.EccKey;
import com.sunyard.chsm.sdf.util.LangUtils;
import com.sunyard.chsm.utils.gm.BCSM2Utils;
import com.sunyard.chsm.utils.gm.BCSM3Utils;
import com.sunyard.chsm.utils.gm.BCSM4Utils;
import lombok.SneakyThrows;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.util.BigIntegers;
import org.springframework.stereotype.Service;
import java.security.KeyPair;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
/**
* @author liulu
* 基于 BC 库的国密软算法实现
* 当前实现类的接口返回的对称密钥和私钥认为是明文在上层 Service 进行使用和存储时的加密和解密运算
* <p>
* 主密钥
* - 生成 软随机数项目启动时不存在则自动生成
* - 存储 存储在 SC_PARAM_CONF , KEY mk 的字段值为固定的 48 字节 HEX 格式 96 字节存储
* 16 字节为主密钥值 32 字节为主密钥值使用 SM3 计算的校验值
* - 同步基于数据库进行导入导出和备份恢复多台应用服务器连接同一个数据库无需同步
* - 使用顶层密钥用于保护其他存于数据的密钥
*
* @author liulu Cheney
* @since 2024/10/23
*/
@Slf4j
@Service
public class BCSdfApiService implements SdfApiService {
public class BCSdfApiService extends AbstractSdfApiService {
public BCSdfApiService() {
super();
}
@Override
public byte[] generateRandom(int len) {
@ -32,6 +53,82 @@ public class BCSdfApiService implements SdfApiService {
return res;
}
/**
* 生成对称算法密钥
*
* @param alg 算法只支持对称算法
* @param keyLen 密钥长度(bit)只针对密钥长度可变的算法
* 禁止传 null
* @return
*/
@Override
public byte[] genSymKey(KeyAlg alg, Integer keyLen) {
switch (alg) {
case SM4: {
if (null == keyLen || keyLen != BCSM4Utils.DEFAULT_KEY_SIZE) {
throw new IllegalArgumentException("Invalid key length: " + keyLen);
}
try {
return BCSM4Utils.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException("算法实现错误", e);
}
}
default: {
throw new IllegalArgumentException("Unsupported algorithm: " + alg);
}
}
}
@Override
public byte[] symEncrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data) {
return symCalc(Cipher.ENCRYPT_MODE, alg, mode, padding, key, data);
}
@Override
public byte[] symDecrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data) {
return symCalc(Cipher.DECRYPT_MODE, alg, mode, padding, key, data);
}
/**
* 对称加解密的统一实现
*
* @param alg 算法只支持对称算法
* @param key 密钥值明文
* @param data 加密时明文数据解密时为密文数据
*/
private byte[] symCalc(int cipherMode, KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data) {
if (alg.getCategory() != KeyCategory.SYM_KEY) {
throw new IllegalArgumentException("Must SYM_KEY, unsupported algorithm: " + alg);
}
// 算法
String algName = null;
if (alg == KeyAlg.SM4) {
algName = "SM4/";
} else {
throw new IllegalArgumentException("Unsupported algorithm: " + alg);
}
// 算法轮模式
algName += mode.getCode() + "/";
// 填充模式
algName += padding.getCode();
Cipher cipher = null;
try {
cipher = BCSM4Utils.generateECBCipher(algName, cipherMode, key);
return cipher.doFinal(data);
} catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException("算法执行错误", e);
}
}
@SneakyThrows
@Override
public EccKey genKeyPairEcc() {
@ -53,14 +150,7 @@ public class BCSdfApiService implements SdfApiService {
@Override
public byte[] hmac(byte[] key, byte[] srcData) {
KeyParameter keyParameter = new KeyParameter(key);
SM3Digest digest = new SM3Digest();
HMac mac = new HMac(digest);
mac.init(keyParameter);
mac.update(srcData, 0, srcData.length);
byte[] result = new byte[mac.getMacSize()];
mac.doFinal(result, 0);
return result;
return BCSM3Utils.hmac(key, srcData);
}
@Override

View File

@ -1,8 +1,12 @@
package com.sunyard.chsm.sdf;
import com.sunyard.chsm.enums.AlgMode;
import com.sunyard.chsm.enums.KeyAlg;
import com.sunyard.chsm.enums.Padding;
import com.sunyard.chsm.sdf.model.EccKey;
/**
* @author liulu
* @since 2024/10/23
@ -18,6 +22,44 @@ public interface SdfApiService {
*/
byte[] generateRandom(int len);
/**
* 生成对称密钥
* @param alg 算法只支持对称算法
* @param keyLen 密钥长度(bit)只针对密钥长度可变的算法
* 禁止传 null
* @return 对称密钥
*/
byte[] genSymKey(KeyAlg alg, Integer keyLen);
byte[] genSymKey(KeyAlg alg);
/**
* 对称加密
* @param alg 算法只支持对称算法
* @param mode 轮模式
* @param padding 填充模式
* @param key 密钥值明文
* @param data 原始数据
*/
byte[] symEncrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data);
byte[] symEncrypt(KeyAlg alg, byte[] key, byte[] data);
/**
* 对称解密
* @param alg 算法只支持对称算法
* @param key 密钥值明文
* @param mode 轮模式
* @param padding 填充模式
* @param data 密文数据
*/
byte[] symDecrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data);
byte[] symDecrypt(KeyAlg alg, byte[] key, byte[] data);
/**
* 产生ECC密钥对并输出
*

View File

@ -170,7 +170,7 @@ public class BCSM4Utils extends GMBaseUtil {
return mac.doFinal();
}
private static Cipher generateECBCipher(String algorithmName, int mode, byte[] key)
public static Cipher generateECBCipher(String algorithmName, int mode, byte[] key)
throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
InvalidKeyException {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
@ -179,7 +179,7 @@ public class BCSM4Utils extends GMBaseUtil {
return cipher;
}
private static Cipher generateCBCCipher(String algorithmName, int mode, byte[] key, byte[] iv)
public static Cipher generateCBCCipher(String algorithmName, int mode, byte[] key, byte[] iv)
throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchProviderException, NoSuchPaddingException {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);

View File

@ -19,6 +19,21 @@
<dependencies>
<!-- JUnit 5 API (仅单元测试使用)-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test (仅单元测试使用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sunyard.chsm</groupId>
<artifactId>chsm-common</artifactId>

View File

@ -0,0 +1,34 @@
package sdf;
import com.sunyard.chsm.WebServerApp;
import com.sunyard.chsm.enums.KeyAlg;
import com.sunyard.chsm.sdf.BCSdfApiService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Random;
@Slf4j
@SpringBootTest(classes = WebServerApp.class)
public class BCSdfApiServiceTest {
@Autowired
private BCSdfApiService bcSdfApiService;
// 对称加密解密测试
@Test
void testSym() {
byte[] data = new byte[128];
new Random().nextBytes( data );
byte[] key = bcSdfApiService.genSymKey( KeyAlg.SM4 );
byte[] enData = bcSdfApiService.symEncrypt( KeyAlg.SM4, key, data );
byte[] deData = bcSdfApiService.symDecrypt( KeyAlg.SM4, key, enData );
Assert.assertArrayEquals( data, deData );
}
}

View File

@ -0,0 +1,62 @@
server:
port: 89
tomcat:
uri-encoding: UTF-8
threads:
max: 1000
min-spare: 30
spring:
main:
allow-circular-references: true
# 数据源
datasource:
driverClassName: dm.jdbc.driver.DmDriver
url: jdbc:dm://172.16.17.236:5236?schema=SSP&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8
username: SUNYARD
# Jasypt加密 可到common-utils中找到JasyptUtil加解密工具类生成加密结果 格式为ENC(加密结果)
password: 123456
hikari:
minimum-idle: 5
maximum-pool-size: 100
idle-timeout: 600000 # 空闲连接的最大等待时间,单位为毫秒 (10 分钟)
max-lifetime: 1800000 # 连接池中连接的最大存活时间,单位为毫秒 (30 分钟)
connection-timeout: 30000 # 获取连接的超时时间,单位为毫秒 (30 秒)
leak-detection-threshold: 2000 # 连接泄漏检测阈值,单位为毫秒 (2 秒)
# 连接测试配置,确保连接有效性
connection-test-query: SELECT 1
validation-timeout: 5000 # 验证连接的超时时间,单位为毫秒 (5 秒)
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
mybatis-plus:
mapper-locations: classpath*:mapper/**/*Mapper.xml
# 原生配置
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
map-underscore-to-camel-case: true
cache-enabled: false
lazy-loading-enabled: false
global-config:
# 数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#驼峰下划线转换
table-underline: true
#是否开启大写命名,默认不开启
capital-mode: true
#逻辑删除配置
#logic-delete-value: 1
#logic-not-delete-value: 0
logging:
level:
root: info
com.sunyard.chsm.mapper: debug
# org.springframework.web: trace
# config: classpath:log4j2.xml

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--<include resource="org/springframework/boot/logging/logback/base.xml"/>-->
<!-- 定义log文件的目录 -->
<property name="LOG_HOME" value="logs"/>
<!-- 加载 Spring 配置文件信息 -->
<springProperty scope="context" name="applicationName" source="spring.application.name" defaultValue="localhost"/>
<!-- 日志输出格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %X{tl} [%thread] %-5level [%logger{0}:%L]- %msg%n%ex{15}"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--日志文件输出格式-->
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/%d{yyyy-MM}/INFO-%d{yyyy-MM-dd}_%i.log</fileNamePattern>
<!--单个文件-->
<maxFileSize>10MB</maxFileSize>
<!--文件保存时间(天)-->
<maxHistory>30</maxHistory>
<!--总文件日志最大的大小-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!--日志文件输出格式-->
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/%d{yyyy-MM}/sspweb-DEBUG-%d{yyyy-MM-dd}_%i.log</fileNamePattern>
<!--单个文件-->
<maxFileSize>10MB</maxFileSize>
<!--文件保存时间(天)-->
<maxHistory>30</maxHistory>
<!--总文件日志最大的大小-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!--日志文件输出格式-->
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/%d{yyyy-MM}/ERROR-%d{yyyy-MM-dd}_%i.log</fileNamePattern>
<!--单个文件-->
<maxFileSize>10MB</maxFileSize>
<!--文件保存时间(天)-->
<maxHistory>30</maxHistory>
<!--总文件日志最大的大小-->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<!--日志文件输出格式-->
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>