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

目录
[隐藏]

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

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

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 相关参考

点赞 (1)
  1. 路人甲6655说道:
    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 的加解密下 性能消耗有点大

  2. 王光卫博客说道:
    Firefox 84.0 Firefox 84.0 Mac OS X  10.16 Mac OS X 10.16

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

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code