Which Algorithm Should I Use?
Which algorithm for which job, with concrete parameters and the wrong answers people actually reach for. Two of the entries here are cases where the usual advice is applied too hard rather than not hard enough.
Store user passwords so a stolen database is not a list of passwords
Argon2id
OWASP minimum: m = 19 MiB, t = 2, p = 1. Or bcrypt at cost 10 or above, or scrypt at N = 2^17, r = 8, p = 1.
A password is low-entropy, people choose them, so the only defence against an offline attack is making each guess expensive. Argon2id is deliberately slow and memory-hard, which defeats the GPU and ASIC parallelism that makes fast hashes useless here. Salting is built in, so identical passwords produce different hashes and one crack does not unlock every account that shared it.
Not this
-
SHA-256, SHA-512, or any plain digest
Built to be fast, which is precisely wrong. A modern GPU tries billions of SHA-256 candidates a second. Unsalted, identical passwords also produce identical hashes.
-
MD5, with or without a salt
Faster still, and collision-broken on top. There is no configuration that makes it acceptable here.
-
Encrypting instead of hashing
Encryption is reversible, so the key becomes a single point of total failure and you now have a key-management problem you did not need. You never need to recover a password, only to check one.
-
Your own scheme of nested hashes and salts
Iterating SHA-256 ten thousand times by hand gets the iteration count roughly right and the memory hardness completely wrong, and gets you no review. Use the library.
Raise the cost parameter until hashing takes 250 to 500ms on your own hardware, then revisit it every couple of years. It is meant to hurt.
Store API keys or session tokens you issued yourself
SHA-256, plain and unsalted
Store sha256(token). Compare in constant time.
This is the case that runs the other way, and it is the one people get wrong by being over-careful. A key you generated is 128+ bits of true randomness, so there is no dictionary to try and no brute force to slow down: the attacker's only option is to guess a random 256-bit value, which is not happening at any speed. A fast hash is entirely sufficient.
Not this
-
bcrypt or Argon2id
Costs 250ms of CPU on every single API request to defend against an attack that is already impossible. On a busy endpoint this is a self-inflicted denial of service.
-
Storing the key in plaintext
A database leak then hands over working credentials. Hashing costs nothing here and means a leak yields nothing usable.
-
Comparing with ==
String comparison short-circuits on the first differing byte, which leaks the correct prefix through timing. Use a constant-time comparison.
The distinction is entropy, not importance. Slow hashing exists to compensate for humans choosing the secret. When you chose it, it is unnecessary.
Encrypt data at rest, a file, a database column, a backup
AES-256-GCM, or ChaCha20-Poly1305
A fresh 96-bit nonce per message, never reused with the same key. Store nonce and tag alongside the ciphertext.
Both are authenticated encryption, they provide confidentiality and integrity in one construction, so tampering is detected on decryption rather than silently producing wrong plaintext. ChaCha20-Poly1305 is the better choice where there is no AES hardware acceleration, which mostly means older mobile and embedded targets.
Not this
-
AES-CBC without a MAC
Unauthenticated. An attacker who can submit ciphertexts and observe whether padding validated can decrypt the whole thing: the padding oracle attack, which has taken down real systems repeatedly.
-
AES-ECB, ever
Identical plaintext blocks produce identical ciphertext blocks, so structure survives encryption. The famous encrypted-penguin image is ECB.
-
Reusing a nonce with the same key
For GCM this is catastrophic rather than merely weak, it leaks the authentication key and lets an attacker forge messages. Random 96-bit nonces are safe up to around 2^32 messages per key; beyond that use a counter.
-
RSA
Asymmetric encryption is for small payloads such as a key, and is thousands of times slower. If you are encrypting data with RSA directly, you want a hybrid scheme instead.
Sign a JWT or an API request
EdDSA (Ed25519), or ES256 where support matters
RS256 only for interoperating with something that requires it.
Asymmetric signing means the verifier needs only the public key, so a compromised verifier cannot mint tokens. Ed25519 is fast, has small keys, and has no parameter choices to get wrong: an unusually pleasant property in cryptography.
Not this
-
HS256 when more than one party verifies
It is symmetric, the verification key is the signing key. Every service that can check a token can also issue one, so a single compromised service can mint an admin token.
-
Trusting the alg field in the header
The attacker supplies that header. Setting it to none, or to HS256 with your RSA public key as the HMAC secret, defeats a verifier that does not pin the algorithm. Pin it server-side.
-
Rolling your own with hash(secret + message)
Length-extension attacks let an attacker append to the message and produce a valid tag without the secret. HMAC exists precisely to prevent this.
Check a file downloaded intact, or has not been modified
SHA-256
Fast, unbroken, and universally available. For integrity against an adversary rather than against corruption, the digest also has to arrive over a channel the attacker does not control: a checksum served from the same compromised host as the file proves nothing at all.
Not this
-
MD5 for security purposes
Collisions take seconds. Two different files with the same MD5 can be produced deliberately, so a matching MD5 does not establish the file is the one you wanted.
-
SHA-1
Broken in practice since SHAttered in 2017. Git still uses it for object identity, which is addressing rather than defence.
-
CRC32
An error-detecting code, not a hash. Trivial to construct a collision, and it is not intended to resist one.
MD5 is still fine for non-adversarial checksums, S3 ETags, cache keys, deduplication, where nobody benefits from a collision.
Turn a password or an existing key into encryption keys
Argon2id from a password. HKDF-SHA256 from a key.
PBKDF2-HMAC-SHA256 at 600,000 iterations if you are constrained to it.
The two cases are different and use different tools. Deriving from a password needs deliberate slowness, because the input is guessable. Deriving from an already-strong key needs only to be a good pseudorandom function, and HKDF is fast because slowness would buy nothing.
Not this
-
Using the password directly as a key
AES-256 wants 256 bits of key. A password is neither the right length nor uniformly distributed.
-
sha256(password) as the key
One fast hash, so the attacker's cost per guess is one fast hash. That is the whole problem a KDF exists to solve.
-
HKDF from a password
HKDF is deliberately fast. Applied to a low-entropy input it offers no resistance to guessing at all.
Generate a token, a session id, a nonce or a salt
The platform CSPRNG
crypto.getRandomValues in a browser, crypto.randomBytes in Node, secrets in Python, crypto/rand in Go, SecureRandom in Java. 128 bits (16 bytes) is plenty; 256 for long-lived secrets.
A cryptographically secure generator is seeded from the operating system's entropy pool and its output does not reveal its internal state. That last property is the one that matters and the one ordinary PRNGs lack.
Not this
-
Math.random, rand(), or any default PRNG
Fast, non-cryptographic, and makes no security claim. V8's Math.random uses xorshift128+, whose internal state can be recovered from a short run of output: after which every future value is predictable.
-
Seeding from the clock or a UUID
Both are guessable. Timestamps have far less entropy than they appear to, and a v1 or v7 UUID encodes the time it was made.
-
random_value % alphabet_length
Biased unless the alphabet divides the range evenly. Use rejection sampling, or a library function that already does.
Prove a message came from someone holding the shared secret
HMAC-SHA256
Compare tags in constant time.
HMAC's nested construction is provably secure given a decent hash and is immune to the length-extension problem that breaks naive hash-based schemes. It is what webhook signatures from Stripe, GitHub and everyone else use.
Not this
-
sha256(secret + message)
Vulnerable to length extension with SHA-256, an attacker can append data and compute a valid tag without knowing the secret.
-
Encrypting instead of authenticating
Encryption without authentication does not prove origin, and unauthenticated ciphertext can often be tampered with meaningfully.
-
Comparing tags with ==
Timing leak, same as any secret comparison. Every crypto library ships a constant-time compare.
Common mistakes
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Choosing an algorithm before the threat model
The right primitive depends on what you are defending against. Encryption does not give integrity, hashing does not give secrecy, and signing does not give confidentiality.
Instead:State the property you need first, then pick.
Using a general-purpose hash for passwords
SHA-256 is designed to be fast, which is exactly wrong here. A GPU tries billions of candidates a second.
Instead:bcrypt, scrypt or Argon2, which are deliberately slow and salted.
Implementing the construction yourself
The primitive is rarely the weakness. Mode of operation, IV reuse, padding and key management are where real breaks happen.
Instead:Use a vetted library at the highest level it offers, such as libsodium's secretbox rather than raw AES.
What this page will not do
It will not tell you to implement any of this yourself. Every recommendation above assumes you are calling a reviewed library with its defaults: libsodium, your language's standard crypto module, the platform keystore. The algorithm is the easy part.
It also will not cover key management, which is where most real systems actually fail. Choosing AES-256-GCM is a five-minute decision. Deciding where the key lives, who can read it, how it rotates and what happens when someone leaves is the rest of the work, and no page of algorithm recommendations substitutes for it.
The three questions that decide almost every choice
Most cryptographic decisions collapse once you answer these. Nearly every wrong answer above comes from getting one of them backwards rather than from picking a weak algorithm.
Did a human choose the secret?
This single question decides whether you need a slow hash or a fast one. A human-chosen password has perhaps 20 to 40 bits of real entropy, so an attacker guesses candidates from a dictionary and your only defence is making each guess expensive: Argon2id, bcrypt, scrypt. A value you generated has 128 bits or more of true randomness, so there is nothing to guess and a fast SHA-256 is completely sufficient. Applying a slow hash there costs you real CPU on every request and buys nothing.
human chose it -> slow, memory-hard -> Argon2id, bcrypt
you generated it -> fast -> SHA-256 Does it need to be secret, or just unmodified?
Confidentiality and integrity are separate properties and need separate tools, which is why the modern answer is usually a construction that provides both. If you only need to know a message was not altered, that is a MAC or a signature and not encryption. If you need both, use authenticated encryption: AES-GCM or ChaCha20-Poly1305, rather than bolting a MAC onto a cipher yourself, which is where padding oracles come from.
Does the verifier need to be able to sign?
If one party issues and many verify, symmetric is the wrong shape, with HMAC the verification key is the signing key, so every verifier can forge. Asymmetric signing gives verifiers only the public key. If issuing and verifying happen in the same trust boundary: one service checking its own session tokens, symmetric is simpler and fine.
one service, both roles -> HMAC-SHA256 is fine
many verifiers -> EdDSA or ES256
third parties verify -> asymmetric, always And the rule underneath all of them
Use the construction your language's standard library or a well-reviewed package already provides, with its defaults. Almost every real-world break is an implementation mistake: a reused nonce, a non-constant-time comparison, a missing authentication step, a home-made scheme, rather than a broken primitive. AES is not what fails. The code around it is.