From 6a135421f547cf3e1ac24e30da6f98fc8058604d Mon Sep 17 00:00:00 2001 From: luke Date: Sat, 7 Jun 2025 00:27:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=C3=A5=E5=8A=A0=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=9A=84=C3=A7=C2=9A=E7=9A=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20+=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pom.xml | 21 + .../base/KnowledgeBaseApplication.java | 7 + .../domain/user/service/UserServiceImpl.java | 3 +- .../config/MyMetaObjectHandler.java | 22 + .../util/crypto/LocalCacheUtil.java | 103 +++ .../infrastructure/util/crypto/SM3Util.java | 45 ++ .../infrastructure/util/crypto/SM4Util.java | 182 ++++++ .../util/crypto/gmhelper/BCECUtil.java | 491 +++++++++++++++ .../util/crypto/gmhelper/GMBaseUtil.java | 11 + .../util/crypto/gmhelper/SM2Cipher.java | 55 ++ .../util/crypto/gmhelper/SM2Util.java | 592 ++++++++++++++++++ .../crypto/gmhelper/Sm2SignatureHelper.java | 98 +++ .../domain/user/service/UserServiceTest.java | 30 + .../resources/application-dev-mac.properties | 39 ++ .../application-dev-windows.properties | 39 ++ src/test/resources/application.properties | 26 + 17 files changed, 1764 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/knowledge/base/infrastructure/repository/config/MyMetaObjectHandler.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/LocalCacheUtil.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/SM3Util.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/SM4Util.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/BCECUtil.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/GMBaseUtil.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Cipher.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Util.java create mode 100644 src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/Sm2SignatureHelper.java create mode 100644 src/test/java/com/knowledge/base/domain/user/service/UserServiceTest.java create mode 100644 src/test/resources/application-dev-mac.properties create mode 100644 src/test/resources/application-dev-windows.properties create mode 100644 src/test/resources/application.properties diff --git a/.gitignore b/.gitignore index 00c25f2..76eefb1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ build/ ### Mac OS ### .DS_Store /knowledge-base-server-log/ +knowledge-base-server.iml diff --git a/pom.xml b/pom.xml index d272218..c8390e2 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,8 @@ 8.0.33 3.5.5 + + 1.70 @@ -195,6 +197,16 @@ ${mybatis-plus-boot-starter.version} + + org.bouncycastle + bcprov-jdk15to18 + ${org.bouncycastle.version} + + + org.bouncycastle + bcprov-jdk15to18 + ${org.bouncycastle.version} + @@ -327,6 +339,15 @@ mybatis-plus-boot-starter + + org.bouncycastle + bcprov-jdk15to18 + + + org.bouncycastle + bcprov-jdk15to18 + + diff --git a/src/main/java/com/knowledge/base/KnowledgeBaseApplication.java b/src/main/java/com/knowledge/base/KnowledgeBaseApplication.java index 46fc3b2..66c867c 100644 --- a/src/main/java/com/knowledge/base/KnowledgeBaseApplication.java +++ b/src/main/java/com/knowledge/base/KnowledgeBaseApplication.java @@ -1,9 +1,12 @@ package com.knowledge.base; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; +import java.security.Security; + /** * @author Luke.ye */ @@ -11,6 +14,10 @@ import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication public class KnowledgeBaseApplication { + static { + Security.addProvider(new BouncyCastleProvider()); + } + public static void main(String[] args) { SpringApplication.run(KnowledgeBaseApplication.class, args); } diff --git a/src/main/java/com/knowledge/base/domain/user/service/UserServiceImpl.java b/src/main/java/com/knowledge/base/domain/user/service/UserServiceImpl.java index 232af6f..0bcb3b2 100644 --- a/src/main/java/com/knowledge/base/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/knowledge/base/domain/user/service/UserServiceImpl.java @@ -5,6 +5,7 @@ import com.knowledge.base.domain.user.repository.iface.UserRepository; import com.knowledge.base.domain.user.repository.iface.UserTokenRepository; import com.knowledge.base.domain.user.repository.po.User; import com.knowledge.base.domain.user.repository.po.UserToken; +import com.knowledge.base.infrastructure.util.crypto.SM3Util; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -35,7 +36,7 @@ public class UserServiceImpl implements UserService { } User user = new User(); user.setUsername(username); - user.setPassword(new SM3().digestHex(plainPassword)); + user.setPassword(SM3Util.digest(plainPassword)); userRepository.save(user); return true; } diff --git a/src/main/java/com/knowledge/base/infrastructure/repository/config/MyMetaObjectHandler.java b/src/main/java/com/knowledge/base/infrastructure/repository/config/MyMetaObjectHandler.java new file mode 100644 index 0000000..9058372 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/repository/config/MyMetaObjectHandler.java @@ -0,0 +1,22 @@ +package com.knowledge.base.infrastructure.repository.config; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class MyMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "addTime", LocalDateTime.class, LocalDateTime.now()); + this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + } + + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/LocalCacheUtil.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/LocalCacheUtil.java new file mode 100644 index 0000000..70c759e --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/LocalCacheUtil.java @@ -0,0 +1,103 @@ +package com.knowledge.base.infrastructure.util.crypto; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * 使用 Guava 实现的本地缓存工具类 + * + * @author Luke.ye + * @date 2025/5/7 08:46 + */ +public class LocalCacheUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(LocalCacheUtil.class); + + // 默认缓存配置 + private static final int DEFAULT_MAX_CACHE_SIZE = 1000; // 最大缓存条目数 + private static final int DEFAULT_INITIAL_CAPACITY = 400; // 缓存初始容量 + private static final int DEFAULT_EXPIRATION_DAYS = 7; // 缓存过期时间(单位:天) + + private static Cache cache; + + static { + // 初始化缓存(使用默认配置) + initializeCache(DEFAULT_MAX_CACHE_SIZE, DEFAULT_INITIAL_CAPACITY, DEFAULT_EXPIRATION_DAYS); + } + + /** + * 初始化缓存。 + * + * @param maxSize 缓存的最大容量。 + * @param initialCapacity 缓存的初始容量。 + * @param expirationDays 缓存条目的过期时间(单位:天)。 + */ + private static void initializeCache(int maxSize, int initialCapacity, int expirationDays) { + cache = CacheBuilder.newBuilder() + .expireAfterAccess(expirationDays, TimeUnit.DAYS) // 设置访问过期时间 + .initialCapacity(initialCapacity) // 设置初始容量 + .maximumSize(maxSize) // 设置最大缓存条目数 + .build(); + } + + /** + * 根据 key 获取缓存中的值。 + * + * @param key 缓存的键。 + * @return 返回对应的值,如果键不存在则返回 null。 + */ + public static Object get(String key) { + return cache.getIfPresent(key); + } + + /** + * 向缓存中存储一个键值对。 + * 如果缓存容量接近上限,记录警告日志。 + * + * @param key 缓存的键。 + * @param value 缓存的值。 + */ + public static void put(String key, Object value) { + long size = cache.size(); + if (size >= cache.stats().loadCount() && size >= DEFAULT_MAX_CACHE_SIZE >> 1) { + LOGGER.warn("缓存接近最大容量 {},当前大小为 {},建议增加缓存容量。", DEFAULT_MAX_CACHE_SIZE, size); + } + cache.put(key, value); + } + + /** + * 根据 key 移除缓存中的条目。 + * + * @param key 要移除的键。 + */ + public static void remove(String key) { + cache.invalidate(key); + } + + /** + * 清空缓存中的所有条目。 + */ + public static void clearAll() { + cache.invalidateAll(); + } + + /** + * 查询缓存中的所有键值对。 + * + * @return 返回包含缓存内容的并发映射。 + */ + public static ConcurrentMap getAll() { + return cache.asMap(); + } + + /** + * 打印当前缓存统计信息(如命中率、加载次数等)。 + */ + public static void logCacheStats() { + LOGGER.info("缓存最大容量 {},当前大小为 {}, 缓存统计信息: {}", DEFAULT_MAX_CACHE_SIZE, cache.size(), cache.stats().toString()); + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM3Util.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM3Util.java new file mode 100644 index 0000000..7262926 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM3Util.java @@ -0,0 +1,45 @@ +package com.knowledge.base.infrastructure.util.crypto; + +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.digest.SM3; +import com.knowledge.base.infrastructure.util.crypto.gmhelper.GMBaseUtil; + +import java.util.Objects; + +/** + * SM3工具类 + * @author Luke.ye + * @date 2025/5/7 11:31 + */ +public class SM3Util extends GMBaseUtil { + + private static final SM3 sm3 = SmUtil.sm3(); + + /** + * 使用SM3算法生成字符串的摘要 + * + * @param data 要加密的输入数据 + * @return 加密后的十六进制字符串 + */ + public static String digest(String data) { + if (Objects.isNull(data)) { + throw new IllegalArgumentException("输入数据不能为空"); + } + return sm3.digestHex(data); + } + + /** + * 验证输入数据与哈希是否匹配。 + * + * @param data 原始数据 + * @param encryptedHash 用于比较的哈希值 + * @return 如果匹配返回true,否则返回false + */ + public static boolean verify(String data, String encryptedHash) { + if (Objects.isNull(data) || Objects.isNull(encryptedHash)) { + throw new IllegalArgumentException("数据和加密哈希值不能为空"); + } + return digest(data).equalsIgnoreCase(encryptedHash); + } +} + diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM4Util.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM4Util.java new file mode 100644 index 0000000..6fb2f9d --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/SM4Util.java @@ -0,0 +1,182 @@ +package com.knowledge.base.infrastructure.util.crypto; + +import com.knowledge.base.infrastructure.util.crypto.gmhelper.GMBaseUtil; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.engines.SM4Engine; +import org.bouncycastle.crypto.macs.CBCBlockCipherMac; +import org.bouncycastle.crypto.macs.GMac; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.paddings.BlockCipherPadding; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.*; + +public class SM4Util extends GMBaseUtil { + public static final String ALGORITHM_NAME = "SM4"; + public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding"; + public static final String ALGORITHM_NAME_ECB_NOPADDING = "SM4/ECB/NoPadding"; + public static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding"; + public static final String ALGORITHM_NAME_CBC_NOPADDING = "SM4/CBC/NoPadding"; + + /** + * SM4算法目前只支持128位(即密钥16字节) + */ + public static final int DEFAULT_KEY_SIZE = 128; + + public static byte[] generateKey() throws NoSuchAlgorithmException, NoSuchProviderException { + return generateKey(DEFAULT_KEY_SIZE); + } + + public static byte[] generateKey(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException { + KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME); + kg.init(keySize, new SecureRandom()); + return kg.generateKey().getEncoded(); + } + + public static byte[] encrypt_ECB_Padding(byte[] key, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data); + } + + public static byte[] decrypt_ECB_Padding(byte[] key, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_ECB_NoPadding(byte[] key, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data); + } + + public static byte[] decrypt_ECB_NoPadding(byte[] key, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException { + Cipher cipher = generateECBCipher(ALGORITHM_NAME_ECB_NOPADDING, Cipher.DECRYPT_MODE, key); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_CBC_Padding(byte[] key, byte[] iv, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, key, iv); + return cipher.doFinal(data); + } + + public static byte[] decrypt_CBC_Padding(byte[] key, byte[] iv, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(cipherText); + } + + public static byte[] encrypt_CBC_NoPadding(byte[] key, byte[] iv, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.ENCRYPT_MODE, key, iv); + return cipher.doFinal(data); + } + + public static byte[] decrypt_CBC_NoPadding(byte[] key, byte[] iv, byte[] cipherText) + throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Cipher cipher = generateCBCCipher(ALGORITHM_NAME_CBC_NOPADDING, Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(cipherText); + } + + public static byte[] doCMac(byte[] key, byte[] data) throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidKeyException { + Key keyObj = new SecretKeySpec(key, ALGORITHM_NAME); + return doMac("SM4-CMAC", keyObj, data); + } + + public static byte[] doGMac(byte[] key, byte[] iv, int tagLength, byte[] data) { + org.bouncycastle.crypto.Mac mac = new GMac(new GCMBlockCipher(new SM4Engine()), tagLength * 8); + return doMac(mac, key, iv, data); + } + + /** + * 默认使用PKCS7Padding/PKCS5Padding填充的CBCMAC + * + * @param key + * @param iv + * @param data + * @return + */ + public static byte[] doCBCMac(byte[] key, byte[] iv, byte[] data) { + SM4Engine engine = new SM4Engine(); + org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, new PKCS7Padding()); + return doMac(mac, key, iv, data); + } + + /** + * @param key + * @param iv + * @param padding 可以传null,传null表示NoPadding,由调用方保证数据必须是BlockSize的整数倍 + * @param data + * @return + * @throws Exception + */ + public static byte[] doCBCMac(byte[] key, byte[] iv, BlockCipherPadding padding, byte[] data) throws Exception { + SM4Engine engine = new SM4Engine(); + if (padding == null) { + if (data.length % engine.getBlockSize() != 0) { + throw new Exception("if no padding, data length must be multiple of SM4 BlockSize"); + } + } + org.bouncycastle.crypto.Mac mac = new CBCBlockCipherMac(engine, engine.getBlockSize() * 8, padding); + return doMac(mac, key, iv, data); + } + + + private static byte[] doMac(org.bouncycastle.crypto.Mac mac, byte[] key, byte[] iv, byte[] data) { + CipherParameters cipherParameters = new KeyParameter(key); + mac.init(new ParametersWithIV(cipherParameters, iv)); + mac.update(data, 0, data.length); + byte[] result = new byte[mac.getMacSize()]; + mac.doFinal(result, 0); + return result; + } + + private static byte[] doMac(String algorithmName, Key key, byte[] data) throws NoSuchProviderException, + NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); + mac.init(key); + mac.update(data); + return mac.doFinal(); + } + + private static Cipher generateECBCipher(String algorithmName, int mode, byte[] key) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, + InvalidKeyException { + Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME); + Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME); + cipher.init(mode, sm4Key); + return cipher; + } + + private 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); + Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + cipher.init(mode, sm4Key, ivParameterSpec); + return cipher; + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/BCECUtil.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/BCECUtil.java new file mode 100644 index 0000000..0c234f8 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/BCECUtil.java @@ -0,0 +1,491 @@ +package com.knowledge.base.infrastructure.util.crypto.gmhelper; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +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.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.generators.ECKeyPairGenerator; +import org.bouncycastle.crypto.params.*; +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.pqc.math.linearalgebra.ByteUtils; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; + +import java.io.*; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * 这个工具类的方法,也适用于其他基于BC库的ECC算法 + */ +public class BCECUtil { + 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"; + + /** + * 生成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; + } + + /** + * @param dHex 十六进制字符串形式的私钥d值,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + String dHex, ECDomainParameters domainParameters) { + return createECPrivateKeyParameters(ByteUtils.fromHexString(dHex), domainParameters); + } + + /** + * @param dBytes 字节数组形式的私钥d值,如果是SM2算法,应该是32字节 + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + byte[] dBytes, ECDomainParameters domainParameters) { + return createECPrivateKeyParameters(new BigInteger(1, dBytes), domainParameters); + } + + /** + * @param d 大数形式的私钥d值 + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#DOMAIN_PARAMS} + * @return + */ + public static ECPrivateKeyParameters createECPrivateKeyParameters( + BigInteger d, ECDomainParameters domainParameters) { + return new ECPrivateKeyParameters(d, domainParameters); + } + + /** + * 根据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); + } + + /** + * @param x 大数形式的公钥x分量 + * @param y 大数形式的公钥y分量 + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + BigInteger x, BigInteger y, ECCurve curve, ECDomainParameters domainParameters) { + return createECPublicKeyParameters(x.toByteArray(), y.toByteArray(), curve, domainParameters); + } + + /** + * @param xHex 十六进制形式的公钥x分量,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param yHex 十六进制形式的公钥y分量,如果是SM2算法,Hex字符串长度应该是64(即32字节) + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#DOMAIN_PARAMS} + * @return + */ + public static ECPublicKeyParameters createECPublicKeyParameters( + String xHex, String yHex, ECCurve curve, ECDomainParameters domainParameters) { + return createECPublicKeyParameters(ByteUtils.fromHexString(xHex), ByteUtils.fromHexString(yHex), + curve, domainParameters); + } + + /** + * @param xBytes 十六进制形式的公钥x分量,如果是SM2算法,应该是32字节 + * @param yBytes 十六进制形式的公钥y分量,如果是SM2算法,应该是32字节 + * @param curve EC曲线参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#CURVE} + * @param domainParameters EC Domain参数,一般是固定的,如果是SM2算法的可参考{@link SM2Util#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 BCECUtil.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; + } + + /** + * 将SEC1标准的私钥字节流恢复为PKCS8标准的字节流 + * + * @param sec1Key + * @return + * @throws IOException + */ + public static byte[] convertECPrivateKeySEC1ToPKCS8(byte[] sec1Key) throws IOException { + /** + * 参考org.bouncycastle.asn1.pkcs.PrivateKeyInfo和 + * org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey,逆向拼装 + */ + X962Parameters params = getDomainParametersFromName(SM2Util.JDK_EC_SPEC, false); + ASN1OctetString privKey = new DEROctetString(sec1Key); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(0)); //版本号 + v.add(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params)); //算法标识 + v.add(privKey); + DERSequence ds = new DERSequence(v); + return ds.getEncoded(ASN1Encoding.DER); + } + + /** + * 将SEC1标准的私钥字节流转为BCECPrivateKey对象 + * + * @param sec1Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + * @throws IOException + */ + public static BCECPrivateKey convertSEC1ToBCECPrivateKey(byte[] sec1Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException { + PKCS8EncodedKeySpec peks = new PKCS8EncodedKeySpec(convertECPrivateKeySEC1ToPKCS8(sec1Key)); + KeyFactory kf = KeyFactory.getInstance(ALGO_NAME_EC, BouncyCastleProvider.PROVIDER_NAME); + return (BCECPrivateKey) kf.generatePrivate(peks); + } + + /** + * 将SEC1标准的私钥字节流转为ECPrivateKeyParameters对象 + * openssl i2d_ECPrivateKey函数生成的DER编码的ecc私钥是:SEC1标准的、带有EC_GROUP、带有公钥的, + * 这个工具函数的主要目的就是为了使Java程序能够“识别”openssl生成的ECC私钥 + * + * @param sec1Key + * @return + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @throws InvalidKeySpecException + */ + public static ECPrivateKeyParameters convertSEC1ToECPrivateKey(byte[] sec1Key) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, IOException { + BCECPrivateKey privateKey = convertSEC1ToBCECPrivateKey(sec1Key); + return convertPrivateKeyToParameters(privateKey); + } + + /** + * 将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/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/GMBaseUtil.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/GMBaseUtil.java new file mode 100644 index 0000000..ba2f789 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/GMBaseUtil.java @@ -0,0 +1,11 @@ +package com.knowledge.base.infrastructure.util.crypto.gmhelper; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; + +public class GMBaseUtil { + static { + Security.addProvider(new BouncyCastleProvider()); + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Cipher.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Cipher.java new file mode 100644 index 0000000..7b26352 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Cipher.java @@ -0,0 +1,55 @@ +package com.knowledge.base.infrastructure.util.crypto.gmhelper; + +public class SM2Cipher { + /** + * ECC密钥 + */ + private byte[] c1; + + /** + * 真正的密文 + */ + private byte[] c2; + + /** + * 对(c1+c2)的SM3-HASH值 + */ + private byte[] c3; + + /** + * SM2标准的密文,即(c1+c2+c3) + */ + private byte[] cipherText; + + public byte[] getC1() { + return c1; + } + + public void setC1(byte[] c1) { + this.c1 = c1; + } + + public byte[] getC2() { + return c2; + } + + public void setC2(byte[] c2) { + this.c2 = c2; + } + + public byte[] getC3() { + return c3; + } + + public void setC3(byte[] c3) { + this.c3 = c3; + } + + public byte[] getCipherText() { + return cipherText; + } + + public void setCipherText(byte[] cipherText) { + this.cipherText = cipherText; + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Util.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Util.java new file mode 100644 index 0000000..1f6b5ca --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/SM2Util.java @@ -0,0 +1,592 @@ +package com.knowledge.base.infrastructure.util.crypto.gmhelper; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.engines.SM2Engine.Mode; +import org.bouncycastle.crypto.params.*; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.custom.gm.SM2P256V1Curve; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.ECFieldFp; +import java.security.spec.EllipticCurve; + +public class SM2Util extends GMBaseUtil { + ////////////////////////////////////////////////////////////////////////////////////// + /* + * 以下为SM2推荐曲线参数 + */ + 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); + public static final int CURVE_LEN = BCECUtil.getCurveLength(DOMAIN_PARAMS); + ////////////////////////////////////////////////////////////////////////////////////// + + public static final EllipticCurve JDK_CURVE = new EllipticCurve(new ECFieldFp(SM2_ECC_P), SM2_ECC_A, SM2_ECC_B); + public static final java.security.spec.ECPoint JDK_G_POINT = new java.security.spec.ECPoint( + G_POINT.getAffineXCoord().toBigInteger(), G_POINT.getAffineYCoord().toBigInteger()); + public static final java.security.spec.ECParameterSpec JDK_EC_SPEC = new java.security.spec.ECParameterSpec( + JDK_CURVE, JDK_G_POINT, SM2_ECC_N, SM2_ECC_H.intValue()); + + ////////////////////////////////////////////////////////////////////////////////////// + + public static final int SM3_DIGEST_LENGTH = 32; + + /** + * 生成ECC密钥对 + * + * @return ECC密钥对 + */ + public static AsymmetricCipherKeyPair generateKeyPairParameter() { + SecureRandom random = new SecureRandom(); + return BCECUtil.generateKeyPairParameter(DOMAIN_PARAMS, random); + } + + /** + * 生成ECC密钥对 + * + * @return + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws InvalidAlgorithmParameterException + */ + public static KeyPair generateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + SecureRandom random = new SecureRandom(); + return BCECUtil.generateKeyPair(DOMAIN_PARAMS, random); + } + + /** + * 只获取私钥里的d值,32字节 + * + * @param privateKey + * @return + */ + public static byte[] getRawPrivateKey(BCECPrivateKey privateKey) { + return fixToCurveLengthBytes(privateKey.getD().toByteArray()); + } + + /** + * 只获取公钥里的XY分量,64字节 + * + * @param publicKey + * @return 64字节数组 + */ + public static byte[] getRawPublicKey(BCECPublicKey publicKey) { + byte[] src65 = publicKey.getQ().getEncoded(false); + byte[] rawXY = new byte[CURVE_LEN * 2];//SM2的话这里应该是64字节 + System.arraycopy(src65, 1, rawXY, 0, rawXY.length); + return rawXY; + } + + /** + * @param pubKey 公钥 + * @param srcData 原文 + * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException { + ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey); + return encrypt(Mode.C1C3C2, pubKeyParameters, srcData); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param pubKey 公钥 + * @param srcData 原文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(Mode mode, BCECPublicKey pubKey, byte[] srcData) throws InvalidCipherTextException { + ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey); + return encrypt(mode, pubKeyParameters, srcData); + } + + /** + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @return 默认输出C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(ECPublicKeyParameters pubKeyParameters, byte[] srcData) + throws InvalidCipherTextException { + return encrypt(Mode.C1C3C2, pubKeyParameters, srcData); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws InvalidCipherTextException + */ + public static byte[] encrypt(Mode mode, ECPublicKeyParameters pubKeyParameters, byte[] srcData) + throws InvalidCipherTextException { + SM2Engine engine = new SM2Engine(mode); + ParametersWithRandom pwr = new ParametersWithRandom(pubKeyParameters, new SecureRandom()); + engine.init(true, pwr); + return engine.processBlock(srcData, 0, srcData.length); + } + + /** + * @param priKey 私钥 + * @param sm2Cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException { + ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey); + return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param priKey 私钥 + * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(Mode mode, BCECPrivateKey priKey, byte[] sm2Cipher) throws InvalidCipherTextException { + ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey); + return decrypt(mode, priKeyParameters, sm2Cipher); + } + + /** + * @param priKeyParameters 私钥 + * @param sm2Cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher) + throws InvalidCipherTextException { + return decrypt(Mode.C1C3C2, priKeyParameters, sm2Cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param priKeyParameters 私钥 + * @param sm2Cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 原文。SM2解密返回了数据则一定是原文,因为SM2自带校验,如果密文被篡改或者密钥对不上,都是会直接报异常的。 + * @throws InvalidCipherTextException + */ + public static byte[] decrypt(Mode mode, ECPrivateKeyParameters priKeyParameters, byte[] sm2Cipher) + throws InvalidCipherTextException { + SM2Engine engine = new SM2Engine(mode); + engine.init(false, priKeyParameters); + return engine.processBlock(sm2Cipher, 0, sm2Cipher.length); + } + + /** + * 分解SM2密文 + * + * @param cipherText 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + * @throws Exception + */ + public static SM2Cipher parseSM2Cipher(byte[] cipherText) throws Exception { + int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS); + return parseSM2Cipher(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipherText); + } + + /** + * 分解SM2密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param cipherText 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + */ + public static SM2Cipher parseSM2Cipher(Mode mode, byte[] cipherText) throws Exception { + int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS); + return parseSM2Cipher(mode, curveLength, SM3_DIGEST_LENGTH, cipherText); + } + + /** + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipherText 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + * @throws Exception + */ + public static SM2Cipher parseSM2Cipher( + int curveLength, int digestLength, byte[] cipherText) throws Exception { + return parseSM2Cipher(Mode.C1C3C2, curveLength, digestLength, cipherText); + } + + /** + * 分解SM2密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipherText 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return + */ + public static SM2Cipher parseSM2Cipher(Mode mode, int curveLength, int digestLength, + byte[] cipherText) throws Exception { + byte[] c1 = new byte[curveLength * 2 + 1]; + byte[] c2 = new byte[cipherText.length - c1.length - digestLength]; + byte[] c3 = new byte[digestLength]; + + System.arraycopy(cipherText, 0, c1, 0, c1.length); + if (mode == Mode.C1C2C3) { + System.arraycopy(cipherText, c1.length, c2, 0, c2.length); + System.arraycopy(cipherText, c1.length + c2.length, c3, 0, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(cipherText, c1.length, c3, 0, c3.length); + System.arraycopy(cipherText, c1.length + c3.length, c2, 0, c2.length); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + SM2Cipher result = new SM2Cipher(); + result.setC1(c1); + result.setC2(c2); + result.setC3(c3); + result.setCipherText(cipherText); + return result; + } + + /** + * DER编码密文 + * + * @param cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return DER编码后的密文 + * @throws IOException + */ + public static byte[] encodeSM2CipherToDER(byte[] cipher) throws Exception { + int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS); + return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, SM3_DIGEST_LENGTH, cipher); + } + + /** + * DER编码密文 + * + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 按指定mode DER编码后的密文 + * @throws Exception + */ + public static byte[] encodeSM2CipherToDER(Mode mode, byte[] cipher) throws Exception { + int curveLength = BCECUtil.getCurveLength(DOMAIN_PARAMS); + return encodeSM2CipherToDER(mode, curveLength, SM3_DIGEST_LENGTH, cipher); + } + + /** + * DER编码密文 + * + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipher 默认输入C1C3C2顺序的密文。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 默认输出按C1C3C2编码的结果 + * @throws IOException + */ + public static byte[] encodeSM2CipherToDER(int curveLength, int digestLength, byte[] cipher) + throws Exception { + return encodeSM2CipherToDER(Mode.C1C3C2, curveLength, digestLength, cipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param curveLength 曲线长度,SM2的话就是256位。 + * @param digestLength 摘要长度,如果是SM2的话因为默认使用SM3摘要,SM3摘要长度为32字节。 + * @param cipher 根据mode不同,需要输入的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @return 按指定mode DER编码后的密文 + * @throws Exception + */ + public static byte[] encodeSM2CipherToDER(Mode mode, int curveLength, int digestLength, byte[] cipher) + throws Exception { + + byte[] c1x = new byte[curveLength]; + byte[] c1y = new byte[curveLength]; + byte[] c2 = new byte[cipher.length - c1x.length - c1y.length - 1 - digestLength]; + byte[] c3 = new byte[digestLength]; + + int startPos = 1; + System.arraycopy(cipher, startPos, c1x, 0, c1x.length); + startPos += c1x.length; + System.arraycopy(cipher, startPos, c1y, 0, c1y.length); + startPos += c1y.length; + if (mode == Mode.C1C2C3) { + System.arraycopy(cipher, startPos, c2, 0, c2.length); + startPos += c2.length; + System.arraycopy(cipher, startPos, c3, 0, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(cipher, startPos, c3, 0, c3.length); + startPos += c3.length; + System.arraycopy(cipher, startPos, c2, 0, c2.length); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + ASN1Encodable[] arr = new ASN1Encodable[4]; + // c1x,c1y的第一个bit可能为1,这个时候要确保他们表示的大数一定是正数,所以new BigInteger符号强制设为正。 + arr[0] = new ASN1Integer(new BigInteger(1, c1x)); + arr[1] = new ASN1Integer(new BigInteger(1, c1y)); + if (mode == Mode.C1C2C3) { + arr[2] = new DEROctetString(c2); + arr[3] = new DEROctetString(c3); + } else if (mode == Mode.C1C3C2) { + arr[2] = new DEROctetString(c3); + arr[3] = new DEROctetString(c2); + } + DERSequence ds = new DERSequence(arr); + return ds.getEncoded(ASN1Encoding.DER); + } + + /** + * 解码DER密文 + * + * @param derCipher 默认输入按C1C3C2顺序DER编码的密文 + * @return 输出按C1C3C2排列的字节数组,C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + */ + public static byte[] decodeDERSM2Cipher(byte[] derCipher) throws Exception { + return decodeDERSM2Cipher(Mode.C1C3C2, derCipher); + } + + /** + * @param mode 指定密文结构,旧标准的为C1C2C3,新的[《SM2密码算法使用规范》 GM/T 0009-2012]标准为C1C3C2 + * @param derCipher 根据mode输入C1C2C3或C1C3C2顺序DER编码后的密文 + * @return 根据mode不同,输出的密文C1C2C3排列顺序不同。C1为65字节第1字节为压缩标识,这里固定为0x04,后面64字节为xy分量各32字节。C3为32字节。C2长度与原文一致。 + * @throws Exception + */ + public static byte[] decodeDERSM2Cipher(Mode mode, byte[] derCipher) throws Exception { + ASN1Sequence as = DERSequence.getInstance(derCipher); + byte[] c1x = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] c1y = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + // c1x,c1y可能因为大正数的补0规则在第一个有效字节前面插了一个(byte)0,变成33个字节,在这里要修正回32个字节去 + c1x = fixToCurveLengthBytes(c1x); + c1y = fixToCurveLengthBytes(c1y); + byte[] c3; + byte[] c2; + if (mode == Mode.C1C2C3) { + c2 = ((DEROctetString) as.getObjectAt(2)).getOctets(); + c3 = ((DEROctetString) as.getObjectAt(3)).getOctets(); + } else if (mode == Mode.C1C3C2) { + c3 = ((DEROctetString) as.getObjectAt(2)).getOctets(); + c2 = ((DEROctetString) as.getObjectAt(3)).getOctets(); + } else { + throw new Exception("Unsupported mode:" + mode); + } + + int pos = 0; + byte[] cipherText = new byte[1 + c1x.length + c1y.length + c2.length + c3.length]; + final byte uncompressedFlag = 0x04; + cipherText[0] = uncompressedFlag; + pos += 1; + System.arraycopy(c1x, 0, cipherText, pos, c1x.length); + pos += c1x.length; + System.arraycopy(c1y, 0, cipherText, pos, c1y.length); + pos += c1y.length; + if (mode == Mode.C1C2C3) { + System.arraycopy(c2, 0, cipherText, pos, c2.length); + pos += c2.length; + System.arraycopy(c3, 0, cipherText, pos, c3.length); + } else if (mode == Mode.C1C3C2) { + System.arraycopy(c3, 0, cipherText, pos, c3.length); + pos += c3.length; + System.arraycopy(c2, 0, cipherText, pos, c2.length); + } + return cipherText; + } + + /** + * 签名 + * + * @param priKey 私钥 + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(BCECPrivateKey priKey, byte[] srcData) throws CryptoException { + ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey); + return sign(priKeyParameters, null, srcData); + } + + /** + * 签名 + * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes() + * + * @param priKeyParameters 私钥 + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] srcData) throws CryptoException { + return sign(priKeyParameters, null, srcData); + } + + /** + * 私钥签名 + * + * @param priKey 私钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(BCECPrivateKey priKey, byte[] withId, byte[] srcData) throws CryptoException { + ECPrivateKeyParameters priKeyParameters = BCECUtil.convertPrivateKeyToParameters(priKey); + return sign(priKeyParameters, withId, srcData); + } + + /** + * 签名 + * + * @param priKeyParameters 私钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 源数据 + * @return DER编码后的签名值 + * @throws CryptoException + */ + public static byte[] sign(ECPrivateKeyParameters priKeyParameters, byte[] withId, byte[] srcData) + throws CryptoException { + SM2Signer signer = new SM2Signer(); + CipherParameters param = null; + ParametersWithRandom pwr = new ParametersWithRandom(priKeyParameters, new SecureRandom()); + if (withId != null) { + param = new ParametersWithID(pwr, withId); + } else { + param = pwr; + } + signer.init(true, param); + signer.update(srcData, 0, srcData.length); + return signer.generateSignature(); + } + + /** + * 将DER编码的SM2签名解码成64字节的纯R+S字节流 + * + * @param derSign + * @return 64字节数组,前32字节为R,后32字节为S + */ + public static byte[] decodeDERSM2Sign(byte[] derSign) { + ASN1Sequence as = DERSequence.getInstance(derSign); + byte[] rBytes = ((ASN1Integer) as.getObjectAt(0)).getValue().toByteArray(); + byte[] sBytes = ((ASN1Integer) as.getObjectAt(1)).getValue().toByteArray(); + //由于大数的补0规则,所以可能会出现33个字节的情况,要修正回32个字节 + rBytes = fixToCurveLengthBytes(rBytes); + sBytes = fixToCurveLengthBytes(sBytes); + byte[] rawSign = new byte[rBytes.length + sBytes.length]; + System.arraycopy(rBytes, 0, rawSign, 0, rBytes.length); + System.arraycopy(sBytes, 0, rawSign, rBytes.length, sBytes.length); + return rawSign; + } + + /** + * 把64字节的纯R+S字节数组编码成DER编码 + * + * @param rawSign 64字节数组形式的SM2签名值,前32字节为R,后32字节为S + * @return DER编码后的SM2签名值 + * @throws IOException + */ + public static byte[] encodeSM2SignToDER(byte[] rawSign) throws IOException { + //要保证大数是正数 + BigInteger r = new BigInteger(1, extractBytes(rawSign, 0, 32)); + BigInteger s = new BigInteger(1, extractBytes(rawSign, 32, 32)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + return new DERSequence(v).getEncoded(ASN1Encoding.DER); + } + + /** + * 验签 + * + * @param pubKey 公钥 + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return + */ + public static boolean verify(BCECPublicKey pubKey, byte[] srcData, byte[] sign) { + ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey); + return verify(pubKeyParameters, null, srcData, sign); + } + + /** + * 验签 + * 不指定withId,则默认withId为字节数组:"1234567812345678".getBytes() + * + * @param pubKeyParameters 公钥 + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return 验签成功返回true,失败返回false + */ + public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] srcData, byte[] sign) { + return verify(pubKeyParameters, null, srcData, sign); + } + + /** + * 验签 + * + * @param pubKey 公钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return + */ + public static boolean verify(BCECPublicKey pubKey, byte[] withId, byte[] srcData, byte[] sign) { + ECPublicKeyParameters pubKeyParameters = BCECUtil.convertPublicKeyToParameters(pubKey); + return verify(pubKeyParameters, withId, srcData, sign); + } + + /** + * 验签 + * + * @param pubKeyParameters 公钥 + * @param withId 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @param srcData 原文 + * @param sign DER编码的签名值 + * @return 验签成功返回true,失败返回false + */ + public static boolean verify(ECPublicKeyParameters pubKeyParameters, byte[] withId, byte[] srcData, byte[] sign) { + SM2Signer signer = new SM2Signer(); + CipherParameters param; + if (withId != null) { + param = new ParametersWithID(pubKeyParameters, withId); + } else { + param = pubKeyParameters; + } + signer.init(false, param); + signer.update(srcData, 0, srcData.length); + return signer.verifySignature(sign); + } + + private static byte[] extractBytes(byte[] src, int offset, int length) { + byte[] result = new byte[length]; + System.arraycopy(src, offset, result, 0, result.length); + return result; + } + + private static byte[] fixToCurveLengthBytes(byte[] src) { + if (src.length == CURVE_LEN) { + return src; + } + + byte[] result = new byte[CURVE_LEN]; + if (src.length > CURVE_LEN) { + 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; + } +} diff --git a/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/Sm2SignatureHelper.java b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/Sm2SignatureHelper.java new file mode 100644 index 0000000..cbac104 --- /dev/null +++ b/src/main/java/com/knowledge/base/infrastructure/util/crypto/gmhelper/Sm2SignatureHelper.java @@ -0,0 +1,98 @@ +package com.knowledge.base.infrastructure.util.crypto.gmhelper; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.crypto.util.PublicKeyFactory; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; + +/** + * + * + * + * + * @author Luke.ye + * @date 2025/5/24 00:27 + */ +public class Sm2SignatureHelper { + + private static final String PROVIDER = "BC"; + + /** + * 标准 ASN.1 格式签名 + */ + public static byte[] signAsASN1(PrivateKey privKey, byte[] content, String userId) throws Exception { + SM2Signer signer = new SM2Signer(); + CipherParameters param = new ParametersWithID(PrivateKeyFactory.createKey(privKey.getEncoded()), + userId.getBytes(StandardCharsets.UTF_8)); + signer.init(true, param); + signer.update(content, 0, content.length); + return signer.generateSignature(); // DER 编码的 R + S + } + + public static boolean verifyAsASN1(PublicKey pubKey, byte[] content, byte[] signature, String userId) throws Exception { + SM2Signer signer = new SM2Signer(); + CipherParameters param = new ParametersWithID(PublicKeyFactory.createKey(pubKey.getEncoded()), + userId.getBytes(StandardCharsets.UTF_8)); + signer.init(false, param); + signer.update(content, 0, content.length); + return signer.verifySignature(signature); + } + + /** + * R||S 拼接格式签名(64 字节,CFCA 等机构常用) + */ + public static byte[] signAsRS(PrivateKey privKey, byte[] content, String userId) throws Exception { + byte[] der = signAsASN1(privKey, content, userId); + ASN1Sequence seq = (ASN1Sequence) ASN1Primitive.fromByteArray(der); + BigInteger r = ((ASN1Integer) seq.getObjectAt(0)).getValue(); + BigInteger s = ((ASN1Integer) seq.getObjectAt(1)).getValue(); + return ByteUtils.join(32, r.toByteArray(), s.toByteArray()); // 拼接为 R || S + } + + public static boolean verifyAsRS(PublicKey pubKey, byte[] content, byte[] rsSig, String userId) throws Exception { + if (rsSig.length != 64) { + throw new IllegalArgumentException("RS签名应为64字节,实际:" + rsSig.length); + } + byte[] rBytes = Arrays.copyOfRange(rsSig, 0, 32); + byte[] sBytes = Arrays.copyOfRange(rsSig, 32, 64); + BigInteger r = new BigInteger(1, rBytes); + BigInteger s = new BigInteger(1, sBytes); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + byte[] der = new DERSequence(v).getEncoded(); + return verifyAsASN1(pubKey, content, der, userId); + } + + /** + * 将 BigInteger 按 32 字节对齐拼接 + */ + static class ByteUtils { + public static byte[] join(int length, byte[] r, byte[] s) { + byte[] result = new byte[length * 2]; + System.arraycopy(alignToLength(r, length), 0, result, 0, length); + System.arraycopy(alignToLength(s, length), 0, result, length, length); + return result; + } + + private static byte[] alignToLength(byte[] input, int length) { + if (input.length == length) return input; + byte[] padded = new byte[length]; + if (input.length > length) { + System.arraycopy(input, input.length - length, padded, 0, length); // 截断右边 + } else { + System.arraycopy(input, 0, padded, length - input.length, input.length); // 左补0 + } + return padded; + } + } +} + diff --git a/src/test/java/com/knowledge/base/domain/user/service/UserServiceTest.java b/src/test/java/com/knowledge/base/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..dc83af0 --- /dev/null +++ b/src/test/java/com/knowledge/base/domain/user/service/UserServiceTest.java @@ -0,0 +1,30 @@ +package com.knowledge.base.domain.user.service; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +@SpringBootTest(properties = { + "spring.profiles.active=dev-windows" +}) +@Slf4j +public class UserServiceTest { + + private static final List USER_NAMES = Lists.newArrayList( + "Luke.Ye", "WangRui", "HuangHuang", "LiFang", "QiHui"); + + @Autowired + private UserService userService; + + @Test + public void testBatchRegisterUsers() { + USER_NAMES.forEach(username -> { + boolean success = userService.registerUser(username, username); + log.info("注册用户 [{}] -> {}", username, success ? "成功" : "已存在"); + }); + } +} diff --git a/src/test/resources/application-dev-mac.properties b/src/test/resources/application-dev-mac.properties new file mode 100644 index 0000000..cec4bfe --- /dev/null +++ b/src/test/resources/application-dev-mac.properties @@ -0,0 +1,39 @@ +# 日志 +logging.config=classpath:log/log4j.xml + +# nacos相关 +## nacos公用 +spring.cloud.nacos.config.data-id-without-suffix=nacos +spring.cloud.nacos.config.file-extension=properties +spring.config.import=nacos:${spring.cloud.nacos.config.data-id-without-suffix}.${spring.cloud.nacos.config.file-extension}?refresh=true +## Nacos配置中心 +spring.cloud.nacos.config.username=nacos +spring.cloud.nacos.config.password=lukeye +spring.cloud.nacos.config.contextPath=/nacos +spring.cloud.nacos.config.server-addr=http://101.132.255.39:8848 +spring.cloud.nacos.config.namespace=lukeye +spring.cloud.nacos.config.refreshEnabled=true + +# Arthas配置 +arthas.telnetPort=-1 +arthas.httpPort=-1 +arthas.ip=127.0.0.1 +arthas.appName=${spring.application.name} +arthas.tunnel-server=ws://101.132.255.39:7777/ws + +# 导入的材料路径 +exclude.file.path.prefix=/Users/admin/Desktop/ahnx-share-src-public/public +markdown.path=/Users/admin/Desktop/ahnx-share-src-public/public +pdf.path=/Users/admin/Desktop/ahnx-share-src-public/public +word.path=/Users/admin/Desktop/ahnx-share-src-public/public +excel.path=/Users/admin/Desktop/ahnx-share-src-public/public + +# mysql +spring.datasource.url=jdbc:mysql://localhost:3306/kbase?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai +spring.datasource.username=root +spring.datasource.password=lukeye@6 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# mybatis-plus +mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +mybatis-plus.global-config.db-config.logic-delete-field=deleted +mybatis-plus.global-config.db-config.id-type=auto diff --git a/src/test/resources/application-dev-windows.properties b/src/test/resources/application-dev-windows.properties new file mode 100644 index 0000000..d095c50 --- /dev/null +++ b/src/test/resources/application-dev-windows.properties @@ -0,0 +1,39 @@ +# 日志 +logging.config=classpath:log/log4j.xml + +# nacos相关 +## nacos公用 +spring.cloud.nacos.config.data-id-without-suffix=nacos +spring.cloud.nacos.config.file-extension=properties +spring.config.import=nacos:${spring.cloud.nacos.config.data-id-without-suffix}.${spring.cloud.nacos.config.file-extension}?refresh=true +## Nacos配置中心 +spring.cloud.nacos.config.username=nacos +spring.cloud.nacos.config.password=lukeye +spring.cloud.nacos.config.contextPath=/nacos +spring.cloud.nacos.config.server-addr=http://127.0.0.1:8848 +spring.cloud.nacos.config.namespace=lukeye +spring.cloud.nacos.config.refreshEnabled=true + +# Arthas配置 +arthas.telnetPort=-1 +arthas.httpPort=-1 +arthas.ip=127.0.0.1 +arthas.appName=${spring.application.name} +arthas.tunnel-server=ws://127.0.0.1:7777/ws + +# 导入的材料路径 +exclude.file.path.prefix=D:/02-documents/01-ahnx-share-src-public/public +markdown.path=D:/02-documents/01-ahnx-share-src-public/public +pdf.path=D:/02-documents/01-ahnx-share-src-public/public +word.path=D:/02-documents/01-ahnx-share-src-public/public +excel.path=D:/02-documents/01-ahnx-share-src-public/public + +# mysql +spring.datasource.url=jdbc:mysql://localhost:3306/kbase?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai +spring.datasource.username=root +spring.datasource.password=lukeye +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# mybatis-plus +mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +mybatis-plus.global-config.db-config.logic-delete-field=deleted +mybatis-plus.global-config.db-config.id-type=auto \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..8caa3b9 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,26 @@ + +spring.application.name=doc-parser-server +application.author=Luke.Ye +server.port=18080 + +# 日志相关 +logging.config=classpath:log/log4j.xml +log4j2.enable.threadlocals=true +logging.level.com.doc.parser=info +logging.level.root=${logging.level.com.doc.parser} +logging.org.springframework.web=${logging.level.com.doc.parser} +logging.org.springframework.context=${logging.level.com.doc.parser} + +# ES相关 +elasticsearch.host=es.wisdompulse.cn +elasticsearch.port=80 +elasticsearch.scheme=http +search.default-page-size=100 + +# 是否打开定时导入,且指定导入的频率 +import.schedule.enabled=true +# 每小时执行一次(可改) +import.schedule.cron=0 0 * * * * + + +