添å加安全相关的ç的代码 + fix
This commit is contained in:
parent
f91475281c
commit
6a135421f5
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,3 +36,4 @@ build/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/knowledge-base-server-log/
|
||||
knowledge-base-server.iml
|
||||
|
||||
21
pom.xml
21
pom.xml
@ -42,6 +42,8 @@
|
||||
|
||||
<mysql-connector-java.version>8.0.33</mysql-connector-java.version>
|
||||
<mybatis-plus-boot-starter.version>3.5.5</mybatis-plus-boot-starter.version>
|
||||
|
||||
<org.bouncycastle.version>1.70</org.bouncycastle.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@ -195,6 +197,16 @@
|
||||
<version>${mybatis-plus-boot-starter.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<version>${org.bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<version>${org.bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
@ -327,6 +339,15 @@
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> 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<String, Object> getAll() {
|
||||
return cache.asMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印当前缓存统计信息(如命中率、加载次数等)。
|
||||
*/
|
||||
public static void logCacheStats() {
|
||||
LOGGER.info("缓存最大容量 {},当前大小为 {}, 缓存统计信息: {}", DEFAULT_MAX_CACHE_SIZE, cache.size(), cache.stats().toString());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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 ? "成功" : "已存在");
|
||||
});
|
||||
}
|
||||
}
|
||||
39
src/test/resources/application-dev-mac.properties
Normal file
39
src/test/resources/application-dev-mac.properties
Normal file
@ -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
|
||||
39
src/test/resources/application-dev-windows.properties
Normal file
39
src/test/resources/application-dev-windows.properties
Normal file
@ -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
|
||||
26
src/test/resources/application.properties
Normal file
26
src/test/resources/application.properties
Normal file
@ -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 * * * *
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user