Cryptography and Security
By Syncfusion®, Inc. · Last updated
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:cryptogives 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:
- What
node:cryptocovers, and Web Crypto - Hashing data and files, and weak algorithms
- HMAC signatures and secure random values
- Password hashing with salts,
scrypt,pbkdf2andtimingSafeEqual - AES-256-GCM encryption, IVs and auth tags
- Asymmetric keys and signatures (advanced)
- Path traversal, command injection and prototype pollution
- ReDoS,
evalandvm, decompression bombs and supply chain risks - 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:cryptovs Web Crypto:node:cryptois the classic Node.js API.crypto.subtleis 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.
A hash turns any input into a fixed-size fingerprint. One small change gives a totally different hash "Asha 91" "Asha 92" SHA-256one way only
4f1c…9a07 b83e…12d5
Same input, same hash. You cannot get the input back from the hash.
A hash is a fingerprint of data, not a way to hide it and get it back.
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)=nrandom bytes. Use them for tokens and keys.randomInt(min, max)= a random whole number, fromminup to belowmax.randomUUID()= a random id like1b9d6bcd-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 =
scryptorpbkdf2. They are slow on purpose, so guessing millions of passwords takes too long. - Cost = how slow.
scrypthasN.pbkdf2has 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.
Animation: a password plus a random salt goes through a slow hash, and the salt and hash are stored together password random salt slow hashscrypt / pbkdf2 storedsalt:hash
To log in: hash the typed password with the stored salt and compare
Salt plus a slow hash: the same password never gives the same stored value.
// 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:
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,authTagand the encrypted text together. - Key derivation = turn a password into a key with
scryptorpbkdf2, never use the password itself.
// 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:
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.
// 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:
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 runsrm -rf reportsas a second command. - With
execFileorspawnand 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__,constructorandprototypekeys. - Use
Object.create(null)or aMapfor 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.
// 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:
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:vmruns code in a separate context, but it is not a security boundary.
// 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:
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
maxOutputLengthon 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 =
postinstallcode that runs on your computer duringnpm install. - Lockfile review = check
package-lock.jsonchanges 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-processand--allow-worker= allow child processes and worker threads.- Blocked actions throw
ERR_ACCESS_DENIED.
read.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"));
$ 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
| 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 |
TL;DR
- Hash = fingerprint, cannot be undone. SHA-256, not MD5 or SHA-1.
- HMAC = hash with a secret key. Proves the sender.
randomBytes,randomInt,randomUUID= secure random. NeverMath.random()for secrets.- Passwords = salt +
scryptorpbkdf2+timingSafeEqual. - AES-256-GCM = encrypt with a key, a new IV each time, and an auth tag.
- Path traversal = resolve and check the folder.
- Command injection =
execFilewith an argument list, neverexecwith input. - Prototype pollution = block
__proto__. ReDoS = avoid(a+)+. --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
cryptorandom 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
execFilewith argument lists, block__proto__. - Use
--permissionto limit what a program may touch.
Next: Production Readiness. You will make your Node.js app ready to run safely for real users.