优雅的处理RSA以及SM2加密解密签名验签

说明

抛开hutool这个不算太好用的工具,自己翻阅了大量资料,借鉴网上的例子,优雅的实现了一个rsa以及sm2的加密处理类,这也是从json script rule 中摘选出来的代码,放弃锁处理后性能比hutool加锁的方式快两倍多。此外虽然采用了内部类包裹参数提高了效率,但同时一种加密算法也只能支持一次调用init方法,不适合做通用的工具类,仅适合调用该工具类的所有地方都使用同一套加密参数的场景,参数如密钥、AES的iv等等,也就是说如果两个调用的地方采用不同的密钥进行初始化,则会相互影响,代码如下

package edi.rule.util;

import edi.rule.config.JSRuleContext;
import edi.rule.work.custom.JSRuleException;
import edi.rule.work.enums.JSRuleSecurityEnum;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.function.BiFunction;
import java.util.function.Consumer;

/**
 * @author 摩拉克斯
 * @date 2022年3月17日 下午4:41:56
 */
@Slf4j
public class ZSSecurity {

    private static final String provider = "BC";

    static {
        Security.addProvider(new BouncyCastleProvider());
    }
    /**
     * <p>判断当前系统是否是安全模式,true是,false不是
     * @param type 待判断的安全模式
     * */
    public static boolean isSecurityMode(JSRuleSecurityEnum type){
        return JSRuleSecurityEnum.NONE != type;
    }
    /**
     * <p>判断当前系统是否是安全模式,true是,false不是
     * */
    public static boolean isSecurityMode(){
        return isSecurityMode(JSRuleContext.getSecurity());
    }
    /**
     * <p>获取签名原文,后台配置的秘钥与文本进行组合
     * @param plainText 文本
     * @return 返回组合后的签名文本
     * */
    public static String joinSignKey(String plainText){
        if (ZSString.isBlank(JSRuleContext.getProperties().security.signKey)){
            return plainText;
        }
        return plainText+JSRuleContext.getProperties().security.signKey;
    }
    /*--------------------------------AES--------------------------------*/
    public static String aesEncrypt(String plainText){
        try {
            return Base64.getEncoder().encodeToString(getAESCipher(Cipher.ENCRYPT_MODE).doFinal(plainText.getBytes(StandardCharsets.UTF_8)));
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new JSRuleException(e);
        }
    }
    public static String aesDecrypt(String encryptText) {
        try {
            return new String(getAESCipher(Cipher.DECRYPT_MODE).doFinal(Base64.getDecoder().decode(encryptText)));
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new JSRuleException(e);
        }
    }
    public static SecretKey aesGenerate(){
        return AESParam.keyGenerator.generateKey();
    }
    public static void initAES(String secretKey,String vector){
        AESParam.secretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8),AESParam.keyInstance);
        AESParam.ivSpec = new IvParameterSpec(vector.getBytes(StandardCharsets.UTF_8));
    }
    public static String initAES(String vector){
        String secretKey = Base64.getEncoder().encodeToString(aesGenerate().getEncoded());
        initAES(secretKey,vector);
        return secretKey;
    }
    private static final class AESParam {
        private static final String algorithm = "AES/CBC/PKCS5Padding";
        private static final String keyInstance = "AES";
        private static final KeyGenerator keyGenerator;
        private static IvParameterSpec ivSpec;
        private static SecretKeySpec secretKey;
        static{
            try {
                keyGenerator = KeyGenerator.getInstance(keyInstance);
                keyGenerator.init(128);
            } catch (NoSuchAlgorithmException e) {
                throw new JSRuleException(e);
            }
        }
    }
    private static Cipher getAESCipher(int mode){
        try {
            Cipher cipher = Cipher.getInstance(AESParam.algorithm);
            cipher.init(mode,AESParam.secretKey,AESParam.ivSpec);
            return cipher;
        } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
            throw new JSRuleException(e);
        }
    }
    /*--------------------------------SM2--------------------------------*/
    /**
     * <p>公钥加密
     * @param plainText 原文
     * */
    public static String sm2Encrypt(String plainText) {
        return base64Encrypt(plainText,SM2Param.publicKey,getCipher(SM2Param.transformation),(d,c)->{
            try {
                return c.doFinal(d);
            } catch (IllegalBlockSizeException | BadPaddingException e) {
                throw new JSRuleException(e);
            }
        });
    }
    /**
     * <p>私钥解密
     * @param encryptText 为公钥加密密文
     * */
    public static String sm2Decrypt(String encryptText) {
        return base64Decrypt(encryptText,SM2Param.privateKey,getCipher(SM2Param.transformation),(d,c)->{
            try {
                return c.doFinal(d);
            } catch (IllegalBlockSizeException | BadPaddingException e) {
                throw new JSRuleException(e);
            }
        });
    }
    /**
     * <p>私钥签名
     * @param text 文本
     * */
    public static String sm2Sign(String text) {
        return base64Sign(joinSignKey(text),SM2Param.privateKey,SM2Param.algorithm);
    }
    public static String sm2Sign(String text, String signKey) {
        return base64Sign(text+signKey,SM2Param.privateKey,SM2Param.algorithm);
    }
    /**
     * <p>公钥验签,自动取后台配置的秘钥对原文文本进行组合,得到组合文本后再与签名后的密文进行比对验签
     * @param text 待做校验的原文文本,可以是密文也可以不是
     * @param signText 需要比对验证的签名文本(进行签名算法后得到的文本)
     * @return true校验签名通过,false校验签名不通过
     * */
    public static boolean sm2Verify(String text,String signText) {
        return base64Verify(joinSignKey(text),signText,SM2Param.publicKey,SM2Param.algorithm);
    }
    public static boolean sm2Verify(String text,String signKey,String signText) {
        return base64Verify(text+signKey,signText,SM2Param.publicKey,SM2Param.algorithm);
    }
    /**
     * <p>生成sm2秘钥对儿</>
     * */
    public static KeyPair sm2Generate() {
        try{
            // 实例化KeyPairGenerator对象,并指定算法为EC(椭圆曲线),提供者为BC(Bouncy Castle)
            KeyPairGenerator kpg = KeyPairGenerator.getInstance(SM2Param.keyInstance, provider);
            //设置椭圆曲线参数为sm2p256v1,这是SM2算法所使用的特定椭圆曲线,初始化KeyPairGenerator对象,使用随机数生成器以增加密钥的随机性
            kpg.initialize(new ECGenParameterSpec("sm2p256v1"), new SecureRandom());
            return kpg.generateKeyPair();
        } catch (Exception e) {
            throw new JSRuleException(e);
        }
    }
    /**
     * <p>初始化SM2对象
     * */
    public static void initSM2(String pubQKeyBase64,String prvDKeyBase64){
        SM2Param.pubQKeyBase64 = pubQKeyBase64;
        SM2Param.prvDKeyBase64 = prvDKeyBase64;
        SM2Param.publicKey = initPublicKey(SM2Param.pubQKeyBase64,SM2Param.keyFactory);
        SM2Param.privateKey = initPrivateKey(SM2Param.prvDKeyBase64,SM2Param.keyFactory);
    }

    public static String[] initSM2(){
        KeyPair keyPair = sm2Generate();
        String pubQKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
        String prvDKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
        initSM2(pubQKeyBase64,prvDKeyBase64);
        return new String[]{pubQKeyBase64,prvDKeyBase64};
    }

    private static final class SM2Param {
        private static final String algorithm = "SM3withSM2";
        private static final String transformation = "SM2";
        private static final String keyInstance = "EC";
        private static final KeyFactory keyFactory;
        private static String pubQKeyBase64;
        private static String prvDKeyBase64;
        private static PublicKey publicKey;
        private static PrivateKey privateKey;
        static {
            try {
                keyFactory = KeyFactory.getInstance(SM2Param.keyInstance, new BouncyCastleProvider());
            } catch (Exception e) {
                throw new JSRuleException(e);
            }
        }
    }
    /*--------------------------------RSA--------------------------------*/
    public static String rsaEncrypt(String plainText) {
        return base64Encrypt(plainText,RSAParam.publicKey,getCipher(RSAParam.transformation), ZSSecurity::rsaEncryptSection);
    }
    public static String rsaDecrypt(String encryptText) {
        return base64Decrypt(encryptText,RSAParam.privateKey,getCipher(RSAParam.transformation), ZSSecurity::rsaDecryptSection);
    }
    private static byte[] rsaEncryptSection(byte[] data,Cipher cipher){
        return writeRsaSection(data,cipher,RSAParam.rsaEncryptSize);
    }
    private static byte[] rsaDecryptSection(byte[] data,Cipher cipher){
        return writeRsaSection(data,cipher,RSAParam.rsaDecryptSize);
    }
    private static byte[] writeRsaSection(byte[] data,Cipher cipher,int size){
        try(ByteArrayOutputStream out = new ByteArrayOutputStream()){
            int length = data.length;int offSet = 0;byte[] cache;int i = 0;
            while (length - offSet > 0) {
                if (length - offSet > size) {
                    cache = cipher.doFinal(data, offSet, size);
                } else {
                    cache = cipher.doFinal(data, offSet, length - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * size;
            }
            byte[] encryptedData = out.toByteArray();
            out.close();
            return encryptedData;
        } catch (IOException | IllegalBlockSizeException | BadPaddingException e) {
            throw new JSRuleException(e);
        }
    }
    public static String rsaSign(String text) {
        return base64Sign(joinSignKey(text),RSAParam.privateKey,RSAParam.algorithm);
    }
    public static String rsaSign(String text,String signKey) {
        return base64Sign(text+signKey,RSAParam.privateKey,RSAParam.algorithm);
    }
    public static boolean rsaVerify(String text,String signText) {
        return base64Verify(joinSignKey(text),signText,RSAParam.publicKey,RSAParam.algorithm);
    }
    public static boolean rsaVerify(String text,String signKey,String signText) {
        return base64Verify(text+signKey,signText,RSAParam.publicKey,RSAParam.algorithm);
    }
    public static KeyPair rsaGenerate() {
        try{
            KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSAParam.keyInstance, provider);
            kpg.initialize(RSAParam.rsaKeySize);
            return kpg.generateKeyPair();
        } catch (Exception e) {
            throw new JSRuleException(e);
        }
    }
    public static void initRSA(String pubQKeyBase64,String prvDKeyBase64){
        RSAParam.pubQKeyBase64 = pubQKeyBase64;
        RSAParam.prvDKeyBase64 = prvDKeyBase64;
        RSAParam.publicKey = initPublicKey(RSAParam.pubQKeyBase64,RSAParam.keyFactory);
        RSAParam.privateKey = initPrivateKey(RSAParam.prvDKeyBase64,RSAParam.keyFactory);
    }
    public static String[] initRSA(){
        KeyPair keyPair = rsaGenerate();
        String pubQKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
        String prvDKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
        initRSA(pubQKeyBase64,prvDKeyBase64);
        return new String[]{pubQKeyBase64,prvDKeyBase64};
    }
    private static final class RSAParam {
        private static final String algorithm = "SHA256withRSA";
        //如果只写RSA,在正常加密算法下解密后明文前面会出现乱码,因此此处写全transformation,PKCS1Padding通常用于非对称加密,PKCS5Padding通常用于对称加密
        private static final String transformation = "RSA";
        private static final String keyInstance = "RSA";
        private static final KeyFactory keyFactory;
        private static String pubQKeyBase64;
        private static String prvDKeyBase64;
        private static PublicKey publicKey;
        private static PrivateKey privateKey;
        private static final int rsaKeySize = 512;
        private static final int rsaDecryptSize = rsaKeySize/8;
        private static final int rsaEncryptSize = rsaDecryptSize-1;
        static {
            try {
                keyFactory = KeyFactory.getInstance(RSAParam.keyInstance);
            } catch (Exception e) {
                throw new JSRuleException(e);
            }
        }
    }
    /*由于Cipher是有状态的,因此每次加解密都需要重新创建Cipher对象*/
    private static Cipher getCipher(String transformation){
        try {
            return Cipher.getInstance(transformation,provider);
        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
            throw new JSRuleException(e);
        }
    }

    private static String base64Encrypt(String plainText,Key key,Cipher cipher,BiFunction<byte[],Cipher,byte[]> doFinal) {
        return Base64.getEncoder().encodeToString(encrypt(plainText.getBytes(StandardCharsets.UTF_8),key,cipher,doFinal));
    }
    private static String base64Decrypt(String encryptText,Key key,Cipher cipher,BiFunction<byte[],Cipher,byte[]> doFinal) {
        return new String(decrypt(Base64.getDecoder().decode(encryptText),key,cipher,doFinal));
    }
    private static String base64Sign(String text,PrivateKey privateKey,String algorithm) {
        return Base64.getEncoder().encodeToString(sign(text.getBytes(StandardCharsets.UTF_8),privateKey,algorithm));
    }
    private static boolean base64Verify(String text,String signText,PublicKey publicKey,String algorithm) {
        return verify(text,Base64.getDecoder().decode(signText),publicKey,algorithm);
    }
    private static PublicKey initPublicKey(String pubQKeyBase64,KeyFactory keyFactory) {
        try {
            return keyFactory.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(pubQKeyBase64)));
        } catch (InvalidKeySpecException e) {
            throw new JSRuleException(e);
        }
    }
    private static PrivateKey initPrivateKey(String prvDKeyBase64,KeyFactory keyFactory) {
        try {
            return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(prvDKeyBase64)));
        } catch (InvalidKeySpecException e) {
            throw new JSRuleException(e);
        }
    }
    private static byte[] cipherFinal(byte[] data, int mode, Key key, Cipher cipher,BiFunction<byte[],Cipher,byte[]> doFinal){
        try{
            cipher.init(mode, key);
        }catch (Exception e){
            throw new JSRuleException(e);
        }
        return doFinal.apply(data,cipher);
    }
    private static byte[] encrypt(byte[] data,Key key, Cipher cipher, BiFunction<byte[],Cipher,byte[]> doFinal) {
        return cipherFinal(data,Cipher.ENCRYPT_MODE, key,cipher,doFinal);
    }
    private static byte[] decrypt(byte[] encryptData,Key key,Cipher cipher, BiFunction<byte[],Cipher,byte[]> doFinal) {
        return cipherFinal(encryptData,Cipher.DECRYPT_MODE, key,cipher,doFinal);
    }
    private static byte[] sign(byte[] data,PrivateKey privateKey,String algorithm) {
        try {
            return getSignature(data,signature->{
                try {
                    signature.initSign(privateKey);
                } catch (InvalidKeyException e) {
                    throw new JSRuleException(e);
                }
            },algorithm).sign();
        } catch (SignatureException e) {
            throw new JSRuleException(e);
        }
    }
    private static boolean verify(String data, byte[] signatureBytes,PublicKey publicKey,String algorithm) {
        try {
            return getSignature(data.getBytes(StandardCharsets.UTF_8),signature->{
                try {
                    signature.initVerify(publicKey);
                } catch (InvalidKeyException e) {
                    throw new JSRuleException(e);
                }
            },algorithm).verify(signatureBytes);
        } catch (SignatureException e) {
            throw new JSRuleException(e.getMessage());
        }
    }
    private static Signature getSignature(byte[] data, Consumer<Signature> behavior,String algorithm) {
        try{
            // 初始化签名对象,指定使用SM3withSM2算法和BC提供者。
            Signature signature = Signature.getInstance(algorithm, provider);
            behavior.accept(signature);
            signature.update(data);
            return signature;
        } catch (Exception e) {
            throw new JSRuleException(e.getMessage());
        }
    }
}

可以把JSRuleException替换成自己的异常类,rsaKeySize经大文本和小文本加解密的反复比较,介于安全和性能来考量,默认值最好在512,通常来说设置的越小,性能越好,不过密钥过短会导致安全性减弱

下面是使用例子,非常简单

ZSSecurity.initRSA(rsaPublicKey,rsaPrivateKey);//也可以通过String[] keyPair = ZSSecurity.initRSA();来创建动态密钥对儿并同时初始化,详情见源码
String miwen = ZSSecurity.rsaEncrypt(json);
System.out.println(ZSSecurity.rsaDecrypt(miwen));
String qianming = ZSSecurity.rsaSign(json,"vvv");
System.out.println(ZSSecurity.rsaVerify(json,"vvv",qianming));

第二个参数vvv是签名的密钥,你可以理解为盐,是一个自定义的字符串

注意:在做rsa分段加解密的时候,两端的分段加密算法相关参数要相同,如rsaKeySIze,encryptSize,deCryptSize等,否则会有解码出来的明文前面多出一部分乱码字符的情况


上面的代码巧用内部类,一来可以隐藏相关配置,二来可以禁止外部随意访问和篡改,三来包裹各自的加密算法的参数,让代码看起来更加的模块化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容