# Cryptography and Security

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Quality and Production
Source: https://www.devtutorials.ai/learn/nodejs/quality-production/crypto-and-security

> What node:crypto covers and how it compares with Web Crypto, hashing with createHash and hashing files with streams, hashing versus encryption, weak algorithms, HMAC, secure random values with randomBytes, randomInt and randomUUID, password hashing with salts, scrypt, pbkdf2, cost settings and timingSafeEqual, AES-256-GCM encryption with IVs and auth tags, key derivation, asymmetric keys and signatures, secure coding against path traversal, command injection, prototype pollution, ReDoS, eval and vm, decompression bombs and supply chain attacks, and the Node.js permission model.

## Cryptography and Security

**Cryptography** = maths that keeps data secret and proves it was not changed.

**Security** = writing code so attackers cannot misuse it.

- `node:crypto` gives you hashing, signing, random values and encryption.
- Most attacks on Node.js apps come from small coding mistakes, not broken maths.
- This lesson covers both: the tools, and the mistakes to avoid.

**Simple lesson:** use proven crypto tools, and never trust input from outside.

### A Real-World Example

Think of the exam paper store room.

- Papers go into sealed envelopes. A broken seal shows someone opened it. That is a **signature**.
- The store room has a lock. Only teachers with keys get in. That is **encryption**.
- The principal never writes the door code on the notice board. That is keeping **secrets** out of code.
- A visitor's note saying "let me in, I'm a teacher" is checked, not trusted. That is **input validation**.

## What You Will Learn

In this lesson, you will learn:

1. What `node:crypto` covers, and Web Crypto
2. Hashing data and files, and weak algorithms
3. HMAC signatures and secure random values
4. Password hashing with salts, `scrypt`, `pbkdf2` and `timingSafeEqual`
5. AES-256-GCM encryption, IVs and auth tags
6. Asymmetric keys and signatures (advanced)
7. Path traversal, command injection and prototype pollution
8. ReDoS, `eval` and `vm`, decompression bombs and supply chain risks
9. The permission model

## The crypto Module

- **Hash** = a fixed-size fingerprint of data. Same input, same fingerprint. You cannot turn it back.
- **HMAC** = a hash mixed with a secret key. Proves who made the data.
- **Random values** = numbers and ids that nobody can guess.
- **Encryption** = scramble data with a key, and unscramble it with the key.
- **Signature** = proof, made with a private key, that data came from you.
- **`node:crypto` vs Web Crypto:** `node:crypto` is the classic Node.js API. `crypto.subtle` is the browser-style Web Crypto API, also in Node.js. Both use the same strong code underneath.

**Simple lesson:** hash to fingerprint, HMAC to prove the sender, encrypt to hide.

## Hashing

- **`createHash("sha256").update(data).digest("hex")`** = the SHA-256 fingerprint as hex text.
- Change one letter and the whole hash changes.
- **Hashing vs encryption:** a hash cannot be undone. Encryption can be undone with the key.
- **Weak algorithms:** MD5 and SHA-1 are broken for security. Use SHA-256 or better.

Never invent your own encryption. Use the tools in `node:crypto` exactly as documented.

### Hashing Files with Streams

- A hash object is a stream: pipe a file into it.
- Memory stays small, even for huge files.
- Websites publish file hashes so you can check a download was not changed.

**Simple lesson:** SHA-256 gives a fingerprint. Never use MD5 or SHA-1 for security.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 180" role="img" ariaLabelledby="fig-node-hash" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-hash">A hash turns any input into a fixed-size fingerprint. One small change gives a totally different hash</title>
<rect x="20" y="30" width="150" height="40" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/><text x="95" y="55" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>"Asha 91"</text>
<rect x="20" y="110" width="150" height="40" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/><text x="95" y="135" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>"Asha 92"</text>
<rect x="215" y="65" width="110" height="50" rx="10"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="270" y="88" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>SHA-256</text><text x="270" y="104" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>one way only</text>
<path d="M170 50 L215 82 M170 130 L215 98 M325 82 L370 50 M325 98 L370 130"  style={{ stroke: "var(--ink)", strokeWidth: 1.5, fill: "none" }}/>
<rect x="370" y="30" width="170" height="40" rx="8"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}/><text x="455" y="55" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 12 }}>4f1c…9a07</text>
<rect x="370" y="110" width="170" height="40" rx="8"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}/><text x="455" y="135" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 12 }}>b83e…12d5</text>
<circle r="6" cx="170" cy="50" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="170;270;370;370" keyTimes="0;0.3;0.6;1" dur="5s" repeatCount="indefinite" /><animate attributeName="cy" values="50;90;50;50" keyTimes="0;0.3;0.6;1" dur="5s" repeatCount="indefinite" /></circle>
<text x="280" y="172" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>Same input, same hash. You cannot get the input back from the hash.</text>
</svg>
<figcaption>A hash is a fingerprint of data, not a way to hide it and get it back.</figcaption>
</figure>

## HMAC and Random Values

**HMAC** = a hash made with a secret key. Only someone with the key can make the right one.

- Use it to sign data you send out, such as a payment webhook or a download link.
- The receiver makes the HMAC again with the same key and compares.

### Secure Random Values

- **`randomBytes(n)`** = `n` random bytes. Use them for tokens and keys.
- **`randomInt(min, max)`** = a random whole number, from `min` up to below `max`.
- **`randomUUID()`** = a random id like `1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed`.
- **Why not `Math.random()`?** It is made for games. Its next values can be guessed. Never use it for passwords, tokens or OTPs.

Keep secret keys in environment variables, and use a different key in each environment.

**Simple lesson:** sign with HMAC, and make tokens with `crypto`, never with `Math.random()`.

## Password Hashing

Never store passwords as plain text, and never with a fast hash like SHA-256.

- **Salt** = random bytes added to each password before hashing. Two students with the same password get different hashes.
- **Slow hash** = `scrypt` or `pbkdf2`. They are slow on purpose, so guessing millions of passwords takes too long.
- **Cost** = how slow. `scrypt` has `N`. `pbkdf2` has iterations (OWASP suggests 600,000 with SHA-256).
- **`timingSafeEqual`** = compare in a fixed time, so attackers learn nothing from how fast a check fails.
- Store the salt next to the hash. The salt is not secret.

Many apps use a ready library such as `bcrypt` or `argon2`. They follow the same idea: a salt plus a slow hash.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 170" role="img" ariaLabelledby="fig-node-password" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-password">Animation: a password plus a random salt goes through a slow hash, and the salt and hash are stored together</title>
<rect x="20" y="30" width="120" height="40" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="80" y="55" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>password</text>
<rect x="20" y="100" width="120" height="40" rx="8"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}/><text x="80" y="125" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>random salt</text>
<rect x="200" y="60" width="150" height="50" rx="10"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="275" y="82" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>slow hash</text><text x="275" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>scrypt / pbkdf2</text>
<rect x="410" y="60" width="130" height="50" rx="10"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="475" y="82" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>stored</text><text x="475" y="100" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 11 }}>salt:hash</text>
<line x1="140" y1="50" x2="200" y2="80"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="140" y1="120" x2="200" y2="90"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="350" y1="85" x2="410" y2="85"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/>
<circle r="6" cx="200" cy="85" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="cx" values="200;350;410" keyTimes="0;0.7;1" dur="3s" repeatCount="indefinite" /></circle>
<text x="280" y="160" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>To log in: hash the typed password with the stored salt and compare</text>
</svg>
<figcaption>Salt plus a slow hash: the same password never gives the same stored value.</figcaption>
</figure>

```js
// main.mjs: scrypt is the other good choice
import { scrypt, randomBytes, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";

const scryptAsync = promisify(scrypt);
const salt = randomBytes(16);
const options = { N: 16384, r: 8, p: 1 }; // N is the cost: higher is slower

const stored = await scryptAsync("green@2026", salt, 32, options);
const attempt = await scryptAsync("green@2026", salt, 32, options);
console.log("scrypt hash bytes:", stored.length);
console.log("Password correct?", timingSafeEqual(stored, attempt));
```

Output:

```text
scrypt hash bytes: 32
Password correct? true
```

**Simple lesson:** salt + slow hash + `timingSafeEqual`. Never plain text, never a fast hash.

## Encryption

**AES-256-GCM** = a strong, modern way to encrypt data with one secret key.

- **Key** = 32 random bytes. Keep it secret, for example in an environment variable.
- **IV** (or **nonce**) = 12 random bytes, new for **every** message. It is not secret.
- **Auth tag** = proof that the encrypted data was not changed. Decryption fails if it was.
- Store `iv`, `authTag` and the encrypted text together.
- **Key derivation** = turn a password into a key with `scrypt` or `pbkdf2`, never use the password itself.

```js
// main.mjs
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

const key = randomBytes(32); // AES-256 needs 32 bytes

function encrypt(text) {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const data = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
  return { iv, data, tag: cipher.getAuthTag() };
}

function decrypt({ iv, data, tag }) {
  const decipher = createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8");
}

const box = encrypt("Ravi's medical note");
console.log("Encrypted looks like:", box.data.toString("base64").slice(0, 12) + "...");
console.log("Decrypted:", decrypt(box));

box.data[0] ^= 1; // someone changes one bit
try {
  decrypt(box);
} catch (err) {
  console.log("Changed data rejected:", err.message);
}
```

Output:

```text
Encrypted looks like: 6J6ff3kgtM9X...
Decrypted: Ravi's medical note
Changed data rejected: Unsupported state or unable to authenticate data
```

### Asymmetric Keys and Signatures (Advanced)

- **Key pair** = a **private key** you keep and a **public key** you share.
- **Sign** with the private key. Anyone **verifies** with the public key.
- Used by HTTPS certificates, JWT tokens (see **Token Authentication and Authorization** in Express.js) and software updates.

```js
// main.mjs
import { generateKeyPairSync, sign, verify } from "node:crypto";

const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const notice = Buffer.from("School closed on Friday");

const signature = sign(null, notice, privateKey);
console.log("Real notice verified?", verify(null, notice, publicKey, signature));
console.log("Fake notice verified?", verify(null, Buffer.from("School closed all week"), publicKey, signature));
```

Output:

```text
Real notice verified? true
Fake notice verified? false
```

**Simple lesson:** AES-GCM with a fresh IV per message, and key pairs for signatures.

## Secure Coding in Node.js

### Path Traversal

**Path traversal** = a request like `../../etc/passwd` that climbs out of the folder you meant to share.

- Resolve the full path, then check it is still inside your folder.

### Command Injection in child_process

**Command injection** = user input that becomes part of a shell command.

- With `exec`, the shell sees `;` and runs `rm -rf reports` as a second command.
- With `execFile` or `spawn` and an argument list, there is no shell. `;` is just a letter.

### Prototype Pollution

**Prototype pollution** = input with a key like `__proto__` that adds properties to **every** object.

- Skip `__proto__`, `constructor` and `prototype` keys.
- Use `Object.create(null)` or a `Map` for data with user-chosen keys.
- Validate input with a schema library such as Zod.

### Regular Expression DoS (ReDoS)

- Some regular expressions take **exponential** time on certain inputs, such as `/^(a+)+$/`.
- One request with a crafted string can freeze the whole event loop.
- Avoid nested repeats like `(a+)+`, limit the input length, and test patterns with tools.

```js
// main.mjs: each extra letter roughly doubles the time
const evil = /^(a+)+$/;
evil.test("aaaa!"); // warm up
for (const n of [22, 24, 26]) {
  const input = "a".repeat(n) + "!";
  const t0 = performance.now();
  evil.test(input);
  console.log(`${n} letters: ${(performance.now() - t0).toFixed(0)} ms`);
}
```

Output:

```text
22 letters: 30 ms
24 letters: 117 ms
26 letters: 455 ms
```

### eval() and vm Are Not Sandboxes

- `eval(text)` runs any code, with your program's power. Never pass user input to it.
- `node:vm` runs code in a separate context, but it is **not** a security boundary.

```js
// main.mjs: code inside vm escapes and reaches process
import vm from "node:vm";

const escaped = vm.runInNewContext("this.constructor.constructor('return process')().version");
console.log("Code in vm reached the real process:", escaped.startsWith("v"));
```

Output:

```text
Code in vm reached the real process: true
```

### Decompression Limits

- A small zip or gzip file can expand to gigabytes. This is a **decompression bomb**.
- Set `maxOutputLength` on zlib functions, and stop streams after a size limit.

### Dependency and Supply Chain Risks

- **Typosquatting** = a fake package with a look-alike name, such as `expresss`.
- **Install scripts** = `postinstall` code that runs on your computer during `npm install`.
- **Lockfile review** = check `package-lock.json` changes in pull requests for strange new packages.
- Run `npm audit`, keep packages few and up to date (see **Packages and npm**).

**Simple lesson:** check paths, never build shell commands from input, block `__proto__`, and keep regexes simple.

## Permission Model (Advanced)

**Permission model** = a Node.js mode where the program may only touch what you allow.

- **`node --permission app.js`** = turn it on. By default, almost nothing is allowed.
- **`--allow-fs-read=<path>`** and **`--allow-fs-write=<path>`** = allow files and folders.
- **`--allow-child-process`** and **`--allow-worker`** = allow child processes and worker threads.
- Blocked actions throw `ERR_ACCESS_DENIED`.

`read.js`

```js
const fs = require("node:fs");
console.log("Own folder:", fs.readFileSync(__filename, "utf8").length > 0 ? "allowed" : "?");
try {
  fs.readFileSync("/etc/hostname");
} catch (err) {
  console.log("Outside folder:", err.code);
}
console.log("Can write?", process.permission.has("fs.write"));
```

```text
$ node --permission --allow-fs-read=./ read.js
Own folder: allowed
Outside folder: ERR_ACCESS_DENIED
Can write? false
```

### What the Permission Model Does Not Protect

- It is a seat belt for **your own** code and packages, not a full sandbox.
- It does not limit network access in every version, CPU or memory use.
- Code that is already allowed can still misuse what it is allowed to touch.

**Simple lesson:** `--permission` limits what a program may touch, but it is not a full sandbox.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Storing passwords with SHA-256 | Any hash is safe | Use `scrypt` or `pbkdf2` with a salt | Leaked hashes are cracked fast |
| Comparing secrets with `===` | Comparison is harmless | Use `timingSafeEqual` | Attackers learn from timing |
| `Math.random()` for tokens | Random is random | Use `randomBytes` or `randomInt` | Tokens can be guessed |
| Reusing an IV with AES-GCM | The IV is just a setting | A new IV for every message | Encryption can be broken |
| Secrets in code or Git | It is a private repo | Use environment variables | Anyone with the code has the keys |
| `exec` with user input | Users type normal names | Use `execFile` with arguments | Command injection |
| Merging user JSON into objects | JSON is just data | Block `__proto__` and validate | Prototype pollution |
| Using MD5 or SHA-1 for security | Old means trusted | Use SHA-256 or better | Forged fingerprints |

</div>

## TL;DR

1. **Hash** = fingerprint, cannot be undone. **SHA-256**, not MD5 or SHA-1.
2. **HMAC** = hash with a secret key. Proves the sender.
3. **`randomBytes`, `randomInt`, `randomUUID`** = secure random. Never `Math.random()` for secrets.
4. **Passwords** = salt + `scrypt` or `pbkdf2` + `timingSafeEqual`.
5. **AES-256-GCM** = encrypt with a key, a new IV each time, and an auth tag.
6. **Path traversal** = resolve and check the folder.
7. **Command injection** = `execFile` with an argument list, never `exec` with input.
8. **Prototype pollution** = block `__proto__`. **ReDoS** = avoid `(a+)+`.
9. **`--permission`** = allow only the files and features a program needs.

## Check Yourself

## Exercises

## What to Remember

- Hash with SHA-256, sign with HMAC, and make secrets with `crypto` random functions.
- Store passwords as salt + slow hash, and compare with `timingSafeEqual`.
- Encrypt with AES-256-GCM and a fresh IV every time.
- Keep keys and secrets out of code.
- Resolve and check paths, use `execFile` with argument lists, block `__proto__`.
- Use `--permission` to limit what a program may touch.

Next: **Production Readiness**. You will make your Node.js app ready to run safely for real users.
