非对称加解密

非对称加解密

概述

非对称加解密与对称加解密不同,非对称加解密密钥成对,分为公钥和私钥。根据不同使用场景使用其中一个密钥加密,另外一个密钥进行解密。对数据进行加解密一般使用公钥加密,私钥解密,若是数字签名,那么一般通过私钥进行签名,用公钥进行验签。

算法

本文介绍两种非对称加解密算法:SM2、RSA

RSA 算法

简介

RSA 加密算法的安全性基于大数不能分解质因数为基础,涉及到一些数学原理:互质关系、欧拉函数、欧拉定理、模反元素。密钥分为:公钥和私钥,公钥可以供任何人使用,私钥为自己使用,采用 768位、1024位 和 2048位密钥。RSA 加密速度比较慢适合较小的数据量进行加解密。

特别提醒:相同数据使用加密后的密文不同,但是都可以用私钥解密

小试牛刀
  1. 基于 JDK 实现完整 demo,示例如下:
   public static void main(String[] args) {
        String content = "RSA demo";
        try {
            // 获得密钥对
            KeyPair keyPair = getKeyPair();
            // 获得进行Base64 加密后的公钥和私钥 String
            String privateKeyStr = getPrivateKey(keyPair);
            String publicKeyStr = getPublicKey(keyPair);

            System.out.println("Base64处理后的私钥:" + privateKeyStr);
            System.out.println("Base64处理后的公钥:" + publicKeyStr);

            // 获得原始的公钥和私钥
            PrivateKey privateKey = string2Privatekey(privateKeyStr);
            PublicKey publicKey = string2PublicKey(publicKeyStr);

            // 公钥加密/私钥解密
            byte[] publicEncryBytes = publicEncrytype(content.getBytes(), publicKey);
            System.out.println("公钥加密后的字符串Base64:" + Base64.getEncoder().encodeToString(publicEncryBytes));
            
            byte[] privateDecryBytes = privateDecrypt(publicEncryBytes, privateKey);
            System.out.println("私钥解密后的原始字符串:" + new String(privateDecryBytes));
            
        } catch (Exception e) {
            log.error("RSA加解密异常", e);
        }

    }

    private static KeyPair getKeyPair() throws NoSuchAlgorithmException, UnsupportedEncodingException {
        // 获得RSA密钥对的生成器实例
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        // 安全的随机数
        SecureRandom secureRandom = new SecureRandom(String.valueOf(System.currentTimeMillis()).getBytes("utf-8"));
        // 这里可以是1024、2048 初始化一个密钥对
        keyPairGenerator.initialize(1024, secureRandom);
        // 获得密钥对
        return keyPairGenerator.generateKeyPair();
    }


    private static String getPublicKey(KeyPair keyPair) {
        PublicKey publicKey = keyPair.getPublic();
        byte[] bytes = publicKey.getEncoded();
        return Base64.getEncoder().encodeToString(bytes);
    }

    private static String getPrivateKey(KeyPair keyPair) {
        PrivateKey privateKey = keyPair.getPrivate();
        byte[] bytes = privateKey.getEncoded();
        return Base64.getEncoder().encodeToString(bytes);
    }


    private static PublicKey string2PublicKey(String pubStr) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = Base64.getDecoder().decode(pubStr);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(keySpec);
    }

    private static PrivateKey string2Privatekey(String priStr) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] bytes = Base64.getDecoder().decode(priStr);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(keySpec);
    }

    private static byte[] publicEncrytype(byte[] content, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(content);
    }

    private static byte[] privateDecrypt(byte[] content, PrivateKey privateKey) throws Exception{
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(content);
    }
  1. Hutool 非对称加密章节中也介绍了 RSA 算法使用,通过创建一个 RSA 对象,通过此对象创建密钥对,获取公钥和私钥,然后通过此对象实现加解密,但是实际开发中加密和解密是分开的,所以在 Hutool 文档中有提示。

特别提醒:对于加密和解密可以完全分开,对于 RSA 对象,如果只使用公钥或私钥,另一个参数可以为null

如果加密和解密分开如何使用呢?以下示例是私钥加密,公钥解密,示例如下:

    // 私钥 Base64 字符串
    private static final String PRIVATE_KEY  = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKYj8EMx+TGu+rA9Pv6+jhIKcZtSA7AqenBbfBEnvJc/gvamPfELgDdk7KKiSN+Q1fGquW945UJuGRioy/0qatB7CtFTvVVVcibaTnXHqBoNScJesldOQy0JYlQztEStMVHkG7KdfW178j0iTLKJnvvQM8bAuiQdx64t4QlNbT0BAgMBAAECgYBKQC6NQWTO7RZRtJWWUUB6VJuQYHeQgHOHCoPowNsat4JGzGZLd6neV+cgCipKbFcJchT8+klvxnfF2w6LvyzL9pIGmCQFwv3U3mcOrTJ/+nYql5NrBiufvuAyWCh5bEKJZYQ/6Uzd7N+HTjdpx0m3XxQQtQPrXLCNLbsnlFW5BwJBAM8tO/kjcUhD7RDoH/KymnffdNs8flkmrZbamnwZgZROSizJ1EMV7nu6FuJZkXHuEQ8P0H2o847F6oigtzoeZdcCQQDNSwYo2aKK5XazTye8zJVVlVS7HUSIYDtWnTLTFNQyFMmZG4jwE2CNQzMzagdH1BmObfs5zSE5Bk03RMr+YGjnAkEAxS8gbbe2EjnUYMsN3UjwjDc6WY/yEZgmj/XwIz2Df0wkfQx74n31Rf2P2k+1huI3ikZbAb7UUYc9+lw9CCv2cQJABkvYwoP6QjxLabBxzY6QvfE4igyZv30EFOH5XxPydh7BGBsKFiLiATMgbOFBm+hbaEzjOaCa9j7FO362oxqd3QJALqL5OcHkgj6XVqJPtZSM5cd57Av+51kDPQlUCuPLk1Kda5TxMkMpNtTbCYcunpbZxzBboNFht+EjAZSQddeotA==";

    // 公钥 Base64 字符串
    private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmI/BDMfkxrvqwPT7+vo4SCnGbUgOwKnpwW3wRJ7yXP4L2pj3xC4A3ZOyiokjfkNXxqrlveOVCbhkYqMv9KmrQewrRU71VVXIm2k51x6gaDUnCXrJXTkMtCWJUM7RErTFR5BuynX1te/I9IkyyiZ770DPGwLokHceuLeEJTW09AQIDAQAB";
    
    // 创建 RSA 对象,设置算法、私钥、公钥为null
    RSA rsa = new RSA("RSA/ECB/PKCS1Padding",PRIVATE_KEY, null);
    String text = "明文测试内容";
    
    // 私钥加密
    byte[] encrypt = rsa.encrypt(text, KeyType.PrivateKey);
    String s2 = Base64.encodeBase64String(encrypt);
    System.out.println("密文:" + s2);

    // 创建 RSA 对象,设置算法、私钥为null、公钥
    RSA rsa1 = new RSA("RSA/ECB/PKCS1Padding",null, PUBLIC_KEY);
    
    // 公钥解密
    String s1 = rsa1.decryptStr(s, KeyType.PublicKey);
    System.out.println("明文:" + s1); 

Hutool 创建密钥的方式也很简单,示例如下:

        // 创建 RSA 对象
        RSA rsa = new RSA();
        
        // 获取公钥 Base64 字符串
        String publicKeyBase64 = rsa.getPublicKeyBase64();
        
        // 获取私钥 Base64 字符串
        String privateKeyBase64 = rsa.getPrivateKeyBase64();
        System.out.println("公钥 Base64" + publicKeyBase64);
        System.out.println("私钥 Base64" + privateKeyBase64);
        
       ---------------------
       // 通过工具类 SecureUtil 获取公钥
       PublicKey publicKey = SecureUtil.generatePublicKey("RSA", Base64.decodeBase64(key)); 
       
       // 通过工具类 SecureUtil 获取私钥
       PrivateKey privateKey = SecureUtil.generatePrivateKey("RSA", Base64.decodeBase64(key)); 

密钥转成 Base64 字符串方便存储。

SM2 算法

简介

SM2算法全称是SM2椭圆曲线公钥密码算法,是一种基于椭圆曲线的密码(ECC),用来替换RSA加密算法的。

由于SM2也是非对称加密算法,与RSA相比,复杂度更高,同等安全强度下,密钥长度较短,运算效率都要更优,但是国密算法尚未实现广泛的兼容性,在主流浏览器,操作系统的终端环境中不受信任,面向互联网的产品引用中采用国密算法将无法满足可用性、易用性和全球通用性的需求。

同等安全强度下,密钥长度对比:

RSA key size (bits)ECC key size (bits)
1024160
2048224
3072256
7680384
15360521

SM2 非对称加密的结果由 C1,C2,C3 三部分组成。其中 C1 是生成随机数的计算出的椭圆曲线点,C2 是密文数据,C3 是SM3的摘要值。最开始的国密标准的结果是按 C1C2C3 顺序的,新标准的是按 C1C3C2 顺序存放的。

小试牛刀
  1. 同样基于 JDK 实现 SM2 加解密,示例如下:
    private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
    private static ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(),
            x9ECParameters.getG(), x9ECParameters.getN());
    private static ECParameterSpec ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(),
            x9ECParameters.getG(), x9ECParameters.getN());

    static {
        if (Security.getProvider("BC") == null) {
            Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        }
    }

    public static void main(String[] args) {
        //生成密钥对
        KeyPair kp = generateKeyPair();
        
        //获取公钥
        PublicKey publicKey = kp.getPublic();
        
        //获取私钥
        PrivateKey privateKey = kp.getPrivate();
        
        //明文
        String text = "SM2 Demo";
        byte[] encrypt = encrypt(text.getBytes(), publicKey, "C1C3C2");
        System.out.println(Base64.encodeBase64String(encrypt));
        
        encrypt = decrypt(encrypt, privateKey, "C1C3C2");
        System.out.println(new String(encrypt));
    }
    
    private static KeyPair generateKeyPair() {
        try {
            KeyPairGenerator kpGen = KeyPairGenerator.getInstance("EC", "BC");
            kpGen.initialize(ecParameterSpec, new SecureRandom());
            return kpGen.generateKeyPair();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] encrypt(byte[] data, PublicKey key, String standard) {
        if ("C1C2C3".equals(standard)) {
            return encryptNew(encryptOld(data, key));
        }
        return encryptOld(data, key);
    }

    private static byte[] decrypt(byte[] data, PrivateKey key, String standard) {
        if ("C1C2C3".equals(standard)) {
            return decryptOld(decryptNew(data), key);
        }
        return decryptOld(data, key);
    }

    private static byte[] encryptOld(byte[] data, PublicKey key) {
        BCECPublicKey bcecPublicKey = (BCECPublicKey) key;
        ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(bcecPublicKey.getQ(), ecDomainParameters);
        SM2Engine sm2Engine = new SM2Engine();
        sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
        try {
            return sm2Engine.processBlock(data, 0, data.length);
        } catch (InvalidCipherTextException e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] encryptNew(byte[] c1c2c3) {
        final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
        final int c3Len = 32;
        byte[] result = new byte[c1c2c3.length];
        System.arraycopy(c1c2c3, 0, result, 0, c1Len);
        System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len);
        System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len);
        return result;
    }

    private static byte[] decryptOld(byte[] data, PrivateKey key) {
        BCECPrivateKey bcecPrivateKey = (BCECPrivateKey) key;
        ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(bcecPrivateKey.getD(), ecDomainParameters);
        SM2Engine sm2Engine = new SM2Engine();
        sm2Engine.init(false, ecPrivateKeyParameters);
        try {
            return sm2Engine.processBlock(data, 0, data.length);
        } catch (InvalidCipherTextException e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] decryptNew(byte[] c1c3c2) {
        final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1;
        final int c3Len = 32;
        byte[] result = new byte[c1c3c2.length];
        System.arraycopy(c1c3c2, 0, result, 0, c1Len);
        System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len);
        System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len);
        return result;
    }
  1. 基于 Hutool 实现 SM2 加解密,Hutool 在国密算法章节中介绍了 SM2,并且列举了加解密和签名验签的示例,如何将加密和解密以及签名和验签分开实现呢?示例如下:
        String PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgbcopbJUM3mK8e+swrzhgausNC9eSbQA4zyIG5GxnUaWgCgYIKoEcz1UBgi2hRANCAAQRa9Lg5jFl2QL0U4d0FN4zlkpMfXffa4eG2ap7JJa0r5D1mR6z/Zraq3aZstzrUf5QkBfyfSkwaW6smmbqNePG";

        String PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEEWvS4OYxZdkC9FOHdBTeM5ZKTH1332uHhtmqeySWtK+Q9Zkes/2a2qt2mbLc61H+UJAX8n0pMGlurJpm6jXjxg==";

        String text = "明文测试内容";
        // 获取公钥
        PublicKey publicKey = SecureUtil.generatePublicKey("SM2", Base64.decodeBase64(PUBLIC_KEY));
        SM2 sm2 = new SM2();
        //设置标准
        sm2.setMode(SM2Engine.Mode.C1C2C3);
        //设置公钥
        sm2.setPublicKey(publicKey);
        //公钥加密
        byte[] encrypt = sm2.encrypt(text.getBytes(), KeyType.PublicKey);
        System.out.println("密文:" + Base64.encodeBase64String(encrypt));

        //获取私钥
        PrivateKey privateKey = SecureUtil.generatePrivateKey("SM2", Base64.decodeBase64(PRIVATE_KEY));
        SM2 sm21 = new SM2();
        //设置标准
        sm21.setMode(SM2Engine.Mode.C1C2C3);
        //设置私钥
        sm21.setPrivateKey(privateKey);
        //私钥解密
        byte[] decrypt = sm21.decrypt(encrypt, KeyType.PrivateKey);
        System.out.println("明文:" + new String(decrypt)); 

特别提醒:SM2 不能私钥加密,公钥解密,否则会抛异常,例如:Encrypt is only support by public key

  1. 非对称加解密算法可用于数字签名,一般通过私钥签名,公钥验签,Hutool 使用 SM2 曲线点构建 SM2 的示例变量有误,而且 Hutool 所举示例都是通过一个对象实现签名和验签的,实际开发签名和验签也是分开的,示例如下:
        //需要加密的明文
        String text = "明文测试内容";

        //私钥16进制字符串
        String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d047930770201010420f60a8692dc3751a728b48b1418b8d9d9d3debf81d6af8e79887fa2b6ed3b996fa00a06082a811ccf5501822da144034200043e7e702ff276d7f7139b6e61f7b1d3b3dc6364b5fa171355db2d3dd8adf71428780ee85d01a85bba4e2c7210bdd00bf936098c44a86e97c962396e3d04c40350";

        //公钥16进制字符串
        String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d034200043e7e702ff276d7f7139b6e61f7b1d3b3dc6364b5fa171355db2d3dd8adf71428780ee85d01a85bba4e2c7210bdd00bf936098c44a86e97c962396e3d04c40350";

        //创建 SM2 对象
        SM2 sm2 = new SM2(HexUtil.decodeHex(privateKey), null);

        //签名
        byte[] sign = sm2.sign(text.getBytes(), null);

        System.out.println("数据:" + HexUtil.encodeHexStr(text.getBytes()));

        String s = Base64.encodeBase64String(sign);
        System.out.println("签名 Base64 字符串:" + Base64.encodeBase64String(sign));


        //创建 SM2 对象
        SM2 sm = new SM2(null, HexUtil.decodeHex(publicKey));

        //验签
        boolean verify = sm.verify(text.getBytes(), Base64.decodeBase64(s));
        System.out.println("结果:" + verify);

本示例私钥和公钥使用了16进制字符串,当然也可以使用 Base64 字符串。

优缺点

  • 非对称加密算法的保密性好,它消除了最终用户交换密钥的需要。但是加解密速度要远远慢于对称加密。

  • 算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。

场景

  1. 信息加密

收信者是唯一能够解开加密信息的人,因此收信者手里的是私钥。发信者手里的是公钥,其它人知道公钥没有关系,因为其它人发来的信息对收信者没有意义。

  1. 登录认证

客户端需要将认证标识传送给服务器,此认证标识(可能是一个随机数)其它客户端可以知道,因此需要用私钥加密,客户端保存的是私钥。服务器端保存的是公钥,其它服务器知道公钥没有关系,因为客户端不需要登录其它服务器。

  1. 数字签名

数字签名是为了表明信息没有受到伪造,确实是信息拥有者发出来的,附在信息原文的后面。就像手写的签名一样,具有不可抵赖性和简洁性。

简洁性:对信息原文做哈希运算,得到消息摘要,信息越短加密的耗时越少。

不可抵赖性:信息拥有者要保证签名的唯一性,必须是唯一能够加密消息摘要的人,因此必须用私钥加密 (就像字迹他人无法学会一样),得到签名。如果用公钥,那每个人都可以伪造签名了。

  1. 数字证书

问题起源:对1和3,发信者怎么知道从网上获取的公钥就是真的?没有遭受中间人攻击?

这样就需要第三方机构来保证公钥的合法性,这个第三方机构就是 CA (Certificate Authority),证书中心。

CA 用自己的私钥对信息原文所有者发布的公钥和相关信息进行加密,得出的内容就是数字证书。

信息原文的所有者以后发布信息时,除了带上自己的签名,还带上数字证书,就可以保证信息不被篡改了。信息的接收者先用 CA给的公钥解出信息所有者的公钥,这样可以保证信息所有者的公钥是真正的公钥,然后就能通过该公钥证明数字签名是否真实了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值