If you’ve ever stored sensitive data — API keys, user tokens, personal information — you’ve probably wondered how to protect it properly. Node.js ships with a built-in crypto module that gives you everything you need. No npm install required.

This post covers the two most common patterns: symmetric encryption (same key to encrypt and decrypt) and hashing (one-way, for passwords).


The crypto module

Node’s crypto module is part of the standard library. You just require it:

JS
const crypto = require('crypto');
Click to expand and view more

That’s it. Let’s get into the actual patterns.


Symmetric encryption with AES-256-GCM

AES-256-GCM is the gold standard for symmetric encryption. It’s fast, widely supported, and authenticated — meaning it will tell you if the data was tampered with.

You need three things: a key, an IV (initialization vector), and the data.

JS
const crypto = require('crypto');

const ALGORITHM = 'aes-256-gcm';
const KEY = crypto.randomBytes(32); // 256-bit key — store this securely!

function encrypt(plaintext) {
  const iv = crypto.randomBytes(12); // 96-bit IV for GCM
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);

  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final()
  ]);

  const authTag = cipher.getAuthTag(); // GCM authentication tag

  return {
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex'),
    encrypted: encrypted.toString('hex')
  };
}

function decrypt({ iv, authTag, encrypted }) {
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    KEY,
    Buffer.from(iv, 'hex')
  );

  decipher.setAuthTag(Buffer.from(authTag, 'hex'));

  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(encrypted, 'hex')),
    decipher.final()
  ]);

  return decrypted.toString('utf8');
}

// Usage
const payload = encrypt('hello@rishavkumar.io');
console.log('Encrypted:', payload);

const original = decrypt(payload);
console.log('Decrypted:', original); // hello@rishavkumar.io
Click to expand and view more

A few things worth calling out:

  • Never reuse an IV. Generate a fresh one for every encryption call. crypto.randomBytes(12) does this correctly.
  • The auth tag is not optional. GCM mode produces an authentication tag that verifies the ciphertext hasn’t been modified. Always store and verify it.
  • The key must be secret. Store it in an environment variable, a secrets manager, or a KMS — never hardcode it or commit it to git.

Hashing passwords with scrypt

For passwords, you don’t encrypt — you hash. The difference matters: encryption is reversible, hashing is not. When a user logs in, you hash what they typed and compare it to the stored hash. You never need to recover the original password.

Node’s crypto.scrypt is the right tool here. It’s memory-hard, which makes brute-force attacks expensive.

JS
const crypto = require('crypto');

function hashPassword(password) {
  return new Promise((resolve, reject) => {
    const salt = crypto.randomBytes(16).toString('hex');

    crypto.scrypt(password, salt, 64, (err, derivedKey) => {
      if (err) reject(err);
      resolve(`${salt}:${derivedKey.toString('hex')}`);
    });
  });
}

function verifyPassword(password, hash) {
  return new Promise((resolve, reject) => {
    const [salt, key] = hash.split(':');

    crypto.scrypt(password, salt, 64, (err, derivedKey) => {
      if (err) reject(err);
      resolve(derivedKey.toString('hex') === key);
    });
  });
}

// Usage
(async () => {
  const hash = await hashPassword('mysecretpassword');
  console.log('Stored hash:', hash);

  const valid = await verifyPassword('mysecretpassword', hash);
  console.log('Password valid:', valid); // true

  const invalid = await verifyPassword('wrongpassword', hash);
  console.log('Password valid:', invalid); // false
})();
Click to expand and view more

The salt is stored alongside the hash (separated by :), which is fine — the salt’s job is to prevent rainbow table attacks, not to be secret.


Generating secure tokens

For things like password reset links, session tokens, or API keys, you just need a cryptographically random string:

JS
const token = crypto.randomBytes(32).toString('hex');
// "a3f8c2e1b4d7..." — 64 hex characters, unpredictable
Click to expand and view more

Don’t use Math.random() for anything security-related. It’s not cryptographically secure. crypto.randomBytes is.


Quick reference

Use caseMethodNotes
Encrypt/decrypt dataAES-256-GCMStore key securely, generate fresh IV each time
Store passwordscrypto.scryptAlways salt, never encrypt passwords
Secure tokenscrypto.randomBytesUse for session IDs, reset links, API keys
Data integrity checkcrypto.createHash('sha256')Not for passwords — use scrypt/bcrypt

The crypto module covers most real-world needs without pulling in external dependencies. That said, if you’re building something more complex — like JWT signing or RSA key pairs — libraries like jose or node-forge are worth looking at.

But for encrypting sensitive fields in a database, hashing passwords, or generating secure tokens? The standard library has you covered.