Appearance
Crypto Module
The purpose of the crypto
module is to provide common encryption and hashing algorithms. Implementing these functions purely in JavaScript is possible, but it would be very slow. Node.js implements these algorithms in C/C++, exposing them as JavaScript interfaces through the crypto
module, making them convenient and fast to use.
MD5 and SHA1
MD5 is a commonly used hashing algorithm that generates a "signature" for any data. This signature is typically represented as a hexadecimal string:
javascript
import crypto from 'node:crypto';
const hash = crypto.createHash('md5');
// Can call update() any number of times:
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
The update()
method defaults to UTF-8 string encoding but can also take a Buffer.
To compute SHA1, simply replace 'md5'
with 'sha1'
, and you can also use more secure options like sha256
and sha512
.
HMAC
HMAC is also a hashing algorithm, but it uses a key along with hashing algorithms like MD5 or SHA1:
javascript
import crypto from 'node:crypto';
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex')); // 80f7e22570...
If the key changes, the same input data will produce a different signature, making HMAC a hashing algorithm "enhanced" with randomness.
AES
AES is a commonly used symmetric encryption algorithm where the same key is used for both encryption and decryption. The crypto
module provides support for AES, but you need to wrap the functions for convenience:
javascript
import crypto from 'node:crypto';
function aes_encrypt(key, iv, msg) {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
// input encoding: utf8
// output encoding: hex
let encrypted = cipher.update(msg, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function aes_decrypt(key, iv, encrypted) {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Key length must be 32 bytes:
let key = 'Passw0rdPassw0rdPassw0rdPassw0rd';
// IV length must be 16 bytes:
let iv = 'a1b2c3d4e5f6g7h8';
let msg = 'Hello, world!';
// Encrypt:
let encrypted_msg = aes_encrypt(key, iv, msg);
// Decrypt:
let decrypted_msg = aes_decrypt(key, iv, encrypted_msg);
console.log(`AES encrypt: ${encrypted_msg}`);
console.log(`AES decrypt: ${decrypted_msg}`);
The output will look like this:
AES encrypt: 11cd65e5fe7e7448b491efabee2f326a
AES decrypt: Hello, world!
As you can see, the encrypted string was decrypted back to the original content.
Note that there are various AES algorithms like aes192
, aes-128-ecb
, aes-256-cbc
, etc. Besides the key, AES can also specify an IV (Initial Vector). As long as the IV differs, the same key encrypting the same data will yield different results. The encrypted result is usually represented in either hex
or base64
, and Node.js supports all of these. However, when applications use different languages (e.g., Node.js and Java or PHP), careful testing is needed. If decryption fails, verify that both parties are using the same AES algorithm, that the keys and IVs are identical, and that the encrypted data is consistently formatted as either hex
or base64
.
Diffie-Hellman
The DH algorithm is a key exchange protocol that allows two parties to negotiate a shared key without revealing it. The DH algorithm is based on mathematical principles. For instance, if Xiao Ming and Xiao Hong want to negotiate a key, they can do the following:
- Xiao Ming selects a prime number and a base, for example, prime
p=97
, baseg=5
(the base is a primitive root ofp
), and then selects a secret integera=123
to calculateA=g^a mod p=34
, and tells Xiao Hong:p=97
,g=5
,A=34
. - Xiao Hong receives
p
,g
, andA
and selects a secret integerb=456
to calculateB=g^b mod p=75
, and tells Xiao Ming:B=75
. - Xiao Ming computes
s=B^a mod p=22
, while Xiao Hong computess=A^b mod p=22
. Therefore, the shared keys
is22
.
In this process, the key 22
is neither revealed by Xiao Ming nor Xiao Hong; both compute it collaboratively. A third party can only know p=97
, g=5
, A=34
, and B=75
, but without knowing the secret integers a=123
and b=456
, they cannot compute the key 22
.
Implementing the DH algorithm using the crypto
module is as follows:
javascript
import crypto from 'node:crypto';
// Xiao Ming's keys:
let ming = crypto.createDiffieHellman(512);
let ming_keys = ming.generateKeys();
let prime = ming.getPrime();
let generator = ming.getGenerator();
console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));
// Xiao Hong's keys:
let hong = crypto.createDiffieHellman(prime, generator);
let hong_keys = hong.generateKeys();
// Exchange and generate secret:
let ming_secret = ming.computeSecret(hong_keys);
let hong_secret = hong.computeSecret(ming_keys);
// Print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));
The output will look like this:
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be
Note that the output is different each time because the selection of the prime is random.
RSA
RSA is an asymmetric encryption algorithm that consists of a private key and a public key. It can encrypt with the private key and decrypt with the public key, or vice versa. The public key can be shared, while the private key must remain secret.
The RSA algorithm was jointly proposed in 1977 by Ron Rivest, Adi Shamir, and Leonard Adleman, hence the acronym RSA.
When Xiao Ming sends a message to Xiao Hong, he can encrypt it with his private key, and Xiao Hong can decrypt it with Xiao Ming's public key. Alternatively, Xiao Ming can encrypt it with Xiao Hong's public key, and she can decrypt it with her private key. This is asymmetric encryption. Compared to symmetric encryption, asymmetric encryption only requires each individual to hold their own private key while publicly sharing their public key, without needing to share a common key like AES.
Before using RSA encryption in Node.js, we first need to prepare the private and public keys.
- First, execute the following command in the command line to generate an RSA key pair:
bash
openssl genrsa -aes256 -out rsa-key.pem 2048
Follow the prompts to enter a password. This password encrypts the RSA key using AES256, and the generated RSA key length is 2048 bits. Upon successful execution, you will obtain the encrypted rsa-key.pem
file.
- Next, using the above
rsa-key.pem
encrypted file, we can export the raw private key with the following command:
bash
openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
Enter the password from step 1 to obtain the decrypted private key.
Similarly, we can use the following command to export the raw public key:
bash
openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem
Now we have the raw private key file rsa-prv.pem
and the raw public key file rsa-pub.pem
, both in PEM format.
Now, we can use the methods provided by the crypto
module to perform asymmetric encryption and decryption.
First, we encrypt with the private key and decrypt with the public key:
javascript
import fs from 'node:fs';
import crypto from 'node:crypto';
// Load key from file:
function loadKey(file) {
// Key is essentially a PEM-encoded string:
return fs.readFileSync(file, 'utf8');
}
let
prvKey = loadKey('./rsa-prv.pem'),
pubKey = loadKey('./rsa-pub.pem'),
message = 'Hello, world!';
// Encrypt using the private key:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log(enc_by_prv.toString('hex'));
let
dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log(dec_by_pub.toString('utf8'));
After execution, you will see the decrypted message is the same as the original message.
Next, we encrypt with the public key and decrypt with the private key:
javascript
// Encrypt using the public key:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log(enc_by_pub.toString('hex'));
// Decrypt using the private key:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log(dec_by_prv.toString('utf8'));
The decrypted message will again be the same as the original.
If we increase the length of the message
string to something large (e.g., 1MB), we might encounter an error like data too large for key size
. This is because the original information encrypted with RSA must be smaller than the key length. How can we encrypt a long message with RSA? In practice, RSA is not suitable for large data encryption. Instead, we can generate a random AES password, use AES to encrypt the original message, and then use RSA to encrypt the AES password. Thus, when using RSA, we send two parts: the AES-encrypted ciphertext and the RSA-encrypted AES password. The recipient first decrypts the AES password with RSA and then uses it to decrypt the ciphertext, obtaining the plaintext.
Certificates
The crypto
module can also handle digital certificates, which are commonly used in SSL connections, i.e., HTTPS connections. Generally, HTTPS connections only need to handle server-side authentication. Unless there are special requirements (e.g., acting as a root certificate authority issuing certificates), it is recommended to use a reverse proxy server like Nginx or another web server to manage certificates.