Hashing, Encryption, and Secure Randomness with Node.js
Security is not a feature you add at the end it is a foundation you build from the start. For Node.js developers, the native node:crypto module is the primary toolkit for implementing cryptographic operations without reaching for a single external dependency. It is battle-tested, actively maintained as part of Node.js LTS, and exposes the full power of OpenSSL through a clean JavaScript API.
This article walks through three pillars of practical cryptography: hashing data with digest algorithms, encrypting and decrypting content with symmetric ciphers, and generating cryptographically secure random values. By the end, you will understand not just how to use each API, but when and why to use it.
All examples in this article require only Node.js LTS (v20 or v22). No external packages are needed.
Hashing Data with crypto.createHash
A hash function takes an arbitrary input and produces a fixed-length output called a digest. Hashing is one-way: you cannot reconstruct the original input from the digest. This makes it ideal for storing passwords (with a proper algorithm), verifying file integrity, and generating deterministic identifiers.
Node.js exposes hashing through crypto.createHash(algorithm), which returns a Hash object. You feed data into it with .update() and retrieve the digest with .digest(encoding).
// hash-example.js
import { createHash } from 'node:crypto';
const input = 'Hello, Node.js crypto!';
// SHA-256 digest in hexadecimal
const sha256Hex = createHash('sha256')
.update(input)
.digest('hex');
console.log('SHA-256 (hex): ', sha256Hex);
// SHA-256 digest in Base64
const sha256B64 = createHash('sha256')
.update(input)
.digest('base64');
console.log('SHA-256 (base64):', sha256B64);
// MD5 - fast, but NOT cryptographically secure for passwords
const md5Hex = createHash('md5')
.update(input)
.digest('hex');
console.log('MD5 (hex): ', md5Hex);
Expected output:
SHA-256 (hex): 2d6f831952e9c04c7d77ae5a6f1dce69dfce7f12f453c67fa6c4b9f6dcd5a4e1
SHA-256 (base64): LW+DGVLpwEx9d3rlbx3OaD/Of...
MD5 (hex): f0a732ca25e23e8c1c53af3a73bb26a4
SHA-256 is the recommended general-purpose algorithm for integrity checks and non-password hashing. MD5 is shown for completeness but should never be used for security-sensitive purposes. For passwords specifically, use crypto.scrypt or crypto.pbkdf2 instead, since those are deliberately slow and include salting. The .update() method can also be called multiple times, making it easy to hash streaming data incrementally without loading it all into memory at once.
Symmetric Encryption with createCipheriv and createDecipheriv
Encryption transforms readable data (plaintext) into unreadable data (ciphertext) using a key. Symmetric encryption uses the same key to encrypt and decrypt. AES-256-GCM is the recommended symmetric algorithm today: it is both an encryption cipher and an authenticated cipher, meaning it also protects against tampering.
The key API pair is crypto.createCipheriv(algorithm, key, iv) and crypto.createDecipheriv(algorithm, key, iv). The iv parameter is an Initialization Vector — a random value that ensures two identical plaintexts produce different ciphertexts, even under the same key.
// aes-gcm-example.js
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
const IV_LENGTH = 12; // 96 bits - GCM standard
function encrypt(plaintext, key) {
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return { iv, encrypted, authTag };
}
function decrypt(encrypted, key, iv, authTag) {
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
return decrypted.toString('utf8');
}
// Demo
const key = randomBytes(KEY_LENGTH);
const message = 'Confidential payload: order #4821';
const { iv, encrypted, authTag } = encrypt(message, key);
console.log('Encrypted (hex):', encrypted.toString('hex'));
const recovered = decrypt(encrypted, key, iv, authTag);
console.log('Decrypted: ', recovered);
// Tampering detection
try {
const tampered = Buffer.from(encrypted);
tampered[0] ^= 0xff;
decrypt(tampered, key, iv, authTag);
} catch (err) {
console.log('Tamper detected:', err.message);
}
Expected output:
Encrypted (hex): 3f9a1c... (varies with each run due to random IV)
Decrypted: Confidential payload: order #4821
Tamper detected: Unsupported state or unable to authenticate data
Three things to internalize from this example. First, never reuse an IV with the same key. Second, always store the IV alongside the ciphertext (it is not secret, it is just random). Third, always call setAuthTag before decrypting with GCM, and always handle the error it throws on authentication failure.
Cryptographically Secure Random Values with randomBytes and randomUUID
Standard Math.random() is a pseudo-random number generator seeded from a predictable source. It is fine for shuffling a UI list but entirely unsuitable for generating tokens, session IDs, or cryptographic keys. node:crypto provides two essential APIs for secure randomness: randomBytes and randomUUID.
crypto.randomBytes(size) returns a Buffer filled with cryptographically strong random bytes sourced from the OS entropy pool:
// random-bytes.js
import { randomBytes, randomUUID } from 'node:crypto';
// Generate a 32-byte secure token in URL-safe Base64
const token = randomBytes(32).toString('base64url');
console.log('Secure token:', token);
// Generate a 6-digit numeric OTP
const otpBuffer = randomBytes(4);
const otp = (otpBuffer.readUInt32BE(0) % 1_000_000).toString().padStart(6, '0');
console.log('OTP: ', otp);
// Generate a UUID v4
const id = randomUUID();
console.log('UUID v4: ', id);
Expected output:
Secure token: xK9mZ2TqVfRbLpN1uY8sAoEcWjHdGiUe (44 chars, varies each run)
OTP: 047382 (6 digits, varies each run)
UUID v4: 3f2d1a9e-74bc-4e1f-a305-8f3c2b1d6a90 (varies each run)
Use randomUUID() when you need a globally unique identifier for a record or resource. Use randomBytes() when you need raw random material for cryptographic keys, CSRF tokens, password reset tokens, or salts. Both APIs are backed by the OS entropy pool and are safe to use in security-sensitive contexts.
The node:crypto module covers the three operations that power the security of nearly every backend application: verifying integrity with hashing, protecting confidentiality with encryption, and generating unpredictable tokens with secure random functions. None of these require a third-party library. they are built into every Node.js LTS installation and backed by OpenSSL.
The key takeaways are practical rules you can apply immediately: use SHA-256 (not MD5) for integrity hashing; use AES-256-GCM (not AES-256-CBC) for symmetric encryption because it includes authentication; always generate a fresh random IV per encryption operation; and replace Math.random() with randomBytes or randomUUID any time security is a concern.
Cryptography in Node.js is accessible, but it rewards careful reading of the documentation. The official node:crypto reference at https://nodejs.org/api/crypto.html is the authoritative source for algorithm support, key length requirements, and deprecation notices bookmark it and consult it whenever you introduce a new cryptographic operation.
node:crypto was originally published in Server-Side JavaScript on Medium, where people are continuing the conversation by highlighting and responding to this story.