Crypto Module¶
Node's crypto module is your Swiss Army knife for security Hashing , HMAC , encryption , decryption - it's all here built into the standard library with no extra dependencies You just need to know which functions to use and which to avoid (spoiler : avoid everything involving createDecipher without GCM)
Hashing with createHash¶
One-way hashing for integrity checks , not password storage
const crypto = require('crypto')
const hash = crypto.createHash('sha256')
hash.update('some data to hash')
const digest = hash.digest('hex')
// -> a1b2c3...
// Streaming interface for large files
const fs = require('fs')
const hashStream = crypto.createHash('sha256')
const input = fs.createReadStream('./large-file.bin')
input.pipe(hashStream).on('finish', () => {
const finalDigest = hashStream.digest('hex')
console.log('File hash:', finalDigest)
})
SHA-256 and SHA-512 are your defaults : MD5 and SHA-1 are for compatibility checks only , not security
HMAC for Integrity¶
Hash-based Message Authentication Code - proves data hasn't been tampered with
const secret = process.env.HMAC_SECRET || 'fallback-dev-only'
function signMessage(message) {
const hmac = crypto.createHmac('sha256', secret)
hmac.update(message)
return hmac.digest('hex')
}
function verifyMessage(message, signature) {
const expected = signMessage(message)
// Constant-time comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
)
}
const token = signMessage('userId=42&role=admin')
const valid = verifyMessage('userId=42&role=admin', token)
// -> true
Use crypto.timingSafeEqual for comparisons : never use === for HMAC or token verification - attackers can time the comparison
Password Hashing: pbkdf2 and scrypt¶
Node provides proper password stretching functions built-in
const crypto = require('crypto')
const { promisify } = require('util')
const pbkdf2 = promisify(crypto.pbkdf2)
async function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex')
const derivedKey = await pbkdf2(password, salt, 100000, 64, 'sha512')
return salt + ':' + derivedKey.toString('hex')
}
async function verifyPassword(password, stored) {
const [salt, key] = stored.split(':')
const derivedKey = await pbkdf2(password, salt, 100000, 64, 'sha512')
return crypto.timingSafeEqual(
Buffer.from(key),
derivedKey.toString('hex')
)
}
scrypt is preferred over pbkdf2 for memory-hardness against GPU attacks
const scrypt = promisify(crypto.scrypt)
async function hashPasswordScrypt(password) {
const salt = crypto.randomBytes(16).toString('hex')
const derivedKey = await scrypt(password, salt, 64, {
N: 16384, // CPU/memory cost
r: 8, // block size
p: 1 // parallelization
})
return salt + ':' + derivedKey.toString('hex')
}
For production : use bcrypt or argon2 npm packages instead - they're more battle-tested for password hashing than raw crypto module usage
Symmetric Encryption: AES-256-GCM¶
Authenticated Encryption with Associated Data - your only choice
const ALGORITHM = 'aes-256-gcm'
const KEY_LENGTH = 32 // 256 bits
const IV_LENGTH = 12 // 96 bits for GCM
const TAG_LENGTH = 16 // 128 bits auth tag
function encrypt(text, key) {
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
let encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag().toString('hex')
// Store iv + authTag + ciphertext together
return iv.toString('hex') + ':' + authTag + ':' + encrypted
}
function decrypt(encoded, key) {
const parts = encoded.split(':')
const iv = Buffer.from(parts.shift(), 'hex')
const authTag = Buffer.from(parts.shift(), 'hex')
const encrypted = parts.join(':')
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
decipher.setAuthTag(authTag)
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
// Generate a proper key
const key = crypto.randomBytes(KEY_LENGTH)
const secret = encrypt('sensitive data', key)
console.log(decrypt(secret, key))
// -> sensitive data
Always use GCM mode : never use ECB (it's broken) or CBC without proper HMAC (doesn't authenticate)
Random Bytes and UUIDs¶
// Cryptographic random bytes
const buf = crypto.randomBytes(32)
console.log(buf.toString('hex'))
// UUID v4
const uuid = crypto.randomUUID()
console.log(uuid)
// -> 550e8400-e29b-41d4-a716-446655440000
// Secure integer
function secureInt(max) {
const numBytes = Math.ceil(Math.log2(max + 1) / 8)
let result
do {
const buf = crypto.randomBytes(numBytes)
result = buf.readUIntBE(0, numBytes)
} while (result >= max) // rejection sampling to avoid bias
return result
}
Always use crypto.randomBytes over Math.random() for anything security-related : Math.random() is predictable
Key Generation and Management¶
// Generate a 256-bit AES key
const aesKey = crypto.randomBytes(32)
// Generate an RSA key pair (don't do this often - expensive)
const { generateKeyPairSync } = crypto
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
})
// Generate ECDSA key pair (faster)
const { publicKey: ecPub, privateKey: ecPriv } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
})
Store keys in environment variables or a secrets manager : never in the codebase , never in config files committed to git
Security Rules¶
// Rule 1: Constant-time comparison for all secrets
function safeCompare(a, b) {
if (a.length !== b.length) return false
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
}
// Rule 2: Generate IVs fresh every encryption
function encryptSafe(plaintext, key) {
const iv = crypto.randomBytes(IV_LENGTH) // NEVER reuse IVs
// ...
}
// Rule 3: Validate algorithm names to prevent downgrade
const ALLOWED_ALGORITHMS = ['aes-256-gcm', 'aes-128-gcm']
if (!ALLOWED_ALGORITHMS.includes(algorithm)) {
throw new Error('algorithm not allowed')
}
Never roll your own crypto : you will fuck up the IV reuse , you will forget the auth tag , you will use ECB mode like a script kiddie Use well-reviewed libraries and standard AEAD constructions like AES-256-GCM or ChaCha20-Poly1305
Prerequisites¶
- sec_01_owasp.md - understand the threat landscape first
next -> sec_03_input_validation.md