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.

Reviewed 10 min read

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.

QuestionPrimitiveWhat it provesCost accepted
Input must come back laterAEAD encryptionNothing, unless the tag is checkedKey custody and nonce discipline
Data must be shown unchangedUnkeyed hashIntegrity against accident onlyFixed 32-byte output, no key
Only a key holder may approveMAC (HMAC)Authorship between two key holdersTwo compression passes per message
Verifier must not be able to forgeSignatureAuthorship to a public verifierKey distribution and slower checks
Input is a low-entropy secretKDFNothing alone, only raises costTens 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 mismatchUsual causeFirst check
Values look identicalHex case or a trailing newlineNormalise, then decode to bytes
Hex compared against Base64Two encodings of one valueEncode both the same way
One file, two different gzipsHeader timestamp and filenameRecompress with a fixed time
Same text, different digestUnicode normalisation formNormalise to NFC under UAX #15
Comparison errors on lengthLength checked before compareHash 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.

ConstructionLength extensionStandardWhen it fits
Hash of key then messageYes, on MD5, SHA-1, SHA-256NoneOnly with a sponge hash
Hash of message then keyNoNoneNever: inherits collisions
HMACNoRFC 2104Shared key, default choice
KMACNoNIST SP 800-185SHA-3 and SHAKE stacks
Poly1305NoRFC 8439Inside 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.

SchemeTuned byKnown limitFit
MD5 or SHA-256, no saltNothingFull hardware speedNever
SHA-256 with a saltNothingBillions of guesses per second on GPUsNever for passwords
PBKDF2-HMAC-SHA256Iteration countGPU friendly, no memory costFIPS and interop constraints
bcryptCost factorSilent 72-byte truncationLegacy stores, short inputs
scryptN, r, pEasy to misconfigureMemory hard with tuning
Argon2idMemory, passes, lanesNeeds a memory budgetNew 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.

EstimateWhat it missesDecide with
9 characters from 94 symbols: 59.0 bitsAssumes a uniform random drawCheck blocklists and breach corpora
Four Diceware words: 51.7 bitsExact only if the dice were realInspect the generation method
12 characters: 71.5 bits over 62 symbolsSilent about reuse and contextReject known-breached strings
A meter that turns green at 12 charsIgnores the site name as a hintVerifier 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.

ModeConfidentialityIntegrityNonce rule
AES-ECBEqual blocks stay equalNoneNo nonce, do not use
AES-CBCYes, paddedNone without a MACRandom IV per message
AES-CTRYes, as a streamNoneUnique counter per key
AES-GCMYes128-bit tag96 bits, never repeated
AES-GCM-SIVYesTag, reuse tolerantRepeat leaks equality only
ChaCha20-Poly1305Yes128-bit tag96 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.

Stored valueLikely primitiveMigration
32 hex charactersMD5, usually unsaltedRehash on next login
40 hex charactersSHA-1, usually unsaltedRehash on next login
64 hex charactersSHA-256, MAC tag or KDF outputInspect salt and parameters
$2b$ or $2a$ prefixbcryptKeep, raise the cost factor
$argon2id$ prefixArgon2id, parameters inlineKeep, audit the memory budget
44 Base64 charactersAny 32-byte digestDecode 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.

Try it now