Skip to content

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

next -> sec_03_input_validation.md