Password Attacks & Cryptography · intermediate · ~11 min
- Recognize the cryptography misuse patterns that show up most often in code review, audits, and pentests: home-rolled crypto, ECB mode, IV/nonce reuse, hardcoded keys, unauthenticated encryption, timing-unsafe comparisons, weak randomness, and JWT signing flaws. - Explain *why* each pattern is dangerous, in plain language, well enough to teach it to a teammate. - For every weak pattern, name the correct, vetted replacement (AEAD, unique nonces, CSPRNG, constant-time compare, secret vaults). - Read a small code snippet or config and spot the specific line that is the cryptographic flaw. - Write an insecure-vs-secure remediation note that a developer can act on, including how to *test* that the fix worked. - Distinguish a real finding from a false alarm (e.g. `rand()` used for a non-security shuffle is fine; `rand()` used for a token is not).
When people imagine a cryptography failure, they picture a genius cracking AES. That is almost never what happens. Modern, standard algorithms — AES, SHA-256, RSA, ECDSA — are extremely hard to break directly. The overwhelming majority of real-world cryptographic failures are misuse of good algorithms, not weaknesses in the algorithms themselves. Someone picked the wrong mode, reused a value that must be unique, pasted a key into source code, or compared two secrets with a function that leaks timing. The math was fine; the usage was broken.
This lesson builds directly on Symmetric and asymmetric encryption, your prerequisite. There you learned the building blocks: a symmetric cipher uses one shared secret key for both encryption and decryption; an asymmetric scheme uses a public/private key pair. Knowing the primitives is step one. This lesson is step two: how those primitives go wrong in practice, and how to use them safely. Together with the next lesson, Password storage: salts, slow hashes, and work factors, it forms the practical "don't shoot yourself in the foot" core of applied cryptography.
Why focus on misuse? Because misuse is what you will actually encounter. In a code review you will not be asked to break a cipher — you will be asked to look at how a cipher is called. A finding like "this AES key is hardcoded on line 42" is concrete, common, and fixable. Learning to spot these patterns, and just as importantly to recommend the correct replacement, is one of the highest-value defensive skills you can have.
A few terms used throughout:
These misuse patterns are exactly what source-code reviews, audits, and penetration tests turn up over and over: hardcoded keys, nonce reuse, timing-unsafe comparisons, and alg:none JWTs. They are not exotic. They are the bread and butter of real findings because they are easy to introduce by accident and invisible until someone looks.
The stakes are high and concrete:
alg:none lets anyone mint a token claiming to be any user, with no signature at all.Spotting the pattern is only half the job. The part that makes a finding useful is knowing the safe replacement and how to verify it. "Use AEAD and a unique nonce" turns a vulnerability report into a fix. That is the skill this lesson teaches.
Each subsection below is a distinct misuse pattern. For each: what it is, why it breaks, when the rule applies (and when it does not), and a common pitfall. Inline Knowledge check prompts ask you to reason, not to recall the quiz.
Definition. Inventing your own cipher, hash, key-exchange, or "obfuscation" instead of using a standard, vetted construction.
Why it breaks. Cryptography is adversarial: a scheme is only trustworthy after years of expert attack. A homemade scheme has had zero. XORing data with a fixed string, "scrambling" bytes, or base64-encoding (which is encoding, not encryption — it has no key) all feel like security but provide none. Attackers reverse them in minutes.
When the rule applies. Always, for anything protecting real data. The only legitimate place to implement a primitive yourself is a learning exercise in an isolated lab — never in production.
Pitfall. Confusing encoding with encryption. Base64, URL-encoding, and hex are reversible by anyone; they keep nothing secret.
Knowledge check (explain in your own words): A developer says "I XOR every byte with the constant 0x5A, so the data is encrypted." Why is this not real encryption, and what is the effective key length an attacker must guess?
Definition. Electronic Codebook mode encrypts each fixed-size block independently with the same key.
Why it breaks. Identical plaintext blocks produce identical ciphertext blocks. Structure in the data — repeated regions, patterns — shows through the ciphertext. The classic demonstration is encrypting a bitmap image in ECB: the outline of the picture is still visible.
Plaintext blocks: [AAAA][BBBB][AAAA][CCCC]
ECB ciphertext: [ X ][ Y ][ X ][ Z ] <- same input block -> same output block
^---------^ pattern leaks: blocks 1 and 3 are identical
When NOT to use it. Essentially never for real data. Prefer an AEAD mode (AES-GCM, ChaCha20-Poly1305).
Pitfall. ECB is often the default in some libraries or examples, so it gets copied without thought.
Definition. An IV or nonce is a per-message value that makes encryption non-deterministic. The rule: it must be unique for every encryption under the same key, and for some modes it must also be unpredictable/random.
Why it breaks. Reuse removes the per-message uniqueness the mode relies on:
Safe: msg1 -> nonce N1 -> ciphertext C1
msg2 -> nonce N2 -> ciphertext C2 (N1 != N2)
Broken: msg1 -> nonce N -> C1
msg2 -> nonce N -> C2 (same N, same key)
C1 XOR C2 == P1 XOR P2 <- plaintext relationship leaks; GCM auth key at risk
When the rule applies. Every encryption operation under a given key. A nonce is not a secret, but it must be unique. Generate it from a CSPRNG or a properly managed counter that can never repeat (watch out for counter resets after a crash or VM restore).
Pitfall. Hardcoding the IV to a fixed value (e.g. all zeros) "to keep decryption simple." That is the worst case: every message shares one nonce.
Knowledge check (find-the-bug): A service encrypts each user record with AES-GCM using
nonce = sha256(user_id). The user_id never changes. What property is violated, and what can an attacker do with two records belonging to the same user?
Definition. Cryptographic keys, API tokens, or passwords written literally into source code or into config files committed to version control.
Why it breaks. Source code spreads: it is shared, forked, backed up, and indexed. Once a secret is in git history it is effectively public forever, even after you delete the line — the old commit still contains it. Anyone with read access (or anyone who finds a leaked repo) gets the key.
When the rule applies. Always. Secrets belong in a secrets manager / vault, in environment variables injected at deploy time, or in a dedicated key-management service — never in the codebase. In examples and labs, use a clearly fake placeholder like API_KEY=<development-placeholder>.
Pitfall. Assuming a private repository is safe. Repos get cloned, leaked, made public by mistake, or accessed by departed employees. Treat every committed secret as compromised and rotate it.
Definition. Encrypting without also verifying integrity — using a plain mode like CBC or CTR with no MAC, instead of an AEAD mode.
Why it breaks. Encryption hides data but, on its own, does not detect tampering. An attacker can flip bits in the ciphertext and the decryptor will happily produce altered plaintext. This enables padding-oracle attacks (where error behavior leaks plaintext) and silent message forgery.
The fix: AEAD. AEAD combines confidentiality and integrity in one operation and rejects any tampered ciphertext before producing plaintext. The "Associated Data" part lets you bind unencrypted context (like a header or message ID) to the ciphertext so it cannot be moved or replayed.
Plain encryption: ciphertext -> [decrypt] -> plaintext (tampering undetected)
AEAD: ciphertext + tag -> [verify tag] --fail--> reject
--ok----> plaintext
When NOT to skip it. Never skip integrity. "It's just encrypted, who cares about tampering" is the exact reasoning that leads to padding-oracle breaches.
Pitfall. Encrypt-then-... in the wrong order, or rolling your own "encrypt + separate MAC" and getting the ordering or the compare wrong. Use a single AEAD primitive and let the library handle it.
Definition. Comparing two secrets — a MAC, a token, a password hash — with an operation that returns as soon as it finds a mismatching byte (==, strcmp, memcmp).
Why it breaks. Early-exit comparison takes slightly longer the more leading bytes match. An attacker who can measure response time can guess the secret one byte at a time: try all values for byte 0, keep the one that is measurably slower (it matched), move to byte 1, and so on. This turns an infeasible brute force over the whole value into a feasible per-byte search.
guess vs secret, early-exit compare:
AXXXXXXX -> mismatch at byte 0 -> returns fast
SXXXXXXX -> matches byte 0, mismatch at byte 1 -> returns slightly SLOWER
^ timing difference reveals "first byte is S"
Repeat per position -> recover the whole secret without ever "guessing" it whole.
The fix. Use a constant-time comparison that always examines every byte and combines the results, so the time does not depend on where the first mismatch is (e.g. accumulate differences with OR/XOR and check at the end; many libraries provide hmac.compare_digest, CRYPTO_memcmp, timingsafe_bcmp). The platform has a C exercise, constant-time-compare, for exactly this.
Knowledge check (predict the behavior): Two functions both correctly return "equal/not equal" for any input. One uses
memcmp; one accumulatesdiff |= a[i] ^ b[i]over all bytes. For correctness they are identical. For security they are not. Why?
Definition. Using a general-purpose, non-cryptographic RNG (like C's rand(), or many languages' default Math.random) to generate keys, IVs, tokens, session IDs, or password-reset codes.
Why it breaks. General-purpose RNGs are built for speed and statistical spread, not unpredictability. Their internal state is small and often seeded from the clock, so an attacker can recover the state from a few outputs and predict all future (and past) values. Predictable "random" tokens are guessable tokens.
The fix. Use a CSPRNG: the OS source (getrandom, /dev/urandom, BCryptGenRandom) or a library function explicitly documented as cryptographically secure (secrets in Python, crypto.randomBytes in Node, RAND_bytes in OpenSSL).
When the rule applies. Anything an attacker should not predict: keys, nonces, salts, session IDs, CSRF tokens, password-reset links, API keys. It does not apply to non-security uses like a game's dice roll or a UI animation jitter — there, rand() is fine.
Pitfall. Seeding a CSPRNG with a predictable value, or calling srand(time(NULL)) and thinking it makes rand() secure. It does not.
Definition. JSON Web Tokens are signed tokens carrying claims (like "user is admin"). The signature is what makes the claims trustworthy. Several classic misuses defeat that signature.
The big three:
alg:none — the token header declares "no signature." A server that honors it accepts unsigned tokens, so anyone can forge any claim.legit: header{alg:RS256} . claims . sign(private_key) verify(public_key) -> ok
attack: header{alg:none} . claims{admin:true} . (no sig) -> server MUST reject
attack: header{alg:HS256} . claims . HMAC(public_key) -> server MUST reject
The fix. Pin the expected algorithm on the verification side (do not let the token choose it), reject none, use a long random secret for HMAC, and keep signing keys in a vault. See the Web App Security track for deeper JWT coverage.
Pitfall. Trusting the alg field from the untrusted token itself to decide how to verify it. The verifier must decide the algorithm, not the attacker-controlled header.
Knowledge check (concept): Why is it a mistake for the server to read the
algvalue out of the incoming token and use that algorithm to verify the token?
This is a concept lesson, so there is no language syntax to memorize. Instead, here is the mental "grammar" of safe vs unsafe choices — a quick lookup you can apply during review.
| Need | Unsafe choice | Safe choice |
|---|---|---|
| Confidentiality + integrity | ECB / CBC / CTR alone | AEAD: AES-256-GCM, ChaCha20-Poly1305 |
| Per-message uniqueness | fixed/zero IV, reused nonce | unique nonce per message (CSPRNG or non-repeating counter) |
| Random values for security | rand(), Math.random |
CSPRNG: getrandom, secrets, crypto.randomBytes, RAND_bytes |
| Compare secrets/MACs | ==, strcmp, memcmp |
constant-time compare (compare_digest, CRYPTO_memcmp) |
| Store keys/secrets | hardcoded in source/config | secrets manager / vault / env injected at deploy |
| Sign tokens (JWT) | alg:none, weak HMAC secret, alg from token |
pin algorithm on verify, long random secret, vault keys |
| "Encrypt" data | home-rolled XOR, base64 | vetted library + standard construction |
Annotated example of the single most important habit — choosing AEAD with a fresh nonce (pseudocode, library-agnostic):
key = load_from_vault("data-key") # never hardcoded
nonce = csprng_bytes(12) # unique per message, from a CSPRNG
ct, tag = AEAD_seal(key, nonce, plaintext, # encrypt + authenticate in one step
associated_data=header) # bind context so it can't be moved
send(nonce, ct, tag, header) # nonce is not secret; it must be sent
Strong algorithms still fail when they are used incorrectly. These misuses are far more common than broken ciphers, and they are what you actually find in code review.
Rolling your own crypto. Home-made schemes are almost always broken. Use vetted libraries and standard constructions.
ECB mode (covered earlier). It leaks patterns in the data.
Static or reused IV/nonce. An IV (initialization vector) or nonce (a number used once) must be unique for every encryption, and for some modes it must also be random. Reuse breaks confidentiality. It is especially dangerous for stream ciphers and for GCM, where nonce reuse can leak the key stream and let an attacker forge messages.
Hardcoded keys and secrets. Keys placed in source code or in config files committed to a repository. This is a top finding. Search for them.
Unauthenticated encryption. Encrypting without integrity protection (no MAC, or a non-AEAD mode). This enables tampering and padding-oracle attacks. AEAD (Authenticated Encryption with Associated Data) combines encryption and integrity in one step.
Non-constant-time comparison. Comparing secrets or MACs with == or memcmp leaks timing information, which can make them forgeable. Use a constant-time comparison. The platform has a C exercise for exactly this.
Weak randomness. Using a non-cryptographic RNG such as rand() to generate keys or tokens. Use a CSPRNG instead.
JWT signing flaws. Examples include alg:none, weak HMAC secrets, and RS256-to-HS256 confusion. See the Web App Security track for more.
Cryptography is a minefield of usage, not just algorithms.
Below is a realistic insecure-vs-secure comparison. The example is a small token-issuing and token-checking flow — the kind of code that protects password-reset links or API access — written as language-agnostic, runnable-style pseudocode so the focus stays on the cryptographic decisions, not on one library's API.
WARNING: Intentionally vulnerable training example — use only in a local, isolated, authorized lab. Do not deploy.
# === INSECURE VERSION === (do NOT deploy)
SECRET_KEY = "hunter2" # (1) hardcoded, short, guessable secret
function make_reset_token(user_id):
r = rand() % 1000000 # (2) non-crypto RNG -> predictable 6-digit code
token = base64(user_id + ":" + r) # (3) base64 is ENCODING, not encryption
return token
function check_token(submitted, expected):
return submitted == expected # (4) early-exit compare -> timing leak
Now the secure version. Each fix maps to one numbered flaw above.
# === SECURE VERSION ===
SECRET_KEY = load_from_vault("reset-token-key") # (1) long random key, stored in a vault
function make_reset_token(user_id):
r = csprng_bytes(16) # (2) 128 bits from a CSPRNG -> unguessable
payload = user_id + ":" + hex(r)
tag = HMAC_SHA256(SECRET_KEY, payload) # (3) real integrity protection, with a key
return payload + "." + hex(tag)
function check_token(submitted):
payload, tag = split_last(submitted, ".")
expected = HMAC_SHA256(SECRET_KEY, payload)
if not constant_time_equal(hex(expected), tag): # (4) constant-time compare
log_event("reset_token_invalid", user=payload_user(payload)) # log attempt, NOT the token
return INVALID
return payload # token is authentic
What it does. The insecure version mints a guessable 6-digit code, "protects" it only with base64 (reversible by anyone), and checks it with a timing-leaky ==. An attacker can predict the code (small space + weak RNG), decode the base64 to read the structure, and probe the comparison by timing. The secure version generates 128 unpredictable bits from a CSPRNG, signs the payload with keyed HMAC so any tampering is detected, keeps the key in a vault, and verifies with a constant-time compare.
Expected behavior. A valid token returns the payload; an invalid or tampered token returns INVALID and logs the attempt (recording that it happened and which user, never the token value or the key). Because the token has 128 bits of entropy and the compare is constant-time, neither guessing nor timing gives the attacker traction.
Key edge cases. (a) A token with the right payload but a wrong tag must be rejected — that is the whole point of the HMAC. (b) Reset tokens should also expire and be single-use; the crypto fixes confidentiality/integrity but not replay. (c) split_last on a malformed token must fail safely (treat as invalid), not crash.
Walking the secure make_reset_token / check_token path, step by step:
| Step | What happens | What is in play |
|---|---|---|
| 1 | load_from_vault("reset-token-key") |
The secret is fetched at runtime from a secrets manager. It never appears in source or git history. |
| 2 | csprng_bytes(16) |
The OS CSPRNG returns 16 unpredictable bytes (128 bits). An attacker cannot recover the RNG state from outputs, so future tokens stay unpredictable. |
| 3 | payload = user_id + ":" + hex(r) |
The clear part of the token. It is not secret — anyone can read it — but it is about to be bound to a signature. |
| 4 | HMAC_SHA256(SECRET_KEY, payload) |
Produces a keyed tag. Only someone holding the vault key can compute a matching tag, so the tag proves authenticity and detects any change to payload. |
| 5 | return payload + "." + hex(tag) |
The wire format: readable payload plus its tag. |
Verification (check_token):
| Step | What happens | Why the result follows |
|---|---|---|
| 1 | split_last(submitted, ".") |
Separates the claimed payload from the claimed tag. Malformed input -> treated as invalid. |
| 2 | expected = HMAC_SHA256(SECRET_KEY, payload) |
Recompute the tag the server would have produced for this payload. |
| 3 | constant_time_equal(expected, tag) |
Compare every byte regardless of where a mismatch occurs. Equal time whether the first byte or no byte matches, so timing reveals nothing. |
| 4a | mismatch -> log + return INVALID | The attempt is recorded (event + user), but the submitted token and the key are never logged. |
| 4b | match -> return payload | The tag could only be produced with the key, so the payload is authentic. |
The critical insight: in the insecure version, the time taken by submitted == expected is data the attacker can read. In the secure version, that timing channel is closed because the compare's duration no longer depends on the secret's contents.
Mistake 1 — "Base64 / hex / XOR is encryption." Wrong:
token = base64("user42:admin") # "looks scrambled, so it's safe"
Why it's wrong: base64 has no key; anyone decodes it instantly. XOR with a fixed constant is equally trivial to reverse. Neither keeps a secret. Corrected: use a real keyed primitive (HMAC for integrity, an AEAD cipher for confidentiality+integrity). Recognize/prevent: if there is no key involved, it is not encryption. Ask "what does an attacker need that they don't have?" If the answer is "nothing," it is encoding.
Mistake 2 — Fixed or reused nonce "to keep things simple." Wrong:
IV = bytes(16) # all zeros, reused for every message
ct = AES_GCM(key, IV, plaintext)
Why it's wrong: under one key, a repeated GCM nonce leaks plaintext relationships and can expose the auth key, enabling forgery.
Corrected: IV = csprng_bytes(12) (or a guaranteed-non-repeating counter) for every single message; transmit the nonce alongside the ciphertext.
Recognize/prevent: any IV/nonce that is a constant, derived from stable data (like a user id), or reset on restart is a red flag.
Mistake 3 — Encrypting without authenticating. Wrong: AES-CBC with no MAC, then trusting the decrypted bytes. Why it's wrong: attackers can tamper with ciphertext undetected; padding-oracle behavior can leak the plaintext. Corrected: use an AEAD mode (GCM, ChaCha20-Poly1305) that verifies a tag before returning plaintext. Recognize/prevent: if you see a cipher mode but no integrity tag anywhere, it is unauthenticated.
Mistake 4 — Comparing secrets with == / memcmp.
Wrong:
if user_mac == real_mac: accept()
Why it's wrong: early-exit timing lets an attacker recover the MAC byte by byte.
Corrected: if constant_time_equal(user_mac, real_mac): accept().
Recognize/prevent: any equality check on a secret, token, hash, or MAC should use a constant-time function.
Mistake 5 — rand() for security values.
Wrong: token = rand() for a session ID or reset code.
Why it's wrong: predictable state -> guessable tokens.
Corrected: draw from a CSPRNG.
Recognize/prevent: scan for rand, random, Math.random, srand near anything called token/key/nonce/salt/session.
Mistake 6 — Letting the JWT choose its own algorithm.
Wrong: read header.alg from the incoming token and verify with that algorithm.
Why it's wrong: an attacker sets alg:none or swaps RS256→HS256 to forge tokens.
Corrected: hardcode the accepted algorithm(s) on the verifier; reject none.
Recognize/prevent: verification code should never branch on an attacker-supplied alg.
Because this is conceptual, "debugging" means spotting and confirming a misuse rather than fixing a compiler error. Concrete steps:
Reviewing code for these patterns
ECB, rand(, srand, Math.random, memcmp, strcmp, == near mac/hmac/token/hash, alg, none, hardcoded-looking strings near key/secret/password.Confirming a finding (mitigation verification / how to TEST the fix)
alg:none: in a local test harness, submit a token with alg:none and confirm the server rejects it. Submit an HS256 token signed with the RSA public key and confirm rejection.Logic-error symptoms to watch for
Questions to ask when something looks off
Authorization & ethics. Everything here is defensive. Practice only on systems you own or are explicitly authorized to test — localhost, containers, intentionally vulnerable lab VMs, or CTF targets. Never test these techniques against third-party systems, and never use the insecure examples outside an isolated lab. The intentionally vulnerable snippet in this lesson is a training artifact, not deployable code.
Threat model (text diagram). Knowing what you are protecting and from whom focuses the fixes.
ASSETS TRUST BOUNDARY ENTRY POINTS / ATTACKER
----------------- ---------------------- ------------------------
encryption keys ===| application / server |=== - submits tokens/JWTs
signing secrets | (holds the secrets) | - reads ciphertext on wire
user data (PT) | | - measures response timing
session tokens ===| vault / KMS (keys) |=== - reads source / git history
---------------------- - controls 'alg' header
The attacker is outside the trust boundary but can send input, observe outputs (including timing), and may read code or leaked repos. Every misuse in this lesson hands the attacker a way across that boundary.
Defensive practices, ranked.
rand() by accident.rand()/memcmp near secrets; alert on bursts of invalid-token or invalid-MAC events (a sign of byte-by-byte probing).Logging guidance. Log security-relevant events: token validation failures, signature mismatches, repeated reset attempts, with enough context (timestamp, user id, source) to investigate. Never log secrets — not keys, passwords, tokens, MACs, nonces tied to keys, or full ciphertext. A log line that contains the very secret you are protecting is itself a vulnerability.
Concrete real-world cases.
alg:none and RS256→HS256 findings; pinning the algorithm on the verifier is the standard mitigation.Professional best-practice habits.
Beginner rules (always):
<development-placeholder> in examples.Advanced rules (as you grow):
Work these in order; each builds on the last. Do not look up full solutions first — reason it through.
Beginner 1 — Spot the misuse (review drill).
Objective: classify each of five short snippets as one of the named misuses (home-rolled, ECB, nonce reuse, hardcoded key, unauthenticated, timing leak, weak RNG, JWT flaw) or "safe."
Requirements: write the misuse name and one sentence on why for each. Use snippets like IV = bytes(12) reused across calls, token = rand() % 100000, if mac == expected, KEY = "s3cr3t", and one correct AEAD-with-fresh-nonce example.
Hints: ask "is there a key?", "is the nonce unique?", "is integrity checked?". Concepts: all core concepts.
Beginner 2 — Encoding vs encryption.
Objective: demonstrate in writing why base64 is not encryption.
Requirements: take the string user42:role=admin, show its base64 form, then describe exactly the steps an attacker takes to read and to modify it (no key needed). Then state what you would use instead to (a) keep it secret and (b) detect tampering.
Example I/O: input user42:role=admin -> base64 dXNlcjQyOnJvbGU9YWRtaW4= -> decoded back to the original.
Hints: encryption needs a key; integrity needs a MAC. Concepts: home-rolled crypto, AEAD, HMAC.
Intermediate 1 — Insecure-to-secure rewrite.
Objective: take the intentionally vulnerable make_reset_token/check_token pseudocode from this lesson and rewrite it securely, then list each fix mapped to its flaw number.
Requirements: replace the hardcoded secret (vault), the weak RNG (CSPRNG, >=128 bits), the base64 "protection" (keyed HMAC), and the == (constant-time compare). Add token expiry and single-use to defend replay.
Constraints: do not invent library functions; describe them generically ("a documented CSPRNG", "a constant-time compare"). Hints: one fix per numbered flaw, plus replay. Concepts: hardcoded keys, weak RNG, integrity/HMAC, constant-time compare.
Intermediate 2 — Design a nonce strategy. Objective: specify, in writing, a nonce scheme for a service that encrypts many messages per second under one AES-GCM key. Requirements: state how nonces are generated, why they cannot repeat, what happens on process restart or crash, and how you would test (in a lab) that no nonce repeats across, say, 1,000,000 messages. Constraints: must survive restarts without reusing a nonce. Hints: random 96-bit nonces vs a persisted counter — discuss the trade-off (collision probability vs state management). Concepts: nonce reuse, CSPRNG.
Challenge — JWT verification threat model + safe verifier spec.
Objective: write a short spec for a JWT verifier that resists alg:none, weak-secret, and RS256→HS256 attacks, and a lab test plan to prove it.
Requirements: (1) a small threat-model diagram (assets, trust boundary, attacker-controlled inputs including the alg header); (2) the verifier rules (algorithm pinned on the server, none rejected, secret length/source requirements, key storage); (3) three lab test cases — a forged alg:none token, an HS256 token signed with the RSA public key, and a tampered-claims token — each with the expected result (reject) and what to log (event + reason, never the token or key).
Constraints: defensive only, localhost/lab. Hints: the verifier, not the token, decides the algorithm. Concepts: JWT flaws, constant-time compare, logging guidance.
Cryptography almost never fails because the algorithm was broken — it fails because the algorithm was used wrong. The recurring misuses are: home-rolled crypto (and mistaking encoding for encryption), ECB mode, IV/nonce reuse, hardcoded keys, unauthenticated (non-AEAD) encryption, non-constant-time comparison, weak (non-CSPRNG) randomness, and JWT signing flaws (alg:none, weak HMAC secrets, RS256→HS256 confusion).
The safe replacements are equally memorable: use vetted libraries and standard constructions; default to AEAD (AES-GCM, ChaCha20-Poly1305); generate a unique nonce per message; draw security values from a CSPRNG; compare secrets in constant time; keep keys in a vault, never in source; and on the JWT verifier, pin the algorithm and reject none.
The most common mistakes to internalize: "it looks scrambled so it's safe" (encoding != encryption), a fixed/reused nonce "for simplicity," encrypting without integrity, ==/memcmp on secrets, rand() for tokens, and trusting the token's own alg. What to remember: in review, hunt the pattern; in remediation, name the vetted fix and how to test it; and never log the secret you are protecting. All practice here is defensive and lab-only.