前言
需求:
对加密文本 textPlain 进行 AES 加密 AES 加密中的 key 通过 RSA 加密 并通过加解签对解密结果进行验证
本文为了统一, 随机数 key 为固定字符串
let key = "zCpdywHdFfPYQwYi";let textPlain = "{id:'123',name:'xiaoyu',address:'address:'上海市浦东新区'}";如果 key 随机生成,代码如下
let key = randomString(16);
function randomString(len) { len = len || 32; // 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1 let $chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"; let maxPos = $chars.length; let str = ""; for (i = 0; i < len; i++) { str += $chars.charAt(Math.floor(Math.random() * maxPos)); } return str;}其中 RSA 加密中的公密钥可通过此链接生成
let privateKey = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgG+u7KERg6DWKDhK7NRUe63FPpxbzBAUWxULzjYCJexsvgzXjj/m UPvTR/wjFXf49sidHDcEwhzx18k8aJ0+Rt5AUtNExTeAIIGo/mytyQwlS0GuazdW Sqw+1IOEJe88f/I62prFuuVuY+cIxucXDVpxwv5mfkMjeiTIixWyju4tAgMBAAEC gYABkOtiitNUswwXVf2iMJkXY7ewC5198DsivWqj2T3c5d14YqFtBfbGwTy2LWvE peoYABQANYHfVAjZmo5A/jmSKWHaCHd+k8sMQeThdklGVGHBctZ46aJ32c4CRmZa pHAtgYVYBxOyn/55WlJb9cYEYGE1M9ouOmsEwW/sUruyZQJBANmmXVhte5LNCrmr c7GFbJQLNT4YTcGPXHyRdxjGBRfcgAK0IHREUrBGjCY+N4DYd3QxRYTAF+EYileF ou3C1bcCQQCDXKz9d3pibVtfi/qcO6bn3Bdqfv641sEO5++dD/SCAeUSTrd8KO/e ZoVIkYV8lcj5Ol6cRhXr9TGTGHUXzbs7AkACvxmwcvk7OgEnpKU6GT/sglOyq+XX SnLVU5SYKG+KBItM/W7naLFObiV4mhJjOtgPDEwfR7lFauTcA+CxdWLzAkBaOEVr lCqO/VKbTdVihim85tGCSBSEOKLBrRRJyBKDU+/TYhLtCGSuV7VEwQVwKqOsYKVo KU6XD4Dr6QlKvvbPAkEAkjlU5ua5BfGh601U8va1xWyxh0TqemBTsyoQ8bSwOpZH NgGjWqv5Z0QoK99pJv7VfReKbLyIg2MXhpigTogeWA== -----END RSA PRIVATE KEY-----`;
let publicKey = `-----BEGIN PUBLIC KEY----- MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgG+u7KERg6DWKDhK7NRUe63FPpxb zBAUWxULzjYCJexsvgzXjj/mUPvTR/wjFXf49sidHDcEwhzx18k8aJ0+Rt5AUtNE xTeAIIGo/mytyQwlS0GuazdWSqw+1IOEJe88f/I62prFuuVuY+cIxucXDVpxwv5m fkMjeiTIixWyju4tAgMBAAE= -----END PUBLIC KEY-----`;另外还需下载aes.js jsencrypt.js jsrsasign.js
<script src="./jsencrypt.js"></script><script src="./aes.js"></script><script src="./jsrsasign-all-min.js">RSA 加解密 key
let RSACrypt = new JSEncrypt();RSA 加密
RSACrypt.setKey(publicKey);let keyEnc = RSACrypt.encrypt(key); // 每次加密结果不一致RSA 解密
RSACrypt.setKey(privateKey);let keyDec = RSACrypt.decrypt(keyEnc); // zCpdywHdFfPYQwYiAES 加解密
AES 加密
let textEnc = AESEncrypt(textPlain, key); // 每次加密结果一致// xoJ9fYIqyoAA3Qbhm/h8nGy4KjcmHVtusheMTd61mpUB8Lr6SDFQ1pWimi+WwlZyL5ncvZ9kQKjoRcRXUB7/D6uVkl724DzxBu/Ns1ivWnA=
// AES加密函数function AESEncrypt(textPalin, key) { let key = CryptoJS.enc.Utf8.parse(key); let textEnc = CryptoJS.AES.encrypt(textPlain, key, { iv: CryptoJS.enc.Utf8.parse("1234567812345678"), mode: CryptoJS.mode.CBC, //padding: CryptoJS.pad.Iso10126 }); return textEnc.toString();}AES 解密
// 利用解密后的keyDec解密textEnclet textDec = AESDecrypt(textEnc, keyDec); // {id:'123',name:'xiaoyu',address:'address:'上海市浦东新区'}
// AES解密函数function AESDecrypt(word, key) { let key = CryptoJS.enc.Utf8.parse(key); let decrypt = CryptoJS.AES.decrypt(word, key, { iv: CryptoJS.enc.Utf8.parse("1234567812345678"), mode: CryptoJS.mode.CBC, //padding: CryptoJS.pad.Pkcs7 }); let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8); return decryptedStr.toString();}RSA 加解签
RSA 加签
let sign = getSign(textPlain); // 每次加签结果一致// 3b073ad8655293c8ec92dddeb6b986fbad190d642e2ca74f57f90aeedb1645be20449e0e3a7824f483d72de120221b0a29f2adf5c0cd0b913cee01d8d6a13eeb55c9f2b9a3868dc29181cddbd0fde15264d40a46f74ab7db23a09bc48d93a376eb8d50c84e67a7d08ea9810a76badfc4bb7f5e8cdbd045ef3551389eb1b9cef7
// 加签函数function getSign(signData) { let signPrivateKey = privateKey; let sig = new KJUR.crypto.Signature({ alg: "SHA1withRSA", prvkeypem: signPrivateKey, }); let hashAlg = "sha1"; // 设置sha1 let sign = sig.signString(signData, hashAlg); // 加签 return sign;}RSA 验签
let result = verifySign(textDec, sign); // true
// 验签函数function verifySign(standData, signData) { try { let signPublicKey = publicKey; let sig = new KJUR.crypto.Signature({ alg: "SHA1withRSA", prvkeypem: signPublicKey, }); sig.updateString(standData); let result = sig.verify(signData); return result; } catch (e) { console.error(e); }}在 vue 项目中使用加解密
安装各加密模块
npm install crypto-jsnpm install jsencryptnpm install jsrsasignimport AESCrypto from "crypto-js";import JSEncrypt from "jsencrypt";import Jsrasign from "jsrsasign";RSA 加解密对象需要初始化
var RSACrypto = new JSEncrypt();在加解签时需要修改
// 将 new KJUR.crypto.Signature 改为new Jsrasign.Signature();利用 Crypto 进行格式转换
字符串转其他格式
let str = "abc";let str_utf8 = CryptoJS.enc.Utf8.parse(str);
// 转为 hexlet str_hex = CryptoJS.enc.Hex.stringify(str_utf8); // '616263'
// 转为 base64let str_base64 = CryptoJS.enc.Base64.stringify(str_utf8); // "YWJj"hex 转为其他格式
let hex = "616263";let hex_hex = CryptoJS.enc.Hex.parse(hex);
// 转为字符串hex_str = CryptoJS.enc.Utf8.stringify(hex_hex); // 'abc'
// 转为 base64let hex_base64 = CryptoJS.enc.Base64.stringify(hex_hex); // "YWJj"我遇到的一个问题
以上所写的都是客户端加密客户端解密,但是通常工作都是客户端加密,服务端解密,服务端加密,客户端解密。
当后端传回来的 keyEnc 为
6e1c9600284212f2425d9a490a1f4d9b2d0e2d8605691cc02debe2dcf7a4b6cc51f81534ed6df592cdb035e14d03b8276a90192b76ba51c240d8e26b55bb9ad195b1044fbe5ce444c3be2940f33b698df812020619c1e4b4071361732d8f0d6fe98c222467d64ce8f0a9f77a02eef22b57833e6474e5fc5cf4e00c97f2706fad将这个 16 进制字符串转为 base64,然后用RSACrypt.decrypt对它进行解密,得到的结果却是茁yˎѬ۽ܽ_뀅,然后代入 AES 解密得到的是乱码。
然后用 node 去解密得到是<Buffer b8 0c c1 79 cb 4e d1 ec db bd dc 7d 5f ab 80 05>,通过toString()得到的是,通过toString('hex')得到b80cc179cb4ed1ecdbbddc7d5fab8005,与茁yˎѬ۽ܽ_뀅转为的 16 进制并不一样。
将这个 16 进制字符串带入 AES 解密,得到了正确的解密结果。
这样看来,node 解密的结果是正确的,难道 js 的解密方法不对吗?
AES 解密 如果 key 是 16 进制的话
// 将 let key = CryptoJS.enc.Utf8.parse(key) 改为let key = CryptoJS.enc.Hex.parse(key);然后查看源码的时候发现在pkcs1unpad2函数里有 utf-8 decode,仔细研究发现这个
b80cc179cb4ed1ecdbbddc7d5fab8005不是一个 utf-8 编码的字符串,因为根据utf-8 的编码规则,没有10110110开头的,但是源码把它当作 utf-8 的 16 进制解析,所以与 node 结果不一致。
那我能不能不让它 decode,在 decode 之前就直接得到它
function pkcs1unpad2(d, n) { // ... var array = []; while (++i < b.length) { var c = b[i] & 255; array.push(c); } array = array .map((value) => value.toString(16).length === 1 ? 0 + value.toString(16) : value.toString(16) ) .join("");
return array;}然后这里得到跟 node 解密一样的 16 进制字符串,问题已解决。
因为后台进行 RSA 加密的时候,key 并不一定就是字符串,也有可能就是非 utf-8 编码的 16 进制的 Buffer。比如:
var key_utf16 = "b80cc179cb4ed1ecdbbddc7d5fab8005";var key = Buffer.from(key_utf16, "hex");这时我们再用 utf-8 解码的时候,会得到错误的结果。