有些场景下需要前端做加密,比如登录的时候,用户输入的密码需要传输给后端,为了保证安全,最好前端先加密后传输,后端接收到之后,再解密拿到明文。 需要在不同端进行加密解密的话 RSA 非对称加密算法最适合。
一、RSA 简介
RSA公开密钥密码体制是一种使用不同的加密密钥与解密密钥,“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。 在公开密钥密码体制中,加密密钥(即公开密钥)PK是公开信息,而解密密钥(即秘密密钥)SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,但却不能根据PK计算出SK。 正是基于这种理论,1978年出现了著名的RSA算法,它通常是先生成一对RSA密钥,其中之一是保密密钥,由用户保存;另一个为公开密钥,可对外公开,甚至可在网络服务器中注册。为提高保密强度,RSA密钥至少为500位长。这就使加密的计算量很大。为减少计算量,在传送信息时,常采用传统加密方法与公开密钥加密方法相结合的方式,即信息采用改进的DES或IDEA对话密钥加密,然后使用RSA密钥加密对话密钥和信息摘要。对方收到信息后,用不同的密钥解密并可核对信息摘要。 RSA允许你选择公钥的大小。512位的密钥被视为不安全的;768位的密钥不用担心受到除了国家安全管理(NSA)外的其他事物的危害;RSA在一些主要产品内部都有嵌入,像 Windows、网景 Navigator、 Quicken和 Lotus Notes。
二、jsrsasign
RSA 加密的第三方库有很多,用的比较多的是 node-rsa 和 jsrsasign。但是 node-rsa 最近更新已经是三年前了,jsrsasign 更新的比较频繁,几天前才有更新,周下载量30万次,算是比较靠谱的。 jsrsasign 官方文档地址:https://kjur.github.io/jsrsasign/
三、openssl 生成公钥和私钥
加密解密需要用到 pem 格式的公钥和私钥,秘钥可以通过 openssl 自己生成。
openssl 生成私钥
打开终端,输入 openssl
回车之后就进入了 OpenSSL 命令行的交互。
生成私钥命令:
genrsa -out /your_path/rsa_pricate.pem 2048
-out
指定了输出的路径,最后的 2048 表示生成 2048 位的秘钥。
打开生成的 pem 文件,可以看到如下格式的内容。
代码语言:javascript复制-----BEGIN RSA PRIVATE KEY-----
xxxxxxxxxxxxx
-----END RSA PRIVATE KEY-----
根据私钥生成公钥
公钥是根据私钥生成的,继续在 OpenSSL 交互式命令行里输入以下命令
代码语言:javascript复制rsa -in /your_path/rsa_pricate.pem -pubout -out /your_path/rsa_public.pem
把 your_path
替换成你电脑中的真实路径即可。执行完后得到公钥:
-----BEGIN PUBLIC KEY-----
xxxxxxxxxxxxx
-----END PUBLIC KEY-----
四、前端加密
安装依赖
代码语言:javascript复制npm install jsrsasign jsrsasign-util
如果用了 TS,还需要安装对应的类型提示
代码语言:javascript复制npm i --save-dev @types/jsrsasign
然后对密码进行加密。
代码语言:javascript复制import { KJUR, KEYUTIL, RSAKey } from 'jsrsasign'
function encryptKey(password: string) {
const keyObj = KEYUTIL.getKey(publicKey);
const encryptPwd = KJUR.crypto.Cipher.encrypt(password, keyObj as RSAKey, 'RSAOAEP')
console.log('密文:', encryptPwd)
}
publicKey
就是上一节生成的公钥的文本,先通过 KEYUTIL.getKey(publicKey)
获取秘钥的对象,再通过 KJUR.crypto.Cipher.encrypt()
进行加密。
encrypt(s, keyObj, algName)
方法有三个参数:
- s: 要加密的文字
- keyObj: 通过 KEYUTIL 获取的公钥的对象
- algName: 加密的方法名
algName 有以下几种取值:
- RSA - RSA/ECB/PKCS1Padding (default for RSAKey)
- RSAOAEP - RSA/ECB/OAEPWithSHA-1AndMGF1Padding
- RSAOAEP224 - RSA/ECB/OAEPWithSHA-224AndMGF1Padding(*)
- RSAOAEP256 - RSA/ECB/OAEPWithSHA-256AndMGF1Padding
- RSAOAEP384 - RSA/ECB/OAEPWithSHA-384AndMGF1Padding(*)
- RSAOAEP512 - RSA/ECB/OAEPWithSHA-512AndMGF1Padding(*)
调用加密方法加密一串英文:encryptKey('Hello Javascript')
,得到密文如下:
密文: 9999d156ce02a130c51c3a94c1010aa8ead62499a373f51f57177894b8634baab62a968c12306c46a2aee8358bd224a0138c7d7c38678a56ed2f6537667b67bdf8b5b7fd555091283609b7077e7146a0cd0f3644f2ad1680bb9cf9ee104305b19153eef15950f9d2b3fdd0074ed420169e6cf63d8aa95aeda7a4dc28f201119eef1f6e354e7e18d43c27e528104fdfecf4363fdb676c96ef5ffc8ba1b99c1bff6955970bf71c4220a7f81e34080f854f43738b414d798c70232719e12d22398e8d36f414556c6dc297525e391be63a5542b2e15834c1870af1ab109c74cf2b094d778a4abc0e82ed43ad0664f2a09806c4d98df9d71e9338236e6e8f7115720e
五、node 端解密
node 端依然可以用 jsrsasign 来解密,跟前端一样安装依赖
代码语言:javascript复制npm install jsrsasign jsrsasign-util
如果用了 TS,还需要安装对应的类型提示
代码语言:javascript复制npm i --save-dev @types/jsrsasign
解密方法如下:
代码语言:javascript复制import { KEYUTIL, KJUR, RSAKey } from 'jsrsasign';
decryptKey(key: string) {
const keyObj = KEYUTIL.getKey(privateKey);
const name = KJUR.crypto.Cipher.decrypt(key, keyObj as RSAKey, 'RSAOAEP');
console.log('明文:', name);
}
privateKey
是第三节生成的私钥的文本,通过 KEYUTIL.getKey()
获取私钥的对象,然后再通过 KJUR.crypto.Cipher.decrypt()
进行解密。需要注意的是第三个参数 algName
要与前端加密时的方法一样。
输出:
明文: Hello Javascript
这样前端用公钥加密,node 端用私钥解密就完成了。
六、中文乱码问题解决
通过上面的方法,加密解密英文没问题,但加密中文解密出来会是乱码。
比如原文是 Javascript你好我是密码
,解密之后得到的是 Javascript}/Æ,后面的中文乱码了。<br />看网上的解决方法有些是修改解密方法,其实最简单的方法是在加密的时候,先用
encodeURI()对中文进行编码之后再进行加密,解密的时候,先解密再用
decodeURI()`将解密后的文本进行转码得到正确的中文。
完整的代码如下:
import { KEYUTIL, KJUR, RSAKey } from 'jsrsasign';
// 加密
function encryptKey(password: string) {
const keyObj = KEYUTIL.getKey(publicKey);
const encryptPwd = KJUR.crypto.Cipher.encrypt(encodeURI(password), keyObj as RSAKey, 'RSAOAEP')
console.log('密文:', encryptPwd)
}
// 解密
decryptKey(key: string) {
const keyObj = KEYUTIL.getKey(privateKey);
const name = decodeURI(
KJUR.crypto.Cipher.decrypt(key, keyObj as RSAKey, 'RSAOAEP') || '',
);
console.log('明文:', name);
}