Intermediate · Authentication and authorization · topic 31 of 54 · 30 min
Storing passwords safely
Explanation
Two words first
Authentication means checking who you are (logging in). Authorization means checking what you are allowed to do (can you delete this order?). This module covers both; this topic is about the first step of authentication: keeping passwords safe.
Why plain-text passwords are a disaster
Databases leak: a stolen backup, a hacked server, a careless employee. If passwords are stored as typed, every account is open at once. Worse, many people use the same password for email and banking, so one leak hurts them everywhere.
Hashing is not encryption
| Hashing | Encryption | |
|---|---|---|
| Direction | One way: you cannot get the input back | Two way: decrypt with the key |
| Needs a key? | No (a salt, which is not secret) | Yes, and the key must stay secret |
| Use it for | Passwords | Data you must read again, like a stored PAN for KYC |
Salt: different hash for the same password
A salt is a random value made for each user and stored next to their hash. Two users with the password "india@123" get completely different hashes. This stops attackers from using rainbow tables, which are huge pre-computed lists of hashes for common passwords.
Slow on purpose
General hashes like SHA-256 are built to be fast: a gaming GPU can try billions per second, so short passwords fall in minutes. Password hashes are deliberately slow and use lots of memory, so each guess costs the attacker real time and money.
| Algorithm | Advice in 2026 |
|---|---|
| Argon2id | First choice. OWASP minimum: 19 MiB memory, 2 iterations, 1 thread |
| scrypt | Good when Argon2id is not available |
| bcrypt | Fine for older systems; cost 10 or more; only the first 72 bytes of a password count |
| PBKDF2-SHA256 | When rules require FIPS; OWASP suggests 600,000 iterations |
| SHA-256 / MD5 alone | Never for passwords: far too fast |
// Example only: runs on a server with Node.js and the argon2 package
import argon2 from 'argon2';
// Sign up: hash and store the result (salt is included inside it)
const hash = await argon2.hash(password, { type: argon2.argon2id });
// hash looks like: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
// Log in: compare the typed password with the stored hash
const ok = await argon2.verify(user.passwordHash, typedPassword);
// bcrypt works the same way:
// const hash = await bcrypt.hash(password, 12);
// const ok = await bcrypt.compare(typedPassword, hash);// PBKDF2 with Web Crypto (built into Node and browsers)
const enc = new TextEncoder();
const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', hash: 'SHA-256', salt: enc.encode(salt), iterations: 600000 },
key,
256 // 256 bits = 32 bytes
);Watch the lesson
Storing passwords safely · 1/6
Today: how to store passwords so that even a stolen database does not reveal them.
Prefer a video by a teacher? Find free videos on “password hashing salt bcrypt argon2 explained” on YouTube ↗
In real life: A leak that did not hurt
Imagine a coaching institute's student portal is hacked and the users table is stolen. If the table holds plain passwords, attackers log in to students' email and UPI apps with the same passwords that very day. If it holds Argon2id hashes with salts, each guess costs real computing time and every user needs a separate attack. The institute still resets all passwords, but most students are safe.
Practice (0/3 done)
Hashes come out as bytes. Write toHex(bytes) that turns an array (or Uint8Array) of numbers 0-255 into a lower-case hex string, two characters per byte. toHex([0, 15, 255]) is "000fff".
Write async hashPassword(password, salt, iterations) using Web Crypto PBKDF2 with SHA-256. Import the password with crypto.subtle.importKey("raw", ..., "PBKDF2", false, ["deriveBits"]), call crypto.subtle.deriveBits with { name: "PBKDF2", hash: "SHA-256", salt, iterations } and 256 bits, and return the result as a 64-character hex string. (Tests use small iteration counts so they run fast; real apps use 600,000 or Argon2id.)
hashPassword is done for you. Write safeEqual(a, b) that compares two strings without stopping at the first difference (return false at once only if the lengths differ), and async verifyPassword(password, salt, iterations, storedHex) that hashes the typed password and returns true only when it matches storedHex, using safeEqual.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.