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

技术博客 (110) 2023-10-16 09:01:01

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

最近开发了一个需求,后端用私钥对返回的数据进行加密,前端用对应的公钥进行解密。为什么会这样呢?因为后端返回的数据有手机号、身份证号等明文数据,公司出于安全考虑,所以要对这样的接口进行安全改造。 

因为之前,前端在项目中有使用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)
}

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
}

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

原因:在本地自测的时候发现,如果数据过长,解密就会报错。

数据量太大,加密解密报错:一般来说密钥长度为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;
         }
     };
         
     

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;
         }
     };

四、使用第三步写的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);

这种是将所有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;
         }
     }
 }

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

第二步:前端解密+拼接:

const decrypt = new JSEncrypt();
 const PrivateKey = "PrivateKey";
 decrypt.setPrivateKey(PrivateKey);
 
 let JsonStr = "";
 if (encryptedList && encryptedList.length > 0) {
     for (let encrypted of encryptedList) {
          JsonStr += decrypt.decryptLong(encrypted);
     }
 }
 let jsonObj = JSON.parse(JsonStr)  // 这样就得到一个JSON对象

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

以上就是我做这个需求经历的问题和探索的过程。

这是我之前发表在其他平台上的原创文章。

一万年太久,只争朝夕!

我们下篇文章再见!

参考文献:

www.wjhsh.net/donkeysmall…
blog.csdn.net/qq\_4156621…

THE END

发表回复