DEVTOOLBOX / GUIDES this guide
Hash, HMAC or encryption: picking the primitive before the algorithm
Most cryptographic mistakes are primitive mistakes: a fast hash where a slow one belonged, a keyed hash that can be extended, a mode with no tag. This guide orders the four primitives by what they can prove, gives a criterion and a cost for each choice, and marks the places where a decision stops being reversible.
Hash, HMAC, KDF, or encryption: choose the primitive before the algorithm
A primitive is a contract about who can produce a valid output and what that output proves; an algorithm is only a work factor. Reaching for SHA-256 where HMAC belonged, or HMAC where Argon2id belonged, gives code that looks correct and protects nothing, and three of these choices are impossible to reverse later.
Four capability questions cut the space. Does the input have to come back? Only encryption is reversible, and only while the key exists. Must the output prove that a specific party produced the data? Then a key is involved: a MAC when both sides share it, a signature when the verifier must be unable to produce one. Is the input a human-chosen secret? The attacker then attacks the input rather than the algorithm, and the only lever is cost per guess. Must the output merely show that nothing changed by accident? An unkeyed digest is enough; anything slower is waste.
Look-alike artifacts differ. A digest and a ciphertext are both opaque strings, but one is one-way and the other is one key away from plaintext. Base64 resembles both and provides neither property, so a credential encoded instead of hashed is a plaintext password with extra steps.
The irreversible decisions land here. Digest size is baked into every column: SHA-256 returns 256 bits, 32 bytes, 64 hex characters, while SHA-512 doubles all three and invalidates every digest already written. A KDF parameter block is baked into every record, because an existing password hash can only be recomputed when the plaintext arrives again, meaning the next successful login. A nonce discipline is baked into every ciphertext already produced, and no re-encryption repairs what a reused nonce has leaked.
| Question | Primitive | What it proves | Cost accepted |
|---|---|---|---|
| Input must come back later | AEAD encryption | Nothing, unless the tag is checked | Key custody and nonce discipline |
| Data must be shown unchanged | Unkeyed hash | Integrity against accident only | Fixed 32-byte output, no key |
| Only a key holder may approve | MAC (HMAC) | Authorship between two key holders | Two compression passes per message |
| Verifier must not be able to forge | Signature | Authorship to a public verifier | Key distribution and slower checks |
| Input is a low-entropy secret | KDF | Nothing alone, only raises cost | Tens of milliseconds per check, always |
The primitive fixes the attack model; the algorithm only fixes the work factor.
Integrity checks: comparing digests correctly
A digest comparison fails in ways that look like a corrupt file. The fault is nearly always in the comparison rather than in the bytes, and it errs silently in both directions: a mismatch on identical data, or a match on bytes that were never fully read.
Text is not the value. Hex is case-insensitive, so AB and ab denote one byte, and a store comparing strings rejects the digest its own tool printed. A pasted value carries a newline, a leading 0x, or the colon-separated fingerprint style that has to be stripped before decoding. Hex and Base64 of the same 32 bytes never compare equal, and truncation is invisible: 8 hex characters are 32 bits, so unrelated inputs collide after roughly 65,536 samples. Decode to bytes first, then compare buffers of equal length.
Comparison must be constant time when an attacker can trigger it repeatedly. A byte-by-byte compare returns at the first difference, so the count of matching leading bytes leaks through elapsed time, and a network attacker can rebuild a MAC byte by byte. Node exposes crypto.timingSafeEqual and Python exposes hmac.compare_digest; both throw on unequal lengths, which is why both sides should be reduced to 32 bytes beforehand rather than length-checked first.
Content mismatches usually have a dull cause. A file rewritten with CRLF endings hashes differently, and so does the same text stored in NFC versus NFD under UAX #15, because the bytes differ. Two gzips of one identical file differ because RFC 1952 puts a modification time and an original filename in the header. None of this is an attack, and all of it is reported as corruption.
| Digest mismatch | Usual cause | First check |
|---|---|---|
| Values look identical | Hex case or a trailing newline | Normalise, then decode to bytes |
| Hex compared against Base64 | Two encodings of one value | Encode both the same way |
| One file, two different gzips | Header timestamp and filename | Recompress with a fixed time |
| Same text, different digest | Unicode normalisation form | Normalise to NFC under UAX #15 |
| Comparison errors on length | Length checked before compare | Hash both sides to 32 bytes |
Where a false mismatch is produced, before blaming the file
HMAC versus a keyed hash with a fixed prefix
The fixed-prefix construction H(key || message) is the most common home-made MAC, and it fails for one class of hash. Merkle-Damgard hashes such as MD5, SHA-1 and SHA-256 expose their internal state in the digest, so that digest plus the key length yields a valid one for key || message || padding || extension without learning the key. That is length extension, and RFC 2104 exists to close it.
HMAC folds the key into one hash block, 64 bytes for SHA-256 and 128 for SHA-512, hashing a longer key first and zero-padding a shorter one, then computes H(opad || H(ipad || message)) with the constants 0x36 and 0x5c. The nesting puts that state out of reach. The cost is two compression passes per message, roughly double a bare hash on short input and less on long input.
The obvious alternative, H(message || key), blocks length extension and is still wrong: it inherits the collision resistance of the hash, and a collision transfers into a MAC collision. Sponge constructions change the picture: SHA-3, KMAC under NIST SP 800-185 and keyed BLAKE2 do not extend, so a prefix key over them is defensible but not portable.
Pick HMAC-SHA-256 when two parties share a secret and one must prove authorship to the other; pick a signature when the verifier must be unable to produce one, since every HMAC verifier is also an HMAC forger. Two boundaries matter. Poly1305 (RFC 8439) is a one-time MAC whose key reuse destroys secrecy and integrity, so it belongs inside an AEAD. A verifier that accepts HS256 from RFC 7518 while using an RSA public key as the HMAC secret is a primitive-boundary bug of this shape.
| Construction | Length extension | Standard | When it fits |
|---|---|---|---|
| Hash of key then message | Yes, on MD5, SHA-1, SHA-256 | None | Only with a sponge hash |
| Hash of message then key | No | None | Never: inherits collisions |
| HMAC | No | RFC 2104 | Shared key, default choice |
| KMAC | No | NIST SP 800-185 | SHA-3 and SHAKE stacks |
| Poly1305 | No | RFC 8439 | Inside an AEAD, one message |
Prefix, suffix and HMAC differ in one property that decides everything
Password storage: why a fast hash is the wrong primitive
A password hash must be slow in a way that scales with hardware, while every other primitive here wants speed. Storing SHA-256(password) is not a weak password hash but a different primitive: whoever takes the table gets full speed from the same silicon. A current GPU computes on the order of 10^10 SHA-256 hashes per second, and an eight-character password over a 62-symbol alphabet is 62^8, about 2.18 x 10^14 candidates or 47.6 bits: roughly three hours of brute force.
A random per-record salt of at least 128 bits removes precomputation and stops one guess testing every row; it does not slow guessing against one account. A pepper held outside the database buys time until an attacker holds both halves: a deployment assumption rather than a parameter.
Iteration floors move, and most advice omits it: PBKDF2 (RFC 8018) fixes no number itself; guidance does. A floor of 1,000 iterations was defensible around 2000, 10,000 served as the NIST SP 800-63B floor, and OWASP guidance now says 600,000 for PBKDF2-HMAC-SHA256. Dividing 2.18 x 10^14 by 600,000 turns those three hours into about two centuries on the same hardware, so the parameter, not the acronym, is the decision. Argon2id (RFC 9106) buys the margin with memory: t=1 with m=2 GiB and p=4, or t=3 with m=64 MiB, plus a 128-bit salt and 256-bit tag.
The irreversible point is migration: an unsalted MD5 cannot be upgraded in place, because the plaintext is gone; rehashing only happens when the user types the password again, so a wrong choice stays live until every account logs in again. bcrypt truncates input at 72 bytes, silently, so a long passphrase is defended only by its first 72 bytes.
| Scheme | Tuned by | Known limit | Fit |
|---|---|---|---|
| MD5 or SHA-256, no salt | Nothing | Full hardware speed | Never |
| SHA-256 with a salt | Nothing | Billions of guesses per second on GPUs | Never for passwords |
| PBKDF2-HMAC-SHA256 | Iteration count | GPU friendly, no memory cost | FIPS and interop constraints |
| bcrypt | Cost factor | Silent 72-byte truncation | Legacy stores, short inputs |
| scrypt | N, r, p | Easy to misconfigure | Memory hard with tuning |
| Argon2id | Memory, passes, lanes | Needs a memory budget | New systems, default |
Every option is fast to verify once and slow to attack forever
Password strength estimation: what an entropy estimate can and cannot tell you
An entropy estimate assumes a generator: the number printed is length times log2 of the alphabet, exact for a random string from a known alphabet and a fiction for anything a person chose. Nine characters from 94 printable ASCII symbols is 59.0 bits by that formula; the same nine spelled P@ssw0rd! are a leet substitution already in the wordlist, and fall in seconds.
Passphrases get the opposite error: four words drawn from the Diceware list of 7,776 carry 4 x log2(7776), about 51.7 bits, and six carry 77.5; both hold only because the words were chosen at random. The same six lifted from a quotation carry far less, since the attacker enumerates famous phrases rather than 7776^6 combinations.
What a meter cannot see: reuse across sites, breach membership, proximity to the service or user name, dates, and above all the ordering of the attacker dictionary. Crackers sweep the keyspace by frequency, not lexicographic order, so a 47-bit space whose first 10^9 candidates cover most real passwords is not 47 bits of work. That is why NIST SP 800-63B requires a blocklist of compromised and common values instead of a composition score, sets 8 characters as the floor while supporting at least 64, and forbids composition rules and forced rotation.
The estimate is trustworthy only where the generator is known. A password from crypto.getRandomValues over 62 symbols at 20 characters carries about 119 bits, and no blocklist or grammar model reduces it, which makes the number a measurement, not an opinion. Use a meter to coach a human, and a blocklist plus a length floor to decide acceptance: the score describes a generator, and only the blocklist describes your attacker.
| Estimate | What it misses | Decide with |
|---|---|---|
| 9 characters from 94 symbols: 59.0 bits | Assumes a uniform random draw | Check blocklists and breach corpora |
| Four Diceware words: 51.7 bits | Exact only if the dice were real | Inspect the generation method |
| 12 characters: 71.5 bits over 62 symbols | Silent about reuse and context | Reject known-breached strings |
| A meter that turns green at 12 chars | Ignores the site name as a hint | Verifier acceptance rules |
A score describes a generator, not the attacker in front of you
Encryption: authentication, modes, and nonce reuse
Confidentiality and integrity are separate properties, and a mode offering only the first is worse than it looks: a well-formed ciphertext invites the assumption that it is authentic. CTR is a stream, so flipping one ciphertext bit flips exactly one plaintext bit and an attacker who knows the field layout rewrites a value without the key; CBC with padding turns decryption failures into a padding oracle, as in POODLE. Use an AEAD under RFC 5116, verify the tag, and never return plaintext that failed verification.
AES-GCM takes a 96-bit nonce, and repeating one under a key is the worst failure in this guide, silent and permanent: two ciphertexts written under the same key and nonce XOR into the XOR of their plaintexts, and the pair exposes the authentication key, which permits forging any message under it. Random 96-bit nonces collide with probability near q^2/2^97 after q messages, so NIST SP 800-38D caps a key at 2^32 invocations, which already leaves about 2^-33. Widen the field instead, as XChaCha20 does at 192 bits, or use a reuse-tolerant mode such as AES-GCM-SIV from RFC 8452, where a repeat leaks only that two plaintexts were equal.
Five parameter decisions decide how a deployment behaves, and each is cheap to get right at the start.
Losing the key loses the data, with no recovery path and no dictionary attack as a fallback; that asymmetry is the price of reversibility. RSA-OAEP from RFC 8017 carries keys, never payloads: at most k - 2h - 2 bytes, which is 190 bytes for a 2048-bit key with SHA-256, and PKCS#1 v1.5 encryption should be retired. Sign with RSA-PSS or Ed25519 under RFC 8032.
- Tag length: 128 bits for GCM, and never truncated below 96, because forgery cost falls exponentially as bits come off.
- Payload per invocation: 2^39-256 bits, about 64 GiB, a limit of the mode rather than of the library.
- Block size: AES keeps 128 bits at every key size, so 128, 192 and 256 describe keys and never blocks, and CBC padding then leaks length in 16-byte steps.
- Context binding: pass row and user identifiers as additional authenticated data, so a ciphertext cannot be replayed into another record.
- Key origin: a key derived from a password is a KDF job, and the usual shape is a random data key wrapped by a key-encryption key.
| Mode | Confidentiality | Integrity | Nonce rule |
|---|---|---|---|
| AES-ECB | Equal blocks stay equal | None | No nonce, do not use |
| AES-CBC | Yes, padded | None without a MAC | Random IV per message |
| AES-CTR | Yes, as a stream | None | Unique counter per key |
| AES-GCM | Yes | 128-bit tag | 96 bits, never repeated |
| AES-GCM-SIV | Yes | Tag, reuse tolerant | Repeat leaks equality only |
| ChaCha20-Poly1305 | Yes | 128-bit tag | 96 bits, or 192 with XChaCha20 |
Modes ranked by the second property, which is the one that gets skipped
A decision order that rules out the wrong primitive first
Sequence matters more than depth, because each step eliminates a class of wrong answers before any parameter is discussed. Run these in order and stop at the first that applies.
Read the cost of starting in the wrong place. A KDF where a hash belonged costs 600,000 times the throughput on every request. A hash where a MAC belonged means anyone can mint a tag. A MAC where a signature belonged makes every verifier a forger. Encryption where a KDF belonged turns a stolen table into an offline oracle answering at hardware speed.
For a system already running, the audit runs the other way: identify the artifacts, then infer the primitive. Digests are fixed size, so length and prefix are legible: two conversions cover almost every case. Hex length is twice the byte length, which gives 32 characters for MD5 at 128 bits, 40 for SHA-1 at 160, 64 for SHA-256 at 256 and 128 for SHA-512; Base64 is ceil(n/3) x 4, turning those same 32 bytes into 44 characters. A 64-character hex column may be a hash, a MAC tag or a KDF output, and only the salt and the parameter block separate them, so read the code that wrote it, not the column width.
Two rules close the loop. Never widen a primitive after the fact: adding HMAC over records written as a bare hash protects nothing already stored, because the tag has to exist from the first write. And date every parameter next to the data it protects, since iteration counts, memory sizes and nonce budgets migrate as guidance moves, and a value that was correct a decade ago is a finding in an audit today.
- Does the input have to be recovered? AEAD encryption, plus an answer for where the key lives and how it rotates. Without that answer, fix the key story before writing ciphertext.
- Is the input a human-chosen password? A memory-hard KDF, never a hash, never a cipher, and never HMAC with a pepper standing in for a work factor.
- Must a party without the secret be unable to forge a check? A MAC when the verifying side holds the key, a signature when it must not.
- Is the data public and only accidental corruption a concern? An unkeyed hash, or a 32-bit CRC when speed matters and no adversary is present.
- Do you need a second key from a first one? HKDF from RFC 5869, which is a KDF with no work factor and the right tool for expanding key material.
| Stored value | Likely primitive | Migration |
|---|---|---|
| 32 hex characters | MD5, usually unsalted | Rehash on next login |
| 40 hex characters | SHA-1, usually unsalted | Rehash on next login |
| 64 hex characters | SHA-256, MAC tag or KDF output | Inspect salt and parameters |
| $2b$ or $2a$ prefix | bcrypt | Keep, raise the cost factor |
| $argon2id$ prefix | Argon2id, parameters inline | Keep, audit the memory budget |
| 44 Base64 characters | Any 32-byte digest | Decode before comparing |
Reading a stored credential backwards to its primitive
Frequently asked questions
- Is hashing a form of encryption?
- No. Encryption is reversible with a key, hashing is not reversible at all, and Base64 is neither because anyone can undo it. The distinction decides the design: if the plaintext must ever come back, only encryption works, and if the value is a password, neither a hash nor a cipher is the primitive, since a stolen table of either is attacked the same way.
- Is SHA-256 with a salt enough to store passwords?
- No. A salt removes precomputation and stops one guess testing every row, but it does not reduce the speed of a single guess. A GPU still reaches on the order of 10^10 SHA-256 hashes per second, so a 47.6-bit password falls in hours. Use Argon2id under RFC 9106, or PBKDF2 under RFC 8018 at 600,000 iterations for HMAC-SHA256 under current guidance.
- Why does a file hash change when I edited nothing?
- The bytes changed even if the text looks the same. CRLF conversion, a different Unicode normalisation form under UAX #15, or a gzip header holding a timestamp and filename from RFC 1952 all alter the input. Hash the raw bytes, record which conversion ran before hashing, and compare decoded buffers rather than hex strings whose case may differ.
- Is HMAC-SHA-256 still safe, or should it move to SHA-3?
- It remains sound: SHA-256 has no practical collision, and HMAC is not affected by length extension, which is the weakness RFC 2104 was designed around. Move when policy requires it, or when an interoperating system exposes a sponge interface such as KMAC under NIST SP 800-185. What does break HMAC is a weak key, a reused Poly1305 key, or a plain comparison that leaks timing.
- Can I reuse an AES-GCM nonce if the plaintext is different?
- No, and the damage cannot be undone after the fact. Under one key and nonce, two ciphertexts XOR into the XOR of their plaintexts, and the pair exposes the authentication key, which permits forging further messages. Keep a 96-bit counter that survives restarts, or use a wider nonce such as XChaCha20 with 192 bits, or AES-GCM-SIV under RFC 8452 where a repeat reveals only equality.