Suggest an editImprove this articleRefine the answer for “How to use the Crypto module in Node.js for hashing and encryption?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Node.js `crypto` module** provides OpenSSL-backed hashing and encryption built into Node.js. Use `createHash` for one-way digests, `pbkdf2` or `scrypt` for passwords, and `createCipheriv` with AES-256-GCM for reversible encryption. ```js const hash = crypto.createHash('sha256').update('data').digest('hex'); const key = crypto.randomBytes(32); // 256-bit key, store in env const iv = crypto.randomBytes(16); // fresh IV every call ``` **Key:** hash passwords (you only need to verify them), encrypt secrets you need to retrieve.Shown above the full answer for quick recall.Answer (EN)Image**Node.js `crypto` module** gives you OpenSSL-backed hashing and encryption built directly into Node.js, no extra packages needed. ## Theory ### TL;DR - Hashing is one-way: data goes in, a fixed-size fingerprint comes out, no way back - Encryption is reversible: the same key (plus an IV) locks and unlocks the data - Analogy: hashing is a meat grinder (steak in, burger out, no reconstruction); encryption is a lockbox (same key opens it again) - For passwords: always hash with PBKDF2 or scrypt, never encrypt - `crypto.randomBytes()` is the only correct source of random values for security work in Node.js ### Quick example ```js const crypto = require('crypto'); // One-way hash - compare only, cannot decode const hash = crypto.createHash('sha256') .update('myPassword123') .digest('hex'); // 8d969eef6ecad3c29a3a629280e75283e6e8a794ea1949be2d6204e551b41d5a // Reversible encryption with AES-256-CBC const key = crypto.randomBytes(32); // 256-bit key const iv = crypto.randomBytes(16); // fresh IV every call const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update('sensitive data', 'utf8', 'hex'); encrypted += cipher.final('hex'); // Decrypt later: createDecipheriv('aes-256-cbc', key, iv) ``` `createHash` gives you a digest you can only compare. `createCipheriv` gives you data you can recover if you hold the key and IV. ### Key difference Hashing is a one-way transform. You store the digest, discard the original, and later verify by hashing the incoming value and comparing. That works for passwords because you never need to know what the original was. Encryption is a two-way transform. You can recover the original with the right key. That is what you want for tokens or session payloads you need to read back. The rule: hash what you will only ever need to verify; encrypt what you will need to retrieve. ### When to use - **Password storage**: `crypto.pbkdf2()` or `crypto.scrypt()` with a random 16-byte salt, store `salt:hash` - **Data integrity / checksums**: `createHash('sha256')` - **API tokens / session payloads**: AES-256-GCM with a key from an env variable - **Webhook signature verification**: HMAC-SHA256 with `timingSafeEqual()` - **Secure random tokens**: `crypto.randomBytes(32).toString('hex')` - **Unique identifiers**: `crypto.randomUUID()` Avoid MD5 and SHA-1 for passwords. Both are fast by design, which is exactly the problem. A modern GPU can compute around 10 billion MD5 hashes per second. ### Password hashing in depth Plain `createHash('sha256')` on a password has one real problem: two users with the same password get the same hash. An attacker with a precomputed rainbow table cracks both at once. A random salt stored alongside the hash fixes this. PBKDF2 runs HMAC thousands of times on purpose. 310,000 iterations is the current OWASP recommendation for SHA-512. That makes each guess expensive. `crypto.scrypt()` (available since Node 12) goes further: it is memory-hard, which makes GPU and ASIC attacks significantly harder because each guess requires a large block of RAM, not just CPU cycles. ### AES encryption in practice AES needs a **key** and an **IV** (initialization vector). The key is secret. The IV is not secret, but it must be unique for every encrypt call. If you reuse an IV with the same key, an attacker can XOR two ciphertexts and extract information about both plaintexts. AES-256-GCM is the better choice over AES-256-CBC for most use cases. GCM does encryption and authentication in one pass and produces a 16-byte auth tag. CBC only encrypts. If someone flips a bit in the ciphertext, CBC decryption produces wrong output with no error. GCM throws. I have seen teams spend days debugging corrupted session data before realizing they were using CBC with no integrity check. ### HMAC for message authentication `createHmac` is `createHash` plus a secret key. Plain `createHash` is vulnerable to length extension attacks if you try to use it as a MAC. `createHmac` is not. GitHub webhooks, Stripe event verification, and most webhook systems use HMAC-SHA256 for exactly this reason. Always use `crypto.timingSafeEqual()` when comparing HMACs. A plain `===` comparison short-circuits at the first mismatched byte. An attacker can measure response time differences to guess the expected value one byte at a time. ### Comparison table | Feature | Hashing (SHA-256 / PBKDF2) | Encryption (AES-256-GCM) | |---------|---------------------------|---------------------------| | Reversible? | No | Yes, with key + IV | | Output size | Fixed (64 hex chars for SHA-256) | Input size + small overhead | | Needs salt / IV? | Salt for passwords | IV always, fresh per call | | Auth built in? | No | Yes (16-byte GCM tag) | | Speed | Fast (PBKDF2 is slow by design) | Fast with AES-NI hardware | | Use case | Passwords, checksums | Tokens, secrets, session data | ### How Node.js handles this internally Node.js `crypto` binds OpenSSL through a native C++ layer. Hashing calls OpenSSL's SHA implementation directly. PBKDF2 and scrypt run on the libuv thread pool, so they do not block the event loop. AES gets hardware acceleration via AES-NI instructions on Intel and AMD. `crypto.randomBytes()` reads from the OS CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows). This is why `Math.random()` is disqualified for anything security-related: it is deterministic and seedable. ### Common mistakes **Mistake 1: Hashing passwords without salt** ```js // Same hash for every user with "password123" crypto.createHash('sha256').update('password123').digest('hex'); ``` Rainbow tables precompute millions of hashes. Without a random salt, one lookup exposes every user who shares the same password. Fix: store `salt:hash` where salt is `crypto.randomBytes(16).toString('hex')`. **Mistake 2: `pbkdf2Sync` on the main thread** ```js // Blocks the event loop for 100ms+ on every login crypto.pbkdf2Sync(password, salt, 310000, 64, 'sha512'); ``` Under any real load, this queues every other request behind each computation. Use async `crypto.pbkdf2()` with a callback. At 100 concurrent logins, the sync version is a denial-of-service gap. **Mistake 3: Reusing the same IV** ```js const iv = Buffer.alloc(16, 0); // fixed zero IV - do not do this ``` Reusing an IV with the same key breaks AES confidentiality. Generate `crypto.randomBytes(16)` fresh for every encrypt call and store the IV with the ciphertext. **Mistake 4: `createCipher` instead of `createCipheriv`** `createCipher` derives a weak IV from the key and has been deprecated since Node 10. Always use `createCipheriv` with an explicit random IV. **Mistake 5: `===` for hash comparison** ```js if (receivedHmac === expectedHmac) { ... } // timing attack ``` String comparison stops at the first mismatch. An attacker measures response time variance to reconstruct the expected value. Use `crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))`. ### Real-world usage - **Express + passport-local**: PBKDF2 for storing and verifying local user passwords - **Next.js**: AES-GCM for encrypting `__Host-` session cookies server-side - **Socket.io**: HMAC signs auth tokens before sending them to clients - **AWS SDK**: SHA-256 for S3 ETag verification on multipart uploads - **GitHub webhooks**: HMAC-SHA256 body signature in the `x-hub-signature-256` header ### Follow-up questions **Q:** What is the difference between `createHash` and `createHmac`? **A:** `createHash` is a plain digest with no key, vulnerable to length extension attacks. `createHmac` wraps the hash in HMAC(key, message), which requires the secret to produce a valid signature. Use `createHmac` for authentication, `createHash` for checksums. **Q:** Why use PBKDF2 instead of plain SHA-256 for passwords? **A:** SHA-256 is designed to be fast, which means billions of guesses per second on a GPU. PBKDF2 runs the hash 310,000+ times intentionally to make each guess slow. scrypt is better still because it is also memory-hard. **Q:** AES-256-CBC vs AES-256-GCM? **A:** CBC only encrypts, it does nothing to verify the ciphertext was not modified. GCM adds a 16-byte authentication tag and throws on tamper. Use GCM for new code. **Q:** How do I generate a cryptographically secure random token? **A:** `crypto.randomBytes(32).toString('hex')`. This uses the OS CSPRNG. `Math.random()` is not acceptable for security tokens. **Q (senior):** Streaming encryption of a 10 GB file ran out of memory. Why and how to fix it? **A:** The full encrypted output was being buffered in memory before writing. The fix is Node streams: `fs.createReadStream('file').pipe(crypto.createCipheriv(...)).pipe(fs.createWriteStream('file.enc'))`. Chunks flow through OpenSSL without loading the file into memory. Generate a fresh IV per file and prepend it to the output so decryption can read it back. ## Examples ### Async password hashing and verification ```js const crypto = require('crypto'); async function hashPassword(password) { const salt = crypto.randomBytes(16).toString('hex'); return new Promise((resolve, reject) => { // 310,000 iterations per OWASP recommendation for sha512 crypto.pbkdf2(password, salt, 310000, 64, 'sha512', (err, key) => { if (err) return reject(err); resolve(`${salt}:${key.toString('hex')}`); }); }); } async function verifyPassword(password, stored) { const [salt, hash] = stored.split(':'); return new Promise((resolve, reject) => { crypto.pbkdf2(password, salt, 310000, 64, 'sha512', (err, key) => { if (err) return reject(err); // timingSafeEqual prevents timing-based attacks resolve(crypto.timingSafeEqual(Buffer.from(hash, 'hex'), key)); }); }); } const stored = await hashPassword('hunter2'); console.log(await verifyPassword('hunter2', stored)); // true console.log(await verifyPassword('wrong', stored)); // false ``` The async version never blocks the event loop. On a server handling 100 concurrent logins, the sync version would stack every request behind each other's 100ms computation. ### AES-256-GCM encryption for session data ```js const crypto = require('crypto'); // Load from environment: openssl rand -hex 32 const SECRET_KEY = Buffer.from(process.env.SESSION_KEY, 'hex'); // 32 bytes function encryptSession(data) { const iv = crypto.randomBytes(16); // unique per call const cipher = crypto.createCipheriv('aes-256-gcm', SECRET_KEY, iv); let ciphertext = cipher.update(JSON.stringify(data), 'utf8', 'hex'); ciphertext += cipher.final('hex'); const tag = cipher.getAuthTag().toString('hex'); // 16-byte integrity tag return `${iv.toString('hex')}:${ciphertext}:${tag}`; } function decryptSession(token) { const [ivHex, ciphertext, tagHex] = token.split(':'); const decipher = crypto.createDecipheriv( 'aes-256-gcm', SECRET_KEY, Buffer.from(ivHex, 'hex') ); decipher.setAuthTag(Buffer.from(tagHex, 'hex')); // throws if tampered let plaintext = decipher.update(ciphertext, 'hex', 'utf8'); plaintext += decipher.final('utf8'); return JSON.parse(plaintext); } const token = encryptSession({ userId: 42, role: 'admin' }); console.log(decryptSession(token)); // { userId: 42, role: 'admin' } ``` The token format is `iv:ciphertext:authTag`. If anything in the ciphertext changes between encrypt and decrypt, `decipher.final()` throws before you ever parse the payload. ### HMAC webhook signature verification ```js const crypto = require('crypto'); const express = require('express'); const app = express(); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; function verifyWebhook(rawBody, signatureHeader) { const expected = 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('hex'); if (expected.length !== signatureHeader.length) return false; return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); } // express.raw() is required - you must hash raw bytes, not parsed JSON app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-hub-signature-256']; if (!sig || !verifyWebhook(req.body, sig)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body); // handle event... res.sendStatus(200); }); ``` A common mistake here is using `express.json()` instead of `express.raw()`. If the body is parsed first, you hash the re-serialized JSON object rather than the original bytes the sender signed. The signatures will never match.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.