• 后端私钥加密,前端使用wxapp_rsa.js用公钥解密


    最近开发了一个需求,后端用私钥对返回的数据进行加密,前端用对应的公钥进行解密。为什么会这样呢?因为后端返回的数据有手机号、身份证号等明文数据,公司出于安全考虑,所以要对这样的接口进行安全改造。
    因为之前,前端在项目中有使用wxapp_rsa.js进行公钥加密,后端私钥解密这样的功能,所以我就尝试用wxapp_rsa.js去解密,但是如果前端私钥解密的话,就会存在私钥泄露的情况,所以最后就定义为后端私钥加密,前端公钥解密。
    真正去开发需求的时候还是遇到了很多问题,下面我会一一道来。

    一、前后端确定加密的方式,后端加密,前端解密

    出于安全考虑,后端私钥加密,前端公钥解密(虽然这不符合常规,一般都是公钥加密,私钥解密或者验签的时候,私钥加密,公钥验签)

    二、前端公钥解密,wxapp_rsa.js有私钥解密的方法,没有公钥解密的方法,需要对wxapp_rsa.js这个插件进行改造。

    改造如下:
    1、修改RSADecrypt函数

    function RSADecrypt(b) {
        var d = parseBigInt(b, 16);
        // 这里doPrivate修改为doPublic
        // var m = this.doPrivate(d);
        var a = this.doPublic(d);
        if (a == null) {
            return null
        }
        return pkcs1unpad2(a, (this.n.bitLength() + 7) >> 3)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2、修改pkcs1unpad2函数

    function pkcs1unpad2(g, j) {
        var a = g.toByteArray();
        var f = 0;
        while (f < a.length && a[f] == 0) {++f
        }
        // // 这里将如下三行代码注释
        // if (a.length - f != j - 1 || a[f] != 2) {
        //     return null
        // }
        ++f;
        while (a[f] != 0) {
            if (++f >= a.length) {
                return null
            }
        }
        var e = "";
        while (++f < a.length) {
            var h = a[f] & 255;
            if (h < 128) {
                e += String.fromCharCode(h)
            } else {
                if ((h > 191) && (h < 224)) {
                    e += String.fromCharCode(((h & 31) << 6) | (a[f + 1] & 63)); ++f
                } else {
                    e += String.fromCharCode(((h & 15) << 12) | ((a[f + 1] & 63) << 6) | (a[f + 2] & 63));
                    f += 2
                }
            }
        }
        return e
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    三、使用修改过的函数进行解密,发现数据过长时,解密报错

    原因:在本地自测的时候发现,如果数据过长,解密就会报错。
    数据量太大,加密解密报错:一般来说密钥长度为1024,加密长度为 128 ,加密长度为 117 ,如果字符超过这个数量就会报错
    解决方案
    采用分段加密解密,计算出来需要加密数据的长度

    那就需要写两个分段加解密的方法:
    1、分段解密的函数

        // 分段解密,支持中文
         RSAKey.prototype.decryptUnicodeLong = function (string) {
             var k = this;
             //解密长度=key size.hex2b64结果是每字节每两字符,所以直接*2
             var maxLength = ((k.n.bitLength()+7)>>3)*2;
             try {
                 var hexString = b64tohex(string);
                 var decryptedString = "";
                 var rexStr=".{1," + maxLength  + "}";
                 var rex =new RegExp(rexStr, 'g'); 
                 var subStrArray = hexString.match(rex);
                 if(subStrArray){
                     subStrArray.forEach(function (entry) {
                         decryptedString += k.decrypt(entry);
                     });
                     return decryptedString;
                 }
             } catch (ex) {
                 return false;
             }
         };
             
         
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2、分段加密的函数

    // 分段加密,支持中文
         RSAKey.prototype.encryptUnicodeLong = function (string) {
             var k = this.getKey();
             //根据key所能编码的最大长度来定分段长度。key size - 11:11字节随机padding使每次加密结果都不同。
             var maxLength = ((k.n.bitLength()+7)>>3)-11;
             try {
                 var subStr="", encryptedString = "";
                 var subStart = 0, subEnd=0;
                 var bitLen=0, tmpPoint=0;
                 for(var i = 0, len = string.length; i < len; i++){
                     //js 是使用 Unicode 编码的,每个字符所占用的字节数不同
                     var charCode = string.charCodeAt(i);
                     if(charCode <= 0x007f) {
                         bitLen += 1;
                     }else if(charCode <= 0x07ff){
                         bitLen += 2;
                     }else if(charCode <= 0xffff){
                         bitLen += 3;
                     }else{
                         bitLen += 4;
                     }
                     //字节数到达上限,获取子字符串加密并追加到总字符串后。更新下一个字符串起始位置及字节计算。
                     if(bitLen>maxLength){
                         subStr=string.substring(subStart,subEnd)
                         encryptedString += k.encrypt(subStr);
                         subStart=subEnd;
                         bitLen=bitLen-tmpPoint;
                     }else{
                         subEnd=i;
                         tmpPoint=bitLen;
                     }
                 }
                 subStr=string.substring(subStart,len)
                 encryptedString += k.encrypt(subStr);
                 return hex2b64(encryptedString);
             } catch (ex) {
                 return false;
             }
         };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    四、使用第三步写的decryptUnicodeLong函数对长数据进行解密,偶发中文乱码问题

    使用第三步写的decryptUnicodeLong函数对长数据进行解密,刚开始本地自测的时候是好的,加大了数据量也是好的。然后发布到测试环境,发现偶尔会出现中文乱码的问题,经过一番搜索之后,发现原因是加密和解密数据都是要转成byte[] 类型的,字符串占字节为3,所以在分割字节的时候,将一个汉字分割成了两个数组的结尾和开头,这样就会出现乱码的情况。

    那怎么解决呢?

    探索了一番之后,解决方案如下:

    • 第一步:后端Java进行分段加密
      这样返回的就是一个string数组到前台,格式如:{ data: [“密文片段1”, “密文片段2”, “密文片段3”] }

    【以前的加密方式】:

    String content = "1234567890";
     
     byte[] data = content.getBytes();
     byte[] encodedData = RSAUtil.encrypt(data, publicKey);
     
     String encryptedContent = Base64Util.encode(encodedData);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这种是将所有json字符串加密为一个字符串。密文有的时候很长很长,几十甚至上百KB。

    【优化后的加密方式】:

    String content = "1234567890";
     
     List<String> encryptedList = new ArrayList<>();
     
     //每X个字符,加密一次
     if (content != null) {
         int startIndex = 0;
         int endIndex = 0;
         int subLength = 50;
     
         while (true) {
             endIndex = startIndex + subLength;
     
             if (content.length() <= endIndex) {
                 endIndex = startIndex + (content.length() - startIndex);
             }
     
     
             //region 将截取到的字符串,进行加密
             byte[] data = content.substring(startIndex, endIndex).getBytes();
             byte[] encodedData = RSAUtil.encrypt(data, publicKey);
             String encryptedStr = Base64Util.encode(encodedData);
     
             encryptedList.add(encryptedStr);
             //endregion
     
     
             startIndex += subLength;
     
             if (startIndex >= content.length()) {
                 break;
             }
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这样返回的就是一个string数组到前台,格式如:{ data: [“密文片段1”, “密文片段2”, “密文片段3”] }

    • 第二步:前端解密&拼接:
    const decryptor = new JSEncrypt();
     const privateKey = "privateKey";
     decryptor.setPrivateKey(privateKey);
     
     let jsonStr = "";
     if (encryptedList && encryptedList.length > 0) {
         for (let encrypted of encryptedList) {
              jsonStr += decryptor.decryptLong(encrypted);
         }
     }
     let jsonObj = JSON.parse(jsonStr)  // 这样就得到一个JSON对象
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样将每个密文片段,解密后,再按顺序拼接起来,即可得到加密前的原始json字符串。随后直接转化为json对象即可。

    以上就是我做这个需求经历的问题和探索的过程。
    感谢以下文章的帮助:
    uni-app app vue 小程序 RSA 加密/解密 使用 jsencrypt 踩坑(字符乱码问题)(二)
    RSA加密解密,分段加密,分段解密,部分汉字乱码详解

  • 相关阅读:
    腾讯云4核8G云服务器租用价格选轻量还是CVM?性能如何?
    Python 算法交易实验41 GMM简单估计
    [人脸算法]技术方向综述
    左神算法(二)
    Stream流中的 mapToInt()方法
    关于8月版本UNICHAR的几个问题
    【2018】【论文笔记】最后一米THz——
    [软考中级]软件设计师-信息安全
    为普通用户授权访问k8s资源(tls,rbac)
    Leetcode算法入门与数组丨5. 数组二分查找
  • 原文地址:https://blog.csdn.net/xiaolinlife/article/details/126042590