Sensitive message exchange has relatively strong requirements for correctness and security.

Using a message digest algorithm to calculate and verify the digest of the message body prevents the message from being tampered with during transmission as an illegal message value; using an encryption algorithm to encrypt the message body prevents the message from being intercepted and read during transmission. The combination of the two can achieve a strong secure message exchange.

Ensure correct message exchange

The message may be tampered with by a man-in-the-middle during transmission. For example, if A sends a message to transfer money to B, a man-in-the-middle intercepts the message during transmission, decrypts the message body and tampers with it to transfer money to C, and tampers with the amount of the transfer, resulting in potentially irreversible damage.

Message digest ensures the correctness of the message transmission. The main idea is relatively simple: a message digest algorithm (such as md5, sha256, sha1, hashMAC, etc.) is used to calculate the digest value for the message body to be sent, and the receiver receives the message, uses the same digest algorithm to calculate the digest value for the message body, and compares it with the received digest value to verify if it is consistent.

The key point to note in this process is to guarantee the confidentiality of the digest algorithm.

Encrypted transmission for message exchange

The symmetric encryption algorithm uses a common ciphertext to encrypt the message body, which is characterized by high speed and efficiency, but the disadvantage is that the secret key needs to be shared between the sender and the receiver, and the encryption will be invalid if either party leaks the ciphertext and the encryption algorithm. Asymmetric encryption algorithm uses public-private key pair, the sender uses the public key to encrypt the message body, the receiver uses the private key to decrypt the message body (or vice versa), and the receiver only needs to keep his own unique secret key to protect the encrypted message from being leaked. However, its encryption speed is slow and inefficient, so it is generally not used for encrypting large message bodies.

The combination of symmetric encryption algorithm and asymmetric encryption algorithm can achieve relatively feasible and effective message exchange and encryption transmission. In the following, RSA is used as an example of asymmetric encryption algorithm and AES is used as an example of symmetric encryption algorithm.

The main idea is: encrypt the key used in AES algorithm by RSA algorithm, encrypt the message body by AES algorithm, and then send the encrypted key and message body to the other party; after the other party receives the message, decrypt the AES key by RSA algorithm, decrypt the message body with the decrypted key, and get the decrypted message; the receiver repeats the similar process in the process of replying the message, thus realizing the message exchange and encrypted transmission.

Let’s take the client-server message communication as an example, the detailed process is outlined as follows.

Preparation:

  1. The client generates the local RSA public-private key clientPublicKey and clientPrivateKey.
  2. the server generates the remote RSA public-private key remotePublicKey and remotePublicKey

Message exchange process.

  1. public key exchange: the client initiates the request, sends the local RSA public key localPublicKey, and obtains the server’s RSA public key remotePublicKey, which is used to encrypt the key of the symmetric algorithm (AES)
  2. the client generates a random 16-bit character aesKey, which is used as the key of the symmetric algorithm (AES)
  3. the client uses the public key remotePublicKey obtained from the server to RSA encrypt the aesKey and obtains the encrypted value aesKeyEncrypted
  4. the client encrypts the body of the message to be sent with the aesKey, and gets the bodyAesEncrypted
  5. The client sends the message body {aesKeyEncrypted, bodyAesEncrypted } using http post method. The localPublicKey is used to encrypt the message returned by the server.
  6. the server receives the message body and decrypts the aesKeyEncrypted with its RSA private key remotePrivateKey to get the AES algorithm key aesKey
  7. the server decrypts the bodyAesEncrypted with the AES key aesKey to get the body, and the encrypted message from the client to the server is transmitted.
  8. The server uses localPublicKey to repeat the above 1-5 process to encrypt and send the reply message, and the client repeats the above 6-7 process to decrypt the received message.

Through the above process, the messages are encrypted and unreadable throughout the transmission, provided that the RSA secrets of both parties are not compromised. In addition, if the message digest algorithm is added to sign the message body, the intermediary cannot forge a valid message without knowing the digest algorithm. Therefore, combining the message digest algorithm and the message encryption algorithm can further enhance the message delivery process. Of course, in peer-to-peer messaging, the other party is not necessarily trusted, and there is still a risk of digest algorithm leakage.

The above process can guarantee the security of the message, but not its authenticity, because the middleman can also send a forged message body (forged message body and sender’s public key) without decrypting the message and using the intercepted public key to encrypt the message. The solution is to introduce a third-party authoritative middleman organization, so that the message receiver can first authenticate the public key from the message sender through the middleman organization when receiving the message.

The above process defaults to a peer-to-peer relationship between the client and the server. In fact, the process can be simplified: the client does not maintain the local RSA public-private key, but caches the randomly generated RES secret key; the server, after getting the message, replies to the message using the same RES algorithm and decrypts the client RES secret key to encrypt the message body, and the client receives the message and decrypts it using the RES secret key. In fact, the simplified process plus the authentication process of the third-party authority that issues the certificate is the basic principle of typical HTTPS message encryption transmission.

Example of encrypted transmission for nodejs-based message exchange

Generate asymmetric encryption RSA algorithm public and private keys

Using the crypto module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

Using the node-rsa library.

1
2
3
4
5
6
7
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') };
}

Generate and save RSA public and private keys.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 };
}

RSA Algorithm Encryption and Decryption

Example of using the crypto module that comes with Node.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
 * 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;
}

Using the node-rsa library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

AES algorithm encryption and decryption

AES encryption and decryption is relatively simple, just pass in the corresponding data and decryption text.

Example of using the crypto module that comes with Node.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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('');
}

Combine AES and RSA encryption algorithms to encrypt and decrypt message bodies

Data encryption and decryption implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** 生成指定长度的字符串 */
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;
    }
}

Encryption of message sender’s data and decryption of receiver’s data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 发送方加密发送消息
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);

Summary

This article introduces a typical scheme that combines the use of symmetric encryption algorithm (AES) and asymmetric encryption algorithm (RSA) to encrypt the transmission of messages on both sides of the message exchange, and demonstrates the implementation of the main processes using nodejs code. In a browser/server (B/S) based service, there is no nodejs interface capability on the browser side, and the encryption and decryption algorithms can be implemented with the help of APIs provided by third-party libraries such as crypto-js and jsencrypt.