优雅的处理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等,否则会有解码出来的明文前面多出一部分乱码字符的情况


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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容