A salt is a short run of random bytes, generated fresh for each record and mixed with a password before it is hashed or used to derive a key. It is stored in plain sight next to the result.
That last part is where most explanations lose people. If the salt is not secret and an attacker can read it, what is it doing? The answer is narrow and worth getting exactly right: a salt does not make any individual password harder to guess. It destroys the attacker's ability to attack many passwords with shared work.
Summary
A salt is random, unique per record, and public. Its job is to guarantee that identical passwords produce different stored values, which means no precomputed table helps and no work an attacker does against one record carries to the next. It is not a secret, it is not a substitute for a slow derivation function, and reusing one across records removes the entire benefit.
The problem, without a salt
Consider a stored table of password hashes with no salt. Three accounts happen to use the same common password.
Because hashing is deterministic — the same input always produces the same output — all three rows store an identical value. An attacker who steals the table sees that immediately, without cracking anything: three accounts share a password, and it is probably a common one.
Worse, the attacker's work is reusable. They can hash the ten million most common passwords once, store the results, and then look up every row in the stolen table against that list. This is the rainbow table, and its defining property is that the expensive step happens once and then serves every victim, forever. A table built five years ago still works against an unsalted database stolen today.
What changes with a per-record salt
Now give each record sixteen random bytes of its own, and hash the password together with them.
The three accounts sharing a password now store three unrelated values. The attacker learns nothing from the table about who shares a password with whom.
More importantly, the precomputed list is worthless. It was built against bare passwords; every stored value here is derived from a password plus a salt the attacker did not know when they built the list. To use a candidate password against a specific record, they must hash it with that record's salt, and that computation helps them with nothing else.
The cost of attacking a stolen database changes shape entirely. Unsalted, it is roughly the cost of attacking one record, because the work is shared. Salted, it is the cost of attacking one record, multiplied by the number of records.
Why it can be public
This is the question that comes up every time, and it has a clean answer.
The salt's purpose is uniqueness, not concealment. It works by ensuring no two records can be attacked with the same computation. That property holds whether or not the attacker knows the salt, because the work still has to be done separately for each one.
Hiding it would buy very little and cost a lot — the salt has to be available to re-derive the key when a legitimate user enters their password, so it must be stored somewhere retrievable anyway. RFC 8018, which specifies PBKDF2, treats the salt as a public parameter throughout.
If you want a secret mixed in as well, that is a different construction with a different name, and it is covered below.
The rules that actually matter
| Rule | Why | Failure mode |
|---|---|---|
| Random, from a CSPRNG | Predictable salts can be precomputed against | Rainbow tables become viable again |
| Unique per record | Shared salt means shared work | Two identical passwords collide, and one table cracks both |
| At least 16 bytes | Long enough that collisions are negligible | Short salts can be exhausted and pre-tabulated |
| Generated at write time | A salt fixed in code is one salt for everyone | Effectively unsalted |
| Stored with the record | It is needed to re-derive | Legitimate users locked out |
The single most common real-world mistake is a salt that is "random" but generated once and compiled into the application. Every record then shares it, which means identical passwords still collide and an attacker can build one table against that specific salt. It has the appearance of salting and none of the effect.
The second most common is Math.random(). It produces something that looks random and is not: its internal state is recoverable from a modest number of outputs, which makes subsequent values predictable. Use crypto.getRandomValues() in a browser, secrets in Python, crypto.randomBytes in Node.
A worked example
Here is the whole mechanism in a few lines, using the browser API:
// Sixteen fresh random bytes, generated at write time, for this record only.
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 310000, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
// The salt is stored alongside the ciphertext. It is not a secret.When the user returns and types their password, the stored salt is read back, the same derivation runs, and the same key comes out. An attacker reading the database gets the salt too — and gains nothing from it except the obligation to do the work separately for every record.
This is the path the Web Crypto API article walks through in full, and the salt in it is the one described here: sixteen bytes, per note, stored with the ciphertext.
Salt, pepper, nonce, IV: four things that are not the same
These get used interchangeably, and they are not.
Salt. Random, unique per record, public, mixed into password derivation. Purpose: defeat precomputation and shared work.
Pepper. A secret value mixed in alongside the salt, stored separately from the database — ideally in a hardware security module or an application config the database dump would not include. Purpose: make a database-only breach insufficient. A pepper is a real technique with a real cost: rotating it requires every user to re-authenticate, and if it is lost, every record is unrecoverable.
Initialisation vector. Random, unique per encryption, public, used by the cipher rather than the derivation function. Purpose: ensure encrypting the same plaintext twice produces different ciphertext. Under AES-GCM, reuse is not a weakness but a break — see NIST SP 800-38D.
Nonce. The general term for a number used once. A GCM IV is a nonce. Not every nonce is an IV.
The practical confusion worth flagging: a salt may be reused across saves of the same record if the password has not changed, because the derived key is meant to be stable. An IV must be fresh on every single encryption, including re-saving the same note with the same password. Conflating them produces IV reuse, which is the one failure in this article that is genuinely catastrophic.
What salting does not do
It does not make a weak password strong. If the password is password123, an attacker attacking that one record will find it quickly regardless. Salting changes the economics of attacking a million records at once; it changes almost nothing about attacking one.
It does not replace a slow derivation function. Salting with a fast hash still lets an attacker test billions of candidates per second against a targeted record. The salt forces the work to be done per-record; the iteration count makes each unit of that work expensive. You need both, and PBKDF2 vs Argon2 covers the second half.
It does not protect the password in transit or at the keyboard. A salt is applied after the password reaches the code. Anything that intercepts it earlier — a keylogger, a malicious extension, a modified script — is untouched by any of this.
It does not make a breach acceptable. Properly salted and stretched storage buys time for users to change passwords. It is not a reason to under-report an incident.
FAQ: salts
Q1. Should the salt be secret?
No. It works through uniqueness rather than concealment, and it has to be retrievable to re-derive the key for a legitimate user. If you want a secret component, that is a pepper, stored separately from the database.
Q2. How long should a salt be?
Sixteen bytes is the common recommendation and is what NIST SP 800-132 suggests as a minimum for password-based derivation. Longer costs almost nothing; shorter starts to make collisions and precomputation plausible.
Q3. Can I reuse the same salt for one user across several records?
You can, but you lose part of the benefit: two records belonging to that user with the same password will collide, and an attacker who cracks one gets both. A fresh salt per record costs sixteen bytes and removes the question.
Q4. Is a username a good salt?
No. It is neither random nor globally unique — the same username appears across services, so an attacker can precompute against it. It also changes if the user renames, which breaks derivation.
Q5. What is the difference between a salt and an IV?
A salt feeds the key derivation function and can stay the same while the password stays the same. An IV feeds the cipher and must be fresh for every single encryption. Reusing a salt is a weakness; reusing a GCM IV is a break.
Q6. Where should the salt be stored?
Directly alongside the record it belongs to — the same row, or prefixed to the ciphertext. It has to be available at decryption time, and there is no security gained by putting it somewhere harder to reach.
Where to go next
- How the Web Crypto API works in the browser — the derivation this salt feeds, as four calls.
- PBKDF2 vs Argon2 — the other half of the defence, and why iteration count alone is not enough.
- Zero-knowledge encryption: what it actually means — what it means for the operator to hold ciphertext and salt and still be unable to read anything.
- Encryption glossary — short definitions for the terms used across these articles.
Primary sources: RFC 8018 for PBKDF2 and the treatment of salts, NIST SP 800-132 for password-based key derivation, and the [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/PasswordStorageCheat_Sheet.html) for current practice.