基于 NodeJs 实现结合 RSA 和 AES 加密算法的消息交换加密传输

2,996次阅读
2 条评论

共计 9112 个字符,预计需要花费 23 分钟才能阅读完成。

提醒:本文最后更新于2025-07-07 14:43,文中所关联的信息可能已发生改变,请知悉!

敏感消息交换对正确性和安全性都有比较强的要求。

使用消息摘要算法对消息体计算和验证摘要,可以防止消息传输过程中被篡改为非法消息值;使用加密算法加密消息体,可以防止消息传输过程中被拦截并读取。二者结合则可以实现较强的安全性消息交换。

1 保证消息交换正确性

消息传输过程中可能被中间人篡改。比如 A 发送消息转账给 B,中间人在消息转发过程中进行了拦截,解密消息体并篡改为转账给 C,而且篡改转账额度,由此造成可能不可逆转的损失。

消息摘要可以保证消息传输的正确性。主要思路比较简单:对要发送对消息体使用消息摘要算法(如 md5、sha256、sha1、hashMAC 等)计算出摘要值,接收方收到消息后,使用相同的摘要算法对消息体计算摘要值,将其与接收到的摘要值比对验证是否一致。

该过程中需要注意的重点是要保障摘要算法的保密性。

2 消息交换加密传输

对称加密算法使用共同的密文对消息体加密,其特点是速度快效率高,缺点是需发送和接收方共享秘钥,任何一方泄露密文和加密算法则加密失效。非对称加密算法采用公私钥对,发送方使用公钥对消息体加密、接收方使用私钥对消息体解密(反之亦可),接收方只需保管好自己独有的秘钥,即可保障加密消息不被泄露。但其加密速度慢效率低,故一般不用其进行大消息体的加密。

将对称加密算法和非对称加密算法结合使用,则可以达到相对可行有效的消息交换加密传输。下文中非对称加密算法以 RSA 为例,对称加密算法以 AES 为例。

主要思路为:通过 RSA 算法加密 AES 算法使用的密钥,通过 AES 算法加密消息体,然后将加密的密钥和消息体发送给对方;对方收到消息后,使用 RSA 算法解密 AES 密钥,用解密后的密钥解密消息体,由此得到解密后的消息;接收方在回复消息的过程中重复类似过程,由此实现消息交换加密传输。

我们以客户端与服务端消息通信为例,详细流程概述如下:

准备:

  1. 客户端生成本地的 RSA 公私钥 clientPublicKey 和 clientPrivateKey
  2. 服务端生成远端的 RSA 公私钥 remotePublicKey 和 remotePublicKey

消息交换流程:

  1. 公钥交换:客户端发起请求,发送本地的 RSA 公钥 localPublicKey,并获取服务端的 RSA 公钥 remotePublicKey,用于加密对称算法(AES)的密钥
  2. 客户端生成随机的 16 位字符 aesKey,用于对称算法(AES)的密钥
  3. 客户端使用从服务器获取的公钥 remotePublicKey 对 aesKey 进行 RSA 加密,得到加密的值 aesKeyEncrypted
  4. 客户端使用 aesKey 对要发送的消息体 body 加密,得到 bodyAesEncrypted
  5. 客户端使用 http post 方法发送消息体 {aesKeyEncrypted, bodyAesEncrypted }。其中 localPublicKey 用于服务器加密返回的消息
  6. 服务端接收到消息体后,使用其 RSA 私钥 remotePrivateKey 对 aesKeyEncrypted 解密得到 AES 算法密钥 aesKey
  7. 服务端使用 AES 密钥 aesKey 对 bodyAesEncrypted 解密,得到 body。至此客户端到服务端的加密消息传输完毕
  8. 服务端使用 localPublicKey 重复上述 1-5 过程对回复消息加密并发送,客户端重复上述 6-7 过程对接收到的消息解密

通过以上流程,使得整个传输过程中的消息都是加密无法被读取的,当然前提是需保证双方的 RSA 秘密未被泄露。
此外,如果加上消息摘要算法对消息体签名,中间人不知道摘要算法则无法伪造有效的消息。所以结合消息摘要算法和消息加密算法,则可以进一步增强消息传递过程。当然,在对等双方的消息传递中,对方不一定是可信的,还是存在摘要算法泄露的风险。

上述流程可以保证消息的安全性,但无法保证其真实性,因为中间人也可以不解密消息,使用拦截到的公钥加密消息,发送伪造的消息体(伪造消息体和发送方公钥)。
解决办法则是引入第三方权威中间人机构,消息接收方在收到消息时,先通过中间人机构对来自消息发送方的公钥进行可靠性认证。

上述流程默认客户端与服务端为对等的双方。其实该流程也可以进行简化:客户端不维护本地 RSA 公私钥,但缓存随机生成的 RES 秘钥;服务端在得到消息后,回复消息时使用相同的 RES 算法和解密得到的客户端 RES 秘钥加密消息体,客户端收到消息后使用 RES 秘钥解密。其实简化流程中再加上颁发证书的第三方权威机构认证流程,就是典型的 HTTPS 消息加密传输的基本原理。

3 基于 nodejs 的消息交换加密传输示例

3.1 生成非对称加密 RSA 算法公私钥

使用 crypto 模块:

import crypto from 'crypto';
/** 使用 crypto 模块生成 RSA 公私钥 */
export function genRsaKeyByCrypto(options?: crypto.RSAKeyPairOptions<"pem", "pem">) {
options = {
modulusLength: 1024,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: '',
},
...options,
};
const result = crypto.generateKeyPairSync('rsa', options);
return result;
}

使用 node-rsa 库:

import NodeRSA from 'node-rsa';
export function genRsaKeyPairByNodeRsa() {
const key = new NodeRSA({ b: 1024 });
key.setOptions({encryptionScheme: 'pkcs1'});
return { publicKey: key.exportKey('public'), privateKey: key.exportKey('private') };
}

生成并保存 RSA 公私钥:

export function saveRsaKey(publicKeyPath='public.pem', privateKeyPath='private.pem', isForce = false) {
publicKeyPath = path.resolve(publicKeyPath);
privateKeyPath = path.resolve(privateKeyPath);
if (existsSync(publicKeyPath) && !isForce) {
// console.log('公钥文件已存在');
return { publicKey: readFileSync(publicKeyPath), privateKey: readFileSync(privateKeyPath) };
} else {
console.log('重新生成公私钥文件');
}
// const { publicKey, privateKey } = await genRsaKeyByCrypto();
const { publicKey, privateKey } = genRsaKeyPairByNodeRsa();
if (!existsSync(path.dirname(publicKeyPath))) mkdirSync(path.dirname(publicKeyPath), {recursive: true});
writeFileSync(publicKeyPath, publicKey);
console.log('写入公钥文件:', publicKeyPath);
writeFileSync(privateKeyPath, privateKey);
console.log('写入私钥文件:', privateKeyPath);
return { publicKey, privateKey };
}

3.2 RSA 算法加密与解密

使用 Node.js 自带的 crypto 模块示例:

/**
* RSA最大加密明文大小
*/
const MAX_ENCRYPT_BLOCK = 117 - 31;
/**
* RSA最大解密密文大小
*/
const MAX_DECRYPT_BLOCK = 128;
/**
* rsa 公钥加密
*/
export function publicEncrypt(data, publicKey, outputEncoding: BufferEncoding = 'base64') {
// 加密信息用buf封装
const buf = Buffer.from(data, 'utf-8');
const inputLen = buf.byteLength;
const bufs = [];
let offSet = 0;
let endOffSet = MAX_ENCRYPT_BLOCK;
// 分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
const bufTmp = buf.slice(offSet, endOffSet);
bufs.push(crypto.publicEncrypt({key: publicKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
} else {
const bufTmp = buf.slice(offSet, inputLen);
bufs.push(crypto.publicEncrypt({key: publicKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
}
offSet += MAX_ENCRYPT_BLOCK;
endOffSet += MAX_ENCRYPT_BLOCK;
}
const result = Buffer.concat(bufs).toString(outputEncoding);
return result;
}
/**
* rsa 私钥解密
*/
export function privateDecrypt(data, privateKey, inputEncoding: BufferEncoding = 'base64') {
// 经过base64编码的密文转成buf
const buf = data instanceof Buffer ? data : Buffer.from(data, inputEncoding);
const inputLen = buf.byteLength;
const bufs = [];
let offSet = 0;
let endOffSet = MAX_DECRYPT_BLOCK;
// 分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
const bufTmp = buf.slice(offSet, endOffSet);
bufs.push(crypto.privateDecrypt({key: privateKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
} else {
const bufTmp = buf.slice(offSet, inputLen);
bufs.push(crypto.privateDecrypt({key: privateKey, passphrase: '', padding: crypto.constants.RSA_PKCS1_PADDING}, bufTmp));
}
offSet += MAX_DECRYPT_BLOCK;
endOffSet += MAX_DECRYPT_BLOCK;
}
const result = Buffer.concat(bufs).toString();
return result;
}

使用 node-rsa 库:

import NodeRSA from 'node-rsa';
/**
* rsa 公钥加密
*/
export function rsaEncrypt(data, publicKey, outputEncoding = 'base64') {
const key = new NodeRSA({ b: 1024 });
key.importKey(publicKey, 'public');
let encryData = key.encrypt(data, outputEncoding, 'utf8');
if (outputEncoding === 'hex' || outputEncoding === 'binary') encryData = Buffer.from(encryData, outputEncoding);
return encryData;
}
/**
* rsa 私钥解密
*/
export function rsaDecrypt(data, privateKey) {
const key = new NodeRSA({ b: 1024 });
key.importKey(privateKey, 'private');
const decryptData = key.decrypt(data, 'utf8');
return decryptData;
}

3.3 AES 算法加密与解密

AES 加密与解密相对比较简单,只需要传入对应的 data 数据与解密密文即可。

使用 Node.js 自带的 crypto 模块示例:

import crypto from 'crypto';
/** aes 加密 */
export aesEncrypt(data, passKey, outputEncoding = 'base64') {
if (typeof data !== 'string') data = JSON.stringify(data);
const cipherChunks = [];
// const key = Buffer.from(passKey, 'utf8');
// 对原始秘钥点加盐
const key = crypto.scryptSync(passKey, 'salt', 16);
const iv = key; // Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
cipher.setAutoPadding(true);
cipherChunks.push(cipher.update(data, 'utf8', outputEncoding as any));
cipherChunks.push(cipher.final(outputEncoding as any));
return cipherChunks.join('');
}
/** aes 解密 */
export aesDecrypt(data, passKey, inputEncoding = 'base64') {
const cipherChunks = [];
// const key = Buffer.from(passKey, 'utf8');
const key = crypto.scryptSync(passKey, 'salt', 16);
const iv = key; // Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(true);
cipherChunks.push(decipher.update(data, inputEncoding as any, 'utf8'));
cipherChunks.push(decipher.final('utf8'));
return cipherChunks.join('');
}

使用 crypto-js 模块示例:

import CryptoJS from 'crypto-js';
const key = CryptoJS.enc.Utf8.parse("1234123412ABCDEF"); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412'); //十六位十六进制数作为密钥偏移量
//解密方法
function Decrypt(data, passKey, iv?) {
if (!iv) iv = passKey;
passKey = CryptoJS.enc.Utf8.parse(passKey);
iv = CryptoJS.enc.Utf8.parse(iv);
const hex = CryptoJS.enc.Hex.parse(data);
const base64 = CryptoJS.enc.Base64.stringify(hex);
const decrypt = CryptoJS.AES.decrypt(base64, passKey, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return decrypt.toString(CryptoJS.enc.Utf8);
}
//加密方法
function Encrypt(data, passKey, iv?) {
if (!iv) iv = passKey;
passKey = CryptoJS.enc.Utf8.parse(passKey);
iv = CryptoJS.enc.Utf8.parse(iv);
const utf8 = CryptoJS.enc.Utf8.parse(data);
const encrypted = CryptoJS.AES.encrypt(utf8, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
return encrypted.ciphertext.toString().toUpperCase();
}

3.4 结合 AES 和 RSA 加密算法对消息体加密与解密

数据加密与解密实现:

/** 生成指定长度的字符串 */
export function genRandomAesKey(len = 16) {
// return crypto.randomBytes(len).toString('utf-8');
const result = [];
for (let i = 0; i < len; i++) {
let code = Math.round(Math.random() * 126);
if (code < 33) code += 32;
result.push( String.fromCharCode(code));
}
return result.join('');
}
/** (使用对方的公钥)数据加密,返回加密后的 key、data 以及本地公钥(用于给对方加密回据使用) */
export function dataEncrypt(data, remotePublicKey) {
const aesKey = genRandomAesKey();
// const localPublicKey = fs.readFileSync(config.publicKeyPath, {encoding: 'utf-8'});
// if (!remotePublicKey) remotePublicKey = localPublicKey;
const aesKeyEncrypted = rsaEncrypt(aesKey, remotePublicKey);
const encryptedData = aesEncrypt(data, aesKey);
return {key: aesKeyEncrypted, data: encryptedData, publicKey: localPublicKey};
}
export function dataDecrypt(encryptedData, aesKeyEncrypted, localPrivateKey) {
// if (!localPrivateKey) localPrivateKey = fs.readFileSync(config.privateKeyPath, {encoding: 'utf-8'});
const aesKey = rsaDecrypt(aesKeyEncrypted, localPrivateKey);
const result = aesDecrypt(encryptedData, aesKey);
try {
return JSON.parse(result);
} catch (_e) {
return result;
}
}

消息发送方数据加密与接收方数据解密:

// 发送方加密发送消息
const data = { user: 'lzwme', pwd: '123456' };
const remotePublicKey = '...';
const encryptedInfo = dataEncrypt(data);
console.log('数据加密:', encryptedInfo);
// 接受方收到消息体后解密消息
const localPrivateKey = '...';
const deCryptedInfo = dataDecrypt(encryptedInfo.key, encryptedInfo.data, localPrivateKey);
console.log('数据解密:', deCryptedInfo);

4 总结

本文主要介绍了结合使用对称加密算法(AES)和非对称加密算法(RSA)对消息交换双方消息加密传输的典型方案,并使用 nodejs 代码演示了主要流程的实现方式。在基于的浏览器/服务器(B/S)的服务中,浏览器端没有 nodejs 接口能力,可借助第三方库 crypto-jsjsencrypt 等提供的 API 实现相关加密和解密算法。

5 相关参考

正文完
 0
任侠
版权声明:本站原创文章,由 任侠 于2020-12-27发表,共计9112字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(2 条评论)
验证码
路人甲6655 评论达人 LV.1
2021-02-24 11:12:33 回复
Google Chrome 88.0.4324.192 Google Chrome 88.0.4324.192 Mac OS X 11.1.0 Mac OS X 11.1.0

NodeJs 在 高频率调用 RSA 的加解密下 性能消耗有点大

 Macintosh  Chrome  中国河北省廊坊市联通
王光卫博客 评论达人 LV.1
2021-01-01 15:41:07 回复
Firefox 84.0 Firefox 84.0 Mac OS X 10.16 Mac OS X 10.16

向博主学习,顺祝元旦节快乐!

 Macintosh  Firefox  中国四川省德阳市电信