敏感消息交换对正确性和安全性都有比较强的要求。
使用消息摘要算法对消息体计算和验证摘要,可以防止消息传输过程中被篡改为非法消息值;使用加密算法加密消息体,可以防止消息传输过程中被拦截并读取。二者结合则可以实现较强的安全性消息交换。
1 保证消息交换正确性
消息传输过程中可能被中间人篡改。比如 A 发送消息转账给 B,中间人在消息转发过程中进行了拦截,解密消息体并篡改为转账给 C,而且篡改转账额度,由此造成可能不可逆转的损失。
消息摘要可以保证消息传输的正确性。主要思路比较简单:对要发送对消息体使用消息摘要算法(如 md5、sha256、sha1、hashMAC 等)计算出摘要值,接收方收到消息后,使用相同的摘要算法对消息体计算摘要值,将其与接收到的摘要值比对验证是否一致。
该过程中需要注意的重点是要保障摘要算法的保密性。
2 消息交换加密传输
对称加密算法使用共同的密文对消息体加密,其特点是速度快效率高,缺点是需发送和接收方共享秘钥,任何一方泄露密文和加密算法则加密失效。非对称加密算法采用公私钥对,发送方使用公钥对消息体加密、接收方使用私钥对消息体解密(反之亦可),接收方只需保管好自己独有的秘钥,即可保障加密消息不被泄露。但其加密速度慢效率低,故一般不用其进行大消息体的加密。
将对称加密算法和非对称加密算法结合使用,则可以达到相对可行有效的消息交换加密传输。下文中非对称加密算法以 RSA 为例,对称加密算法以 AES 为例。
主要思路为:通过 RSA 算法加密 AES 算法使用的密钥,通过 AES 算法加密消息体,然后将加密的密钥和消息体发送给对方;对方收到消息后,使用 RSA 算法解密 AES 密钥,用解密后的密钥解密消息体,由此得到解密后的消息;接收方在回复消息的过程中重复类似过程,由此实现消息交换加密传输。
我们以客户端与服务端消息通信为例,详细流程概述如下:
准备:
- 客户端生成本地的 RSA 公私钥 clientPublicKey 和 clientPrivateKey
- 服务端生成远端的 RSA 公私钥 remotePublicKey 和 remotePublicKey
消息交换流程:
- 公钥交换:客户端发起请求,发送本地的 RSA 公钥 localPublicKey,并获取服务端的 RSA 公钥 remotePublicKey,用于加密对称算法(AES)的密钥
- 客户端生成随机的 16 位字符 aesKey,用于对称算法(AES)的密钥
- 客户端使用从服务器获取的公钥 remotePublicKey 对 aesKey 进行 RSA 加密,得到加密的值 aesKeyEncrypted
- 客户端使用 aesKey 对要发送的消息体 body 加密,得到 bodyAesEncrypted
- 客户端使用 http post 方法发送消息体 {aesKeyEncrypted, bodyAesEncrypted }。其中 localPublicKey 用于服务器加密返回的消息
- 服务端接收到消息体后,使用其 RSA 私钥 remotePrivateKey 对 aesKeyEncrypted 解密得到 AES 算法密钥 aesKey
- 服务端使用 AES 密钥 aesKey 对 bodyAesEncrypted 解密,得到 body。至此客户端到服务端的加密消息传输完毕
- 服务端使用 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-js
、jsencrypt
等提供的 API 实现相关加密和解密算法。
NodeJs 在 高频率调用 RSA 的加解密下 性能消耗有点大
向博主学习,顺祝元旦节快乐!