Locks & Keys · Nº 19

Locks and Keys

What "encrypted" actually means — and when it's a lie

AES has never been broken in the field; systems that use it are breached every week. The difference is everything around the math — which guarantee you actually needed, where the keys lived, and what "encrypted" meant in the sentence where it appeared. This guide teaches cryptography the way it fails in practice: as a discipline of naming the threat and managing the keys, so you can read an architecture diagram or a vendor pitch and know exactly what is protected, against whom, and what is theater.

Module 01 What cryptography can promise

Cryptography is the one part of security where the defender's tools genuinely work. AES has stood twenty-plus years of concentrated public attack without a practical break. SHA-256 has no known preimage attack. The primitives are, for operational purposes, finished business. And yet systems that use them correctly-ish get breached constantly — which tells you that the interesting failures live somewhere other than the math.

This module builds the two instruments you will use for the rest of the course. The first is a map: four distinct guarantees cryptography can sell you, and which primitive family sells which. The second is a question: against whom? — asked before any algorithm is named, because a control without a named adversary is a ritual, not a defense. We also open the running example that threads through every module: a security review of the wine-cellar product, a subscription wine service with a mobile app, a public API, customer webhooks, and a database full of purchase histories that people would prefer stayed private.

Unbreakable math, broken systems

Pick any large breach of the last decade and trace the root cause. You will find stolen credentials, a key committed to a repository, a misconfigured storage bucket, an injection flaw that let the attacker query data through the application's own decryption path, a token that turned out to be predictable. What you will not find is somebody factoring a 2048-bit modulus or recovering an AES key from ciphertext. Brute-forcing a 128-bit key is not an engineering problem awaiting better hardware; it is a thermodynamics problem, and the energy budget is not on anyone's roadmap.

This is not a comforting fact — it is a redirecting one. If the primitives hold, then every real failure is a failure of composition and custody: which guarantee you bought, how you wired it up, and where the keys were sitting when the attacker arrived. That is why this course spends one module each on the primitives and two on keys, randomness, and honest claims. The math is the short part.

An attacker facing an AES-256 vault: the direct path through the cipher is crossed out, while three live paths go around it — steal the key, exploit the mode, and stand where the data is decryptedAttackerAES-256the primitivebreak the cipherno practical break in 20+ years1 · Steal the keyrepo, image, env, backup → m72 · Exploit the modeno authentication, reused nonce → m43 · Stand where it's decryptedthe app server itself → m8
Figure 1.1 — Around, not through. The direct path — cryptanalysis of a modern cipher — is closed, and has been for decades. The three live paths are key theft, mode misuse, and simply operating where the plaintext already exists. Each is a later module in this course, which is why the course spends far more time on custody than on ciphers.

The four guarantees, precisely

Cryptography sells four properties, and they are separately purchasable. Conflating them is the single most expensive mistake in architecture reviews, because it produces controls that satisfy a requirement nobody had.

Confidentiality is the guarantee that only authorized parties can read the data. Integrity is the guarantee that alteration is detectable — note that cryptography never prevents someone from changing bytes, it only makes the change visible to a verifier. Authenticity is the guarantee that the data came from the claimed source. Non-repudiation is authenticity plus provability to a third party who trusts neither participant: the author cannot later plausibly deny authorship.

The legal analogies are close enough to be useful. Confidentiality is the sealed envelope. Integrity is the tamper-evident seal — it does not stop opening, it makes opening evident. Authenticity is authentication of evidence under Rule 901: this is what its proponent claims it is. Non-repudiation is the notarized signature — the thing you can put in front of a tribunal when the counterparty says I never sent that.

When it misleads

Cryptographic non-repudiation is narrower than the legal concept, and the gap is exactly the interesting part. A valid signature proves the private key produced this message. It does not prove the human associated with that key did so, or that they intended what the message says, or that the key was not compromised last Tuesday. In a dispute, the cryptography establishes a fact about a key; everything from key to person is ordinary evidence — device custody, access logs, enrollment records. Say "the key signed it," and let the rest be argued.

Two more distinctions worth internalizing now. Authenticity and integrity travel together in practice — knowing who sent it is worthless if the message could have been edited in flight, and detecting edits is worthless if you never knew who wrote the original. And confidentiality is the only one of the four that hiding the data provides; the other three require some form of verifiable computation over the data, which is why encryption alone is such a poor answer to so many requirements.

The guarantee map

Here is the reference the rest of the course hangs on. Five primitive families as rows, four guarantees as columns. Every later module teaches one row; when a claim in an architecture document confuses you, come back and find the cell.

Matrix of five cryptographic primitive families against the four guarantees, marking which primitive provides which guaranteeWhich primitive provides which guaranteePrimitive familyConfidentialityIntegrityAuthenticityNon-repudiationHashingSHA-256 · m2✓*MACHMAC-SHA256 · m4✓†Symmetric encryption (AEAD)AES-GCM · m4✓‡✓†‡Asymmetric encryptionRSA-OAEP, ECIES · m5Digital signatureEd25519, ECDSA, RSA-PSS · m6* Integrity only against a reference digest you obtained through a trusted channel — a hash next to its file protects nothing.† "Among key-holders": either party sharing the secret could have produced it, so it convinces them, not a third party.‡ Authenticated modes only. Unauthenticated encryption (raw CBC, CTR) forfeits both of these cells — see m4.
Figure 1.2 — The guarantee map. Hashing gives integrity only, and only against a trusted reference digest. MACs add authenticity but only among the holders of the shared key. Authenticated symmetric encryption gives confidentiality plus integrity and key-holder authenticity; unauthenticated modes give confidentiality alone. Asymmetric encryption gives confidentiality only. Signatures give integrity, authenticity, and non-repudiation — and are the only row with a mark in the last column.

Read it as a row and you learn what a primitive buys. Read it as a column and you learn your options: need non-repudiation, and only the bottom row will do — no amount of encryption or HMAC gets you there, because in both cases more than one party can produce the artifact. That single observation resolves an astonishing share of design arguments.

The load-bearing idea

Guarantees are purchased individually. "We encrypt it" answers exactly one of four possible questions, and in most requirements documents it is not the question being asked.

"Against whom?" before "with what?"

The discipline that makes the map usable is refusing to name an algorithm until you have named an adversary and their position. Positions are concrete: on the network path between app and API; holding a stolen backup file; holding a stolen laptop or disk; running code on the application server; a database administrator with read access; the cloud provider under legal compulsion; a former employee whose credential was never revoked. Each position defeats a different set of controls, and "we use AES-256" tells you nothing about which.

Here is the sentence pattern this course uses for every control claim, and which you should adopt in review documents. Call it the honesty test:

<Control> addresses <threat, stated as an attacker in a position>.
It deliberately does not address <a threat someone might assume it covers>.

The second line is the load-bearing one. Any control can be made to sound comprehensive by omitting it; naming a residual risk on purpose is what distinguishes a threat model from a marketing claim. It also protects you politically — a documented, deliberate gap is an engineering decision, while an undocumented one is negligence in hindsight.

The wine-cellar review charter

The running example for this course is a security review of the wine-cellar product. The charter names assets and adversaries before any technology appears:

Customer passwords — adversary: anyone holding a leaked copy of the users table, working offline (m3). API keys — adversary: the same leaked table, plus an insider with read access (m3). Webhook payloads in flight to customer endpoints — adversary: anyone who can post to a customer's public webhook URL and forge our identity (m4). Cellar data: purchase history, addresses — adversaries: a stolen database backup, a curious DBA, and separately an attacker with code execution on the app server (m7, m8). Session traffic from the mobile app — adversary: someone on the same hostile café Wi-Fi (m5, m6).

Note what is missing: no algorithms. Each later module contributes exactly one decision to this charter, and each decision carries its honesty line.

From the reader's other life

This is the same instinct as pleading with specificity. "Defendant harmed plaintiff" survives no motion; "defendant, on 14 March, in this capacity, did this act" states a claim. "The data is encrypted" is the former. "Cellar rows are encrypted at rest under per-record data keys wrapped by a KMS master key, which defeats theft of a database backup and does not defeat SQL injection through the application" is the latter — and only the latter can be evaluated.

Module 02 Fingerprints: cryptographic hashing

A hash function is the least glamorous primitive and the most widely misunderstood. It appears in password storage, git, content-addressed caches, signatures, file downloads, deduplication, blockchain, and the integrity row of the guarantee map — and in roughly half of those places, somebody on the team believes it is a form of encryption.

This module fixes the mental model. A cryptographic hash makes three separate promises that can fail separately, so "is SHA-1 broken?" is not a yes/no question but a question about which promise your use depends on. Then it draws the distinction that ends the industry's most common argument: hashing, encrypting, and encoding are three unrelated operations that people group together because all three produce output that looks like noise to a human.

What a hash is

A cryptographic hash is a deterministic function that maps an input of any length to a fixed-size output — the digest — such that computing the digest is fast and reversing it is not feasible. SHA-256 takes a byte, a novel, or a 4 GB disk image and returns 256 bits. Determinism matters: same input, same digest, forever, on every machine. Fixed size matters: the function is compressive, so infinitely many inputs share each possible output. Collisions exist, necessarily; the security claim is only that you cannot find one.

Three promises, and they fail separately:

PropertyThe promiseAttacker's task it blocks
Preimage resistanceGiven a digest, you cannot find any input that produces it.Recovering the original data from a stored digest — the property password storage leans on.
Second-preimage resistanceGiven a specific input, you cannot find a different input with the same digest.Substituting a malicious file for a known-good one whose digest was published.
Collision resistanceYou cannot find any two distinct inputs sharing a digest.Crafting a pair of documents — one benign for signing, one malicious — with the same digest.

Collision resistance is the strongest and always breaks first, because the attacker gets to choose both inputs and birthday-bound mathematics halves the effective security. That ordering is why MD5 and SHA-1 died as signature hashes years before anyone could reverse a digest of your password.

The avalanche effect

Change one bit of the input and roughly half the output bits change, unpredictably. This is the avalanche effect, and it is what separates a cryptographic hash from a checksum. A CRC also detects accidental corruption, but its structure is linear: an attacker can compute what edit to make elsewhere in the file to restore the original checksum. With SHA-256, there is no such steering — the relationship between input change and output change is designed to be structureless.

Two nearly identical inputs and their digests shown as bit blocks: a single character change flips about half the digest bitsOne character in; half the bits out"Margaux 2015"SHA-256 digest, first 16 bits"Margaux 2016"same function, one character later8 of these 16 bits flipped — and which 8 is not predictable from the input changeControl caseHash "Margaux 2015" again, on any machine, in any year:every bit identical. Determinism and avalanche are the same property seen from two sides.
Figure 2.1 — The avalanche effect. Bit blocks show the leading bits of two digests; the inputs differ by one character and about half the bits differ, with no pattern relating the two. Hashing the first input again reproduces its digest exactly. Illustrative rendering of a real property: an attacker cannot nudge the input to steer the digest, which is what makes a digest tamper-evident rather than merely error-detecting.
Note

This is also why digests are useless as "similar content" detectors. Two nearly identical contracts have completely unrelated digests. If you want fuzzy matching, you want a similarity hash, which is explicitly not a cryptographic one — and should never be used where an attacker chooses the input.

What hashes are actually for

Every legitimate use of a hash leans on a specific property. Naming that property is how you evaluate whether a deprecation announcement is your emergency or someone else's.

Integrity verification. You download a 6 GB installer and compare its digest to one published on the vendor's site. The property is second-preimage resistance: nobody should be able to produce a different file matching the published digest. Note the guarantee map's asterisk — this works only because the digest arrived through a channel you trust more than the file. A digest published next to the file it describes protects against a corrupted disk, not an attacker who serves you both.

Content addressing. Git names every object by the digest of its contents. Two identical files are automatically one object; a commit's identity depends on its tree, which depends on its blobs, so the whole history is chained and any retroactive edit changes every subsequent identifier. The property is collision resistance — which is precisely why git's migration off SHA-1 is a real, if unhurried, project.

Deduplication. The wine-cellar stores label photographs; hashing each upload and storing one copy per distinct digest saves a large fraction of the bucket. Collision resistance again: a collision means one customer's photo silently replaces another's.

Signature input. Signatures are computed over a digest rather than a message, because the message can be a gigabyte and the signing operation cannot. This is why a collision attack is fatal to a signature scheme — module 6 walks it.

Credential storage. Storing a digest instead of the secret means a database leak yields no directly usable credential. The property is preimage resistance, and for low-entropy secrets that promise is much weaker than it sounds — which is the entirety of module 3.

When it misleads

Hashing an enumerable input is not anonymization. If you hash phone numbers before sharing a dataset, an attacker generates all ten-digit numbers, hashes them, and builds a lookup table in an afternoon — the space is about 10¹⁰ candidates, which is minutes of GPU time for a fast hash. The same applies to email addresses (against any breach corpus), postal codes, dates of birth, and account numbers with checksums. Preimage resistance means no shortcut exists; it does not mean the input was hard to guess. The privacy question is always the size of the input space, never the strength of the hash.

Hash vs. encrypt vs. encode

Here is the distinction that ends the most common confusion in the industry. Three operations, grouped in people's heads because all three turn readable text into something unreadable, and otherwise unrelated.

Three panels showing the same input hashed, encrypted, and base64-encoded, comparing reversibility, key requirement, and output shapeSame input: the card number 4242424242424242HASH · SHA-25664 hex charactersfixed length, any inputNot reversible, everKey required: nonePurpose: compare and verifywithout keeping the inputGuarantee: integrityENCRYPT · AES-GCMnonce + ciphertext + taggrows with the inputReversible — with the keyKey required: yes, secretPurpose: keep the input andhide it from non-key-holdersGuarantee: confidentiality (+ integrity)ENCODE · base64NDI0MjQyNDI0MjQyNDI0Mg==4/3 the input lengthReversible by anyoneKey required: nonePurpose: move bytes throughtext-only channelsGuarantee: none whatsoever
Figure 2.2 — Hashed, encrypted, encoded. One input, three operations. The one-line test: can you get the input back, and what do you need in order to do it? Never (hash), the key (encryption), nothing at all (encoding). Encoding is a transport format, and the base64 string above decodes to the original card number in any browser console.

Base64's ubiquity in security-adjacent places is exactly why it gets mistaken for protection. JWTs are base64url; TLS certificates are base64 in PEM form; API payloads carry base64 blobs. In every one of those cases the base64 is doing transport work, and the security — if any — comes from a signature or an encryption layer inside the encoded bytes. A JWT's payload is readable by anyone holding the token. What the signature buys is that they cannot change it and have it still verify.

Cross-reference

Find the row you just met on the guarantee map in module 1. Hashing sits in the integrity column only, and only with a trusted reference digest — it never enters the confidentiality column, which is the formal statement of "a hash is not encryption." Encoding is not on the map at all, because it makes no guarantee to be mapped.

"Broken" — for what?

When a hash function is declared broken, the correct next question is which property fell. MD5 has practical collision attacks — two different inputs producing the same digest can be constructed in seconds on a laptop. It does not have a practical preimage attack: given a digest, nobody can produce a matching input. SHA-1 followed the same path; the 2017 SHAttered result produced two PDFs with identical SHA-1 digests, at substantial but affordable compute cost, and the cost has fallen since.

That distinction decides your remediation urgency. A collision attack is fatal wherever an attacker supplies content that gets signed, addressed, or deduplicated: certificate signing (a colliding pair means a CA signs the benign document and the attacker holds a valid signature over the malicious one), software update manifests, content-addressed storage. It is largely irrelevant where you hash your own data for a non-adversarial purpose — an MD5 cache key over an internally generated query string is ugly and worth replacing on the next pass through that code, but it is not an incident.

Wine-cellar decision · hashing

The review's hashing entries: SHA-256 everywhere by default; label-photo deduplication keys on SHA-256 (collision resistance matters — a collision means one customer sees another's label); webhook payload digests feed HMAC-SHA256 in module 4; API keys are stored as SHA-256 digests, which module 3 defends against the objection that all credentials need slow hashing. The one MD5 remaining is a cache key for rendered tasting-note HTML, generated from our own template inputs — flagged as cleanup, not as a finding, with the reasoning written down so the next reviewer does not re-litigate it.

The working default: SHA-256 unless a standard or a library says otherwise. SHA-3 and BLAKE3 are fine and sometimes faster; the choice among modern hashes is rarely where your risk lives. Where a standard specifies a hash — the certificate ecosystem, JWS algorithm names — follow the standard rather than optimizing.

Module 03 Hashing under attack: passwords

Module 2 ended on an uncomfortable note: preimage resistance means there is no shortcut from digest to input, and says nothing about whether the input was guessable. Human passwords are the worst case of that gap. This module is what you do about it.

The frame that organizes everything here is the shift from the online game to the offline game. Online, the attacker submits guesses to your login endpoint and you hold every advantage — rate limits, lockouts, anomaly detection, a second factor. Offline, they hold a copy of your hashes and none of that machinery exists. Password storage is the discipline of making the offline game expensive, and it is the only place in cryptography where you deliberately choose a slow algorithm.

Cross-reference · Guide Nº 15

Guide Nº 15 gave you the rules: never store passwords recoverably, use an argon2- or bcrypt-class function, salt per user, never invent your own scheme. This module supplies the mechanics underneath those rules — what each one defends against, what it costs the attacker, and where each stops working. Rules you can derive are rules you can apply to cases the rulebook did not anticipate.

The offline game

Your login endpoint is a fortress. Five attempts per minute per account, exponential backoff, lockout, device fingerprinting, a second factor for anything sensitive, and an alert if one IP address tries a thousand accounts. Against an online attacker guessing passwords, those controls are worth more than any hashing decision you will ever make.

Then the users table leaks — through a SQL injection flaw, a stolen backup, an over-permissive object storage bucket, a contractor's credential. Now every one of those controls is irrelevant. The attacker has the hashes on their own hardware and can test candidates as fast as silicon allows, with no network, no logging, and no limit. There is no lockout in a GPU.

The number that matters is guesses per second. Against a fast general-purpose hash like unadorned SHA-256, a single commodity GPU tests on the order of 10¹⁰ candidates per second; a modest rented rig with eight of them is closer to 10¹¹. Against a properly parameterized argon2id, the same hardware manages something like 10¹ to 10² per second, because each guess must allocate and traverse a large block of memory that cannot be parallelized away. Both figures are illustrative and vary by hardware generation and parameters, but the ratio — roughly eight to nine orders of magnitude — is the entire subject of this module.

Note on the candidate list

Real cracking does not enumerate all possible strings. It runs a leaked-password corpus first (hundreds of millions of real passwords, ordered by frequency), then rule-based mutations of that corpus — capitalize, append a year, substitute a zero for an o — then masks for common structures. This is why "eight characters with a symbol" is a much weaker requirement than its search space suggests: humans satisfy it in predictable ways, and the rules encode exactly those ways.

Salt: killing the batch discount

Without salt, a password hash is a pure function of the password, which hands the attacker an enormous economy of scale. Hash the top billion passwords once and you have a table that inverts every unsalted database in the world, forever — that is a rainbow table, or in its simpler modern form, a sorted lookup file. Worse, within a single leaked table, identical hashes reveal identical passwords: you learn which accounts share a password and which password is most popular, and you crack thousands of accounts with one success.

A salt is a per-user random value, stored in the clear alongside the hash and mixed into the computation. It makes each user's hash a function of password and salt, so the precomputed table is worthless — the attacker would need one table per salt value, which is the whole point.

Two users with the same password: unsalted hashing produces identical digests that a precomputed table inverts, while salted hashing produces distinct digests and forces per-user workUnsalted — one table inverts everythingamara · "Bordeaux1"dmitri · "Bordeaux1"9f3c1e… (identical)Precomputed table hitboth accounts cracked at onceSalted — the table is worthlessamara · salt a7f2…dmitri · salt 4c19…e2b7a0… (distinct)1d84f6… (distinct)No shared workattacker pays per user, from scratchSalt removes the attacker's economies of scale. It does not slow down a single guess — that is the work factor's job.
Figure 3.2 — What salt actually does. Two users choose the same password. Unsalted, they produce the same digest, which reveals the shared password and lets one precomputed table crack both — and every other database using that algorithm. Salted, the digests differ, precomputation is worthless, and the attacker must run a separate campaign per user. Salt buys the destruction of amortization; it buys nothing per guess.

Three consequences follow, and the third is where people go wrong. First, salts must be unique per user — a single application-wide salt restores most of the amortization within your database. Second, salts must be random, from a CSPRNG, not a username or an email address, because those are predictable enough for an attacker to precompute against a known target. Third, salts are not secrets. They live in the same row as the hash, they leak with it by design, and a scheme whose security depends on the salt staying hidden is misdesigned. If you want a secret in the calculation, that is a pepper, and it comes later in this module.

Slow by design

Salt removes the discount. The work factor raises the price. A password-hashing function such as Argon2, scrypt, or bcrypt is built to be deliberately, tunably expensive: a cost parameter controls how much computation each single hash requires, and in the memory-hard families a second parameter controls how much memory it must touch.

The asymmetry is the whole design. Your legitimate user pays the cost once, at login, where a quarter of a second is invisible. The attacker pays it per candidate, per user. If a guess costs 250 ms of one CPU core plus 64 MB of memory, a rig that could have run 10¹⁰ SHA-256 guesses per second runs perhaps a few hundred argon2id guesses per second, because the memory requirement is what defeats GPU and ASIC parallelism — you can put thousands of hashing cores on a die, but you cannot give each of them 64 MB of fast memory.

Two-lane comparison of offline cracking against fast-hashed versus salted argon2id passwords, with attacker throughput and time to crack annotatedSame leaked table, same 8-character human password, two storage choicesLane A · SHA-256, salted, no work factorleaked table120k rows→ one GPU rig →~10¹⁰ guesses/secondmemory-light, fully parallelhoursmost of the table fallsSalt did its job — no precomputation — and the attacker did not care, because each guess costs nanoseconds.Common corpus + mutation rules crack the frequent passwords long before exhaustive search is needed.Lane B · argon2id, salted, 64 MB · 3 iterationsleaked tablesame 120k rows→ same GPU rig →~10¹–10² guesses/secondmemory-hard, parallelism blockedcenturiesfor the same candidate setThe user pays ~250 ms once per login. The attacker pays it once per candidate, per user, with no way to batch.Weak and reused passwords still fall — the work factor buys time, not immunity.Figures illustrative, order of magnitude only; actual throughput varies with hardware generation and parameters.
Figure 3.1 — The offline cracking gap. Both lanes start from the same leaked, salted table and the same attacker hardware. The fast hash allows roughly 10¹⁰ guesses per second and the table largely falls within hours; argon2id at 64 MB and three iterations allows roughly 10¹–10² per second, moving the same work to centuries. All figures illustrative and order-of-magnitude. The asymmetry is the design: one cost per login for the user, one cost per guess for the attacker.

Choose parameters by measuring, not by copying. Pick the latency you can afford at your login volume — 200 to 500 ms of server time is a common landing zone — and raise memory and iterations until you hit it on production-class hardware. Then write the parameters into the stored hash string, as argon2 and bcrypt both do by design, so that raising them later does not invalidate existing rows: on the next successful login you verify with the stored parameters and immediately re-hash with the new ones.

Pepper: the key that isn't in the database

Salt and work factor both assume the attacker has your hashes. A pepper attacks that assumption. It is a single server-side secret — a random key, not per-user — mixed into the password hashing, most cleanly by computing an HMAC of the password under the pepper before it goes into argon2, or by encrypting the finished hash under a key held outside the database.

What it buys is a change in the attacker's requirements. Without pepper, "they stole the database" is sufficient for offline cracking. With pepper, they need the database and the application secret, which are different systems with different access paths and, usually, different backup regimes. For an attacker whose foothold was a leaked read-only replica credential or a misplaced backup file, that difference is total: no pepper, no guessing, no matter how weak the passwords.

The load-bearing idea

A pepper only helps if it lives somewhere the database backup does not. Pepper stored in a configuration table, in the same database, in the same nightly dump, is a decoration — the attacker who has the hashes has it too. The pepper belongs in a secrets manager or KMS, injected at runtime, backed up separately, with its own access policy. Module 7 is where that placement gets built.

The cost is operational and worth stating honestly. Rotating a pepper is awkward: you cannot recompute hashes without the plaintext passwords, so rotation means versioning — tag each row with the pepper version used, verify with the old one, re-hash with the new one on next successful login, and carry both until the population has turned over. That is a real burden, which is why pepper is a sensible addition for a system holding consumer credentials and an over-engineering flag for a two-person internal tool. Choose it knowingly.

When fast hashing is right: API keys

The wine-cellar's API keys are stored as bare SHA-256 digests, no salt and no work factor, and that is the correct design. Understanding why is the test of whether the previous four sections landed as mechanics or as ritual.

The defense budget tracks the entropy of the input, not the sensitivity of the data it protects. A password is a human-chosen string with perhaps 20 to 40 bits of real entropy, drawn from a distribution an attacker knows better than the user does; the whole apparatus of salting and slow hashing exists to compensate for that weakness. An API key is 32 bytes from a CSPRNG, rendered as wc_live_ plus 43 base64url characters — 256 bits of entropy with no distribution to exploit. There is no candidate list. At 10¹⁰ guesses per second, exhausting even a 128-bit space takes longer than the age of the universe by an enormous margin. Slowing each guess by a factor of 10⁸ changes nothing that matters, because the attacker was never going to make the guesses.

What the slow hash would cost is concrete: API keys are verified on every request, not once per session. Adding 250 ms and 64 MB per verification to a service handling a few hundred requests per second is not a security improvement, it is a capacity incident. And the salt would break the lookup: with a per-row random salt you cannot find the row by hashing the presented key, so you would need an unsalted index anyway or a linear scan of every key in the table.

The load-bearing idea

Slow hashing compensates for low-entropy inputs. It is not a sacrament applied to all credentials. Ask "how many candidates does the attacker have to try?" — if the answer is 10⁹, buy time with a work factor; if the answer is 2²⁵⁶, spend the latency somewhere useful.

Wine-cellar decision · password and key storage

Passwords: argon2id, 64 MB memory, 3 iterations, 4 lanes, per-user 16-byte CSPRNG salt, parameters encoded in the stored string, pepper applied as HMAC-SHA256 of the password under a key held in the secrets manager and injected at boot. Measured login cost on production instance class: about 240 ms.

API keys: generated as 32 CSPRNG bytes, shown to the customer exactly once, stored as SHA-256 with a key-id prefix for lookup, compared in constant time.

Honesty line: this addresses offline cracking of a leaked or stolen credential table, including a stolen backup, whose contents become useless without the separately held pepper. It deliberately does not address credential stuffing or phishing, where the attacker obtains the plaintext password by other means and our hashing is bypassed entirely — those are Guide Nº 15's controls: breach-corpus screening at registration, step-up authentication, and a second factor.

Module 04 Shared secrets: symmetric encryption

Symmetric encryption is where the bulk of the world's data protection actually happens: one key, held by both parties, used to encrypt and decrypt. Every TLS connection, every encrypted disk, every encrypted database column ends up here, because symmetric operations are fast enough to run over gigabytes and asymmetric ones are not.

The cipher itself is not where you can go wrong — AES has held for two decades and you are not going to break it or improve it. Everything in this module is about the three decisions around the cipher: whether the mode authenticates, whether the nonce is used exactly once, and whether you needed authenticity without confidentiality in the first place, which is what a MAC is for. Each of these has a canonical failure, and each canonical failure has shipped in production at companies with security teams.

AES, the workhorse

AES is a block cipher: a keyed, reversible transformation on 128-bit blocks, standardized in 2001 after an open international competition, implemented in hardware on every server and phone you will deploy to. With a 128- or 256-bit key it is the default for essentially all bulk symmetric encryption, and ChaCha20 is its equally respectable stream-cipher counterpart, generally preferred where hardware AES acceleration is absent.

Brute force is not a threat model here. Exhausting a 128-bit keyspace is not slow engineering — it is a thermodynamic impossibility: the energy required to merely count to 2¹²⁸ in ideal irreversible computation exceeds anything a civilization-scale power budget makes available. AES-256 is chosen over AES-128 for long-lived data and standards compliance, not because 128 is close to falling.

Note

The practical consequence of this section is a reallocation of attention. Since the cipher will not fail, every symmetric-encryption incident you ever investigate will be about a mode, a nonce, a key, or a boundary. Argue about those in design review, and let the cipher choice take thirty seconds.

What AES alone does not give you is any structure for messages longer than 16 bytes. That is the mode of operation's job — CBC, CTR, GCM — and the mode is where the guarantees are decided. The next two sections are entirely about that choice.

Encryption without authentication is broken

Here is the module's central misconception, stated the way it is usually said aloud: "the field is encrypted, so it can't be tampered with." It is false, and the reason is a property called malleability.

Take AES-CTR, or any stream construction. The cipher generates a keystream, and the ciphertext is plaintext XOR keystream. Flip bit 47 of the ciphertext and bit 47 of the decrypted plaintext flips too — no key required, no detection. If the attacker knows the record layout, and record layouts are rarely secret, they can turn "role":"user" into something else, or subtract a digit from a quantity. CBC is malleable differently and no better: an attacker who alters a ciphertext block garbles that block on decryption and makes a controlled, bit-precise edit to the next block. Either way, the ability to make targeted plaintext edits without the key is fully available.

Worse, the receiver's error behavior can leak the plaintext itself. In a padding oracle attack, the attacker submits modified ciphertexts and observes only whether decryption reported a padding error, a parse error, or success — a single bit of feedback per attempt. That bit, applied byte by byte, recovers the entire plaintext without ever recovering the key. This is not exotic: it broke real frameworks, real payment systems, and real VPN products, and it works through nothing more than a difference in response codes or timings.

Anti-pattern · encrypt-then-hope

Symptom: a design that names a cipher and a mode ("AES-256-CBC") with no tag, no MAC, and no mention of what happens when decryption fails. Why it is wrong: the ciphertext is malleable and the error path is an oracle. The corrective: use an authenticated mode, and if you truly cannot, apply encrypt-then-MAC over the ciphertext and the IV, verify the MAC in constant time, and refuse to decrypt at all on mismatch. Never MAC the plaintext and encrypt the result; that ordering reintroduces the oracle.

Authenticated encryption

Authenticated encryption (AEAD) — AES-GCM, ChaCha20-Poly1305, AES-SIV — collapses the two operations into one primitive that cannot be assembled wrongly. Sealing takes a key, a nonce, the plaintext, and optional associated data, and produces ciphertext plus an authentication tag. Opening takes the same key, nonce, associated data, and the ciphertext-plus-tag, and either returns the plaintext or fails. There is no third outcome, and critically, no plaintext is released before the tag verifies.

AEAD seal and open: sealing produces ciphertext and a tag; opening verifies the tag before releasing any plaintext, and a tampered input is rejected with no outputSEALplaintext recordkeynonce (unique)associated datarecord id, not encryptedAES-GCMciphertext+ 16-byte tagOPENciphertext + tagverify tag firstover ciphertext + assoc. datatag valid → plaintextreleased only nowtag invalid → rejectno output, no oracleonebit offeedbackFail-closed decryption is the property that removes the padding-oracle class of attack entirely.
Figure 4.1 — Seal and open. Sealing consumes plaintext, key, a unique nonce, and optional associated data, and emits ciphertext with a 16-byte authentication tag. Opening verifies the tag over the ciphertext and associated data before releasing any plaintext, so a tampered input produces a single outcome — rejection with no output — rather than the graded error signals an attacker can use as an oracle.

The associated-data parameter is the underused half of AEAD and worth designing deliberately. It is data that is authenticated but not encrypted, bound into the tag. Encrypt a wine-cellar record's contents with the record's id and tenant id as associated data, and a ciphertext lifted from row 5001 will not open in row 5002 — the tag check fails. Without that binding, an attacker with database write access can move valid ciphertexts around and produce well-formed lies out of authentic components. Use it for the record identity, the tenant, and any context that must not be interchangeable.

Cross-reference · the guarantee map

Return to Figure 1.1's third row. AEAD sits in the confidentiality, integrity, and authenticity-among-key-holders columns, with the footnote that unauthenticated modes forfeit the last two — that footnote is this section. AEAD never reaches the non-repudiation column, and no configuration of it will: the receiver holds the same key, so a third party cannot distinguish sender from receiver.

Nonce discipline

A nonce is a number used once — per key, without exception. AES-GCM's nonce does not need to be secret or unpredictable; it needs to be unique. Repeat one under the same key and the failure is not a gradual weakening. It is total, and it is total in two directions at once.

Timeline showing two messages encrypted under the same key and nonce, producing the same keystream, allowing an attacker to XOR the ciphertexts and cancel it outSame key, same nonce, two messages09:14 · message 109:41 · message 2P₁ (order, card last4)⊕ keystream(K, N)= C₁P₂ (order, card last4)⊕ keystream(K, N) — identical= C₂C₁ ⊕ C₂ = P₁ ⊕ P₂the keystream cancels — attacker needs no key at allConsequence 1 · plaintext structure recovered: known or guessable fields in either message peel the other open.Consequence 2 · in GCM, nonce reuse also leaks the authentication subkey — the attacker can forge valid tags.
Figure 4.2 — The nonce-reuse catastrophe. Two messages encrypted under the same key and nonce use the identical keystream, so XORing the two ciphertexts cancels it and yields the XOR of the plaintexts — recoverable with nothing but knowledge of typical message structure. Under AES-GCM there is a second, worse consequence: repeated nonces expose the authentication subkey, letting the attacker forge tags on messages of their choosing. Nonce reuse is a total break, not a degradation.

Two safe strategies, and you should be able to say which one your system uses. A counter: maintain a monotonic sequence per key, which is exact and cheap but requires state that must survive restarts, and must never be shared across two processes encrypting under the same key — the classic failure is a container image with a snapshot counter that restarts at zero. Or random: draw 96 bits from a CSPRNG each time, which needs no state but relies on collision probability staying negligible, which it does for GCM's 96-bit nonce until you approach roughly 2³² messages under one key — after which you rotate the key. AES-SIV and XChaCha20-Poly1305 exist precisely to blunt this edge: SIV degrades gracefully on reuse and XChaCha's 192-bit nonce makes random collision a non-issue. If your system generates many messages per key from distributed workers, choose one of those.

MACs: authenticity from a shared secret

Sometimes you do not need confidentiality at all. A webhook payload going to a customer's endpoint is not secret from that customer — they are the intended reader. What they need is a way to know it came from you and was not altered or replayed. That is authenticity and integrity without confidentiality, and the primitive is a HMAC: a keyed hash producing a tag that anyone holding the shared secret can compute and verify, and nobody else can produce.

Swimlane sequence of webhook signing: the wine-cellar API canonicalizes a payload, computes an HMAC over timestamp and body, sends it in a header, and the customer endpoint recomputes, compares in constant time, and checks the timestamp windowWine-cellar APICustomer endpointcanonicalize body bytesexact bytes sent, no re-serializationtag = HMAC-SHA256(secret,timestamp + "." + body)POST /hooks · X-Cellar-Signature: t=…,v1=…recompute tag over raw bodyusing their copy of the secretconstant-time compareno early-exit string equalitytimestamp within 5 minuteselse reject — replay defenseForged POST from anyoneno secret → no valid tag →rejected before parsing
Figure 4.3 — Webhook signing, end to end. The wine-cellar computes HMAC-SHA256 over the timestamp and the exact body bytes under a per-customer secret and ships it in a header. The receiver recomputes over the raw body it received, compares in constant time, and rejects anything outside a five-minute timestamp window. Anyone can POST to the public webhook URL; only a holder of the secret can produce a tag that verifies, and a captured request stops working after five minutes.

Three implementation details carry most of the failures. Sign the exact bytes: if the receiver parses JSON and re-serializes before verifying, key ordering and whitespace will change the digest and verification will fail intermittently in ways that lead teams to disable it. Compare in constant time: a normal string comparison returns early on the first differing byte, and that timing difference is enough to let an attacker discover a valid tag byte by byte; use the library's compare_digest-equivalent. Include a timestamp inside the signed material and enforce a window: the MAC proves authorship, not freshness, so without it a captured request is replayable forever.

When it misleads

The industry calls this "webhook signing," and the word invites a conclusion it does not support. An HMAC convinces the key-holders and nobody else, because the wine-cellar holds the same secret and could have produced any tag the customer received. If a payload might one day be evidence in a commercial dispute — a purchase instruction, a price confirmation — a MAC will not arbitrate it. That requires an actual digital signature, where only one party can produce the artifact, which is module 6.

Module 05 The introduction problem: asymmetric cryptography

Module 4 left a hole large enough to drive a product through. Symmetric encryption requires both parties to already hold the same key. Your mobile app has never met your API server, has no shared secret with it, and the only channel between them is the one an attacker might be sitting on. How does the first secret get established?

This is the introduction problem, and it is the problem asymmetric cryptography was invented to solve. Notice what it is not: it is not "asymmetric encryption is stronger." Asymmetric operations are orders of magnitude slower than AES, constrained in message size, and awkward for streams. Every real protocol uses them for the introduction and then hands the conversation to symmetric crypto — the hybrid pattern, and the thing TLS is doing under the hood of every request your product makes.

The key-distribution deadlock

Symmetric cryptography assumes the hard part is already done. Two parties who share a secret can protect everything between them; two parties who do not share a secret cannot establish one over a channel an attacker is watching — anything you send to agree on a key, the attacker also receives.

The deadlock has a second face at scale. If every pair of participants needs its own shared secret, a network of n participants requires n(n−1)/2 secrets: 10 services means 45 secrets; 100 means 4,950; the public internet means a number with no meaning. Every one of those has to be generated, delivered securely, stored, rotated, and revoked. Before 1976 the honest answer to "how do we get the key to them?" was couriers and sealed pouches, which worked for governments and banks and for nobody else.

The load-bearing idea

Asymmetric cryptography exists to solve key distribution among strangers, not to encrypt data better. Every design decision in this module follows from that framing, and most design mistakes come from forgetting it — using asymmetric encryption where symmetric belongs is not a strengthening, it is a category confusion with a performance bill attached.

Public and private keys

An asymmetric keypair is two mathematically linked values with an intentional asymmetry: what one key does, only the other can undo, and deriving the private key from the public one is infeasible. One half is published to anyone who wants it; the other never leaves its holder. That single structural fact yields two distinct capabilities, and keeping them separate in your head prevents most confusion in this area.

Two columns comparing what the public key can do — encrypt toward the holder and verify signatures — against what the private key can do: decrypt and signOne keypair, two capabilitiesPUBLIC KEYprinted on the business cardEncrypt toward the holderanyone can lock; only the holder opensVerify a signatureanyone can check authorship → m6Safe to publish, post, and indexdisclosure is the intended statePRIVATE KEYnever leaves the holderDecrypt what was sent to methe only party who canProduce a signaturethe basis of non-repudiation → m6Disclosure is total compromiseidentity and past decryption both fallThe two capabilities are independent: a key used for signing should not also be used for encryption, and standards keep them apart.
Figure 5.1 — What each key can do. The public half lets anyone encrypt toward the holder and verify the holder's signatures; publishing it is the intended state. The private half lets its holder — and only its holder — decrypt those messages and produce signatures; disclosing it compromises both identity and confidentiality. The two capabilities are separate, which is why certificates carry key-usage constraints and why signing and encryption keys are kept distinct in practice.

Two clarifications that prevent later errors. First, the folk phrase "encrypt with the private key to sign" describes a historical RSA implementation detail, is not how modern signature schemes are constructed, and is meaningless for elliptic-curve signatures where no encryption operation exists to repurpose. Signing is its own operation; module 6 says this again at greater volume because it matters. Second, a public key on its own is an anonymous mathematical object. Nothing about it says whose it is — and that gap between key and identity is the entire subject of module 6.

Key exchange and forward secrecy

The most elegant use of the asymmetry is not encryption at all. In a Diffie-Hellman key exchange, each party generates a keypair, sends the public half in the clear, and combines its own private half with the other's public half. The mathematics is arranged so that both computations produce the same value, while an observer who saw only the two public halves cannot compute it. Neither party ever transmits the secret; both derive it. Two strangers, one public channel, one shared secret — the deadlock broken.

Now the crucial operational question: which keys are used for that exchange? If the server always uses the same long-term keypair, then an attacker who records years of traffic and later steals that key can go back and derive every session's secret. If instead each connection uses a freshly generated, immediately discarded keypair — an ephemeral exchange — then the session secret cannot be reconstructed after the fact, because the private values that produced it no longer exist anywhere. That property is forward secrecy, and it is why TLS 1.3 removed non-ephemeral key exchange from the protocol entirely.

State the consequence precisely, because this is a question you will be asked in an incident. A stolen long-term server key lets the attacker impersonate the server going forward — they can complete handshakes as you until the certificate is revoked and the revocation is honored. With forward secrecy, it does not let them decrypt any recorded past session. Without it, everything they recorded opens at once.

Note · post-quantum, positioned

The threat that drives post-quantum migration is exactly the one forward secrecy addresses in the classical case: harvest now, decrypt later. An adversary recording encrypted traffic today could decrypt it once a sufficiently capable quantum computer exists, because the key-exchange mathematics — not the symmetric cipher — is what such a machine would break. Migration is therefore underway for key exchange first (hybrid classical-plus-post-quantum exchanges are already deployed in major browsers and cloud services) and signatures next, while AES and SHA-2 need only larger parameters. Nothing else in this guide changes; the discipline of naming threats and managing keys is identical.

Hybrid encryption: introduce, then converse

Asymmetric operations are expensive — orders of magnitude slower than AES per byte — and structurally limited: RSA encryption cannot handle a message longer than its modulus, and no asymmetric primitive is designed for streaming gigabytes. So no serious protocol uses them for bulk data. Instead: use asymmetric cryptography once, to establish a symmetric session key; then encrypt everything with AEAD under that key. That is hybrid encryption, and it is the shape of TLS, SSH, Signal, PGP, S/MIME, and every envelope-encryption scheme in module 7.

Client and server swimlane sequence of a TLS 1.3 handshake showing ephemeral key exchange in the first round trip, derivation of the symmetric session key, certificate and signature authenticating the server, and application data flowing under AEADWine-cellar appapi.cellar.exampleGuide Nº 14 alignmentClientHello +key_shareServerHello +key_sharehandshake keysderived hereCertificate +CertificateVerifyapplication data1-RTT totalephemeral public value + supported suitesserver ephemeral public value + chosen suitederive session keysnothing secret was sentderive session keyssame value, independentlycertificate + signature over the handshake · m6now the client knows who it derived a key withGET /cellar — AES-GCM under the session key · m4responses, same key, bulk speed
Figure 5.2 — Hybrid encryption, mapped onto TLS 1.3. The client offers an ephemeral key-exchange value in its first message and the server answers with its own, so both sides derive the same session key within one round trip without ever transmitting it. The server then presents its certificate and a signature over the handshake — module 6's machinery — which is what turns "I share a key with somebody" into "I share a key with api.cellar.example." All application data then flows under AEAD with the derived symmetric key, at module 4's speeds. The side rail names the corresponding TLS 1.3 messages so this can be laid beside Guide Nº 14's handshake diagram.
Cross-reference · Guide Nº 14

Lay this figure next to Guide Nº 14's TLS handshake diagram and the alignment is exact: key exchange completes in the first round trip, the certificate arrives after key establishment (and is itself encrypted under the handshake keys), and application data follows in one round trip total. If you have seen the older description — client encrypts a session key with the server's RSA public key and sends it over — that is TLS 1.2-era static key exchange, and it was removed in 1.3 precisely because it has no forward secrecy: recording that one message and later stealing the server key decrypts the entire session.

Read the figure once more and notice the honest gap. After the second flight, the client has a session key and no idea who holds the other copy. An attacker who intercepted both messages and substituted their own key-exchange values would have completed a perfectly confidential channel — to themselves. Key exchange gives you a secret with whoever you talked to; it never tells you who that was. Everything that closes this gap is a signature, and module 6 is where it closes.

Module 06 Proving authorship: signatures and certificates

Module 5 ended on an unfinished channel: the app and the server share a key, and the app does not know who holds the other copy. This module finishes it. Digital signatures let one party produce something only they could have produced and anyone can check; certificates apply that mechanism to identity claims; the chain of trust is what makes the whole arrangement work between parties who have never met.

Cross-reference · Guide Nº 14

Guide Nº 14 walked the TLS handshake and told you the browser "verifies the certificate." That was a stand-in for the machinery in this module. By the end you will be able to say what the certificate asserts, who signed it, what the browser checks in what order, and — the part that matters most in review — what a green padlock does not prove.

Digital signatures, precisely

A digital signature is a value computed from a message and a private key, such that anyone holding the corresponding public key can verify the pair (message, signature) and nobody without the private key can produce a value that verifies. Signing operates on the message's digest for practical reasons — the message may be gigabytes and the signing operation works on a fixed-size input — which is why a collision attack against the hash is fatal to the whole scheme: two messages sharing a digest share every signature.

Sign and verify flow: the signer hashes a message and produces a signature with the private key; the verifier recomputes the hash and checks the signature with the public key, accepting or rejectingSIGNER · holds the private keyrelease manifestSHA-256 digestsign(digest, privkey)produced with — not encrypted withsignature(message + signature travel together, over any channel, public)VERIFIER · holds only the public keyreceived manifestrecompute digestverify(digest, sig, pubkey)a check, not a decryptionvalidrejectWhat a valid signature tells the verifierIntegrity — the message is byte-identical to what was signed.Authenticity — it was produced by the holder of that private key.Non-repudiation — and a third party can check this too,because the verifier could not have produced it.
Figure 6.1 — Sign and verify. The signer hashes the message and produces a signature using the private key; the verifier independently hashes the received message and checks the signature against the public key, yielding exactly two outcomes. Because verification requires only the public key, a third party can perform it — which is what makes signatures, alone among the primitives, deliver non-repudiation.
A description to stop using

You will hear that "a signature is the hash encrypted with the private key." Do not repeat it. It came from a superficial reading of textbook RSA, where signing and decryption happen to share an underlying operation — but even for RSA the real scheme (RSA-PSS) involves randomized padding and is not an encryption of anything. For ECDSA and Ed25519, which sign the overwhelming majority of traffic you will encounter, there is no encryption operation in the algorithm at all: nothing is being encrypted, and the phrase describes a computation that does not exist. The binding language: a signature is produced with the private key and verified with the public key.

With both MACs (module 4) and signatures on the table, the choice between them becomes a decision with criteria:

DimensionMAC (HMAC)Digital signature
Key arrangementOne secret shared by both partiesPrivate key held by signer; public key distributed freely
Who can verifyOnly holders of the secretAnyone, including parties who trust neither side
Third-party provabilityNone — either party could have produced itYes — this is non-repudiation
CostMicroseconds; effectively freeMilliseconds; matters at high volume
Scaling to many verifiersPoor — a shared secret per pairExcellent — one public key serves everyone
Right forWebhooks, session tokens, internal service callsSoftware releases, certificates, disputable instructions, anything a third party audits

The rule of thumb: if the verifier and the producer are the same trust domain and no outsider will ever adjudicate, a MAC is simpler, faster, and sufficient. The moment a third party must be convinced — a court, an auditor, a user's device that never met you — you need a signature.

Certificates: signed identity claims

A certificate is a document containing a public key, a name, a validity period, and some constraints, signed by somebody else. That is the whole idea. It is the mechanism from the previous section applied to a specific kind of message: an assertion that this public key belongs to this name. Everything mystical about the certificate ecosystem is organizational, not cryptographic.

The fields that carry meaning in a modern web certificate:

FieldWhat it means
Subject Alternative Name (SAN)The names this certificate is valid for. This is the field browsers check; the legacy Common Name is no longer authoritative.
Public keyThe key being bound to those names — the entire point of the document.
IssuerWhich certificate authority signed this, and therefore whose key verifies the signature.
Validity (notBefore / notAfter)The window during which the assertion is offered. Short windows limit the damage from an undetected key compromise.
Key usage / extended key usageConstraints on what the key may be used for — server authentication, code signing, certificate signing. This is what stops a leaf certificate from being used to issue others.
Basic constraints (CA true/false)Whether this certificate may sign other certificates. A missing or unchecked CA flag has caused real, catastrophic breaks.

Note which fields are absent from that list: the organization's honesty, its security posture, the content of the site. A certificate authority (CA) issuing a domain-validated certificate has verified exactly one thing — that the requester controlled the domain at issuance time, typically by answering a challenge on it. That is a meaningful check and a narrow one.

Note

Certificate Transparency changes the accountability picture without changing the mechanics. Every publicly trusted certificate must be logged in append-only public logs, so a mis-issued certificate for your domain is discoverable rather than invisible. That is why domain monitoring of CT logs belongs on your detection list: it is the control that catches a CA issuing for cellar.example to somebody who is not you.

The chain, link by link

A CA cannot sign every certificate with a key that sits online in an issuance service — that key would be the single most attractive target on the internet, and its compromise would be unrecoverable. So the ecosystem uses a chain: a root whose key stays offline, intermediates that do the daily work, and leaves for actual servers.

Chain of trust from root certificate authority through an intermediate to the leaf certificate for the wine-cellar domain, with signing arrows downward and the browser's verification walk upwardIssuance — downward, "signs"Verification — upwardRoot CA — ISRG Root X1self-signed · already in the device trust storeprivate key kept offline, in an HSM, ceremonially accessedasserts: nothing about anyone — it is the anchor you chose to trustsignsIntermediate CA — R11CA:TRUE · may issue · online, does the daily signingshipped by the server, not the trust storeasserts: the root vouched for this issuing keysignsLeaf — api.cellar.exampleCA:FALSE · SAN: api.cellar.example · 90-day validitypublic key matching the server's private keyasserts: this key belongs to this name, until this date1 · leaf signaturechecks against R11's key2 · R11 signaturechecks against the root's key3 · root is in the truststore → walk terminatesPlus, atthe leaf:name, dates,revocation
Figure 6.2 — The chain of trust, walked both ways. Issuance runs downward: the offline root signs an intermediate, the online intermediate signs the leaf for api.cellar.example. Verification runs upward: the client checks the leaf's signature using the intermediate's key, checks the intermediate's signature using the root's key, and stops when it reaches a root already present in its trust store — additionally checking that the requested name matches a SAN, that the dates are current, and that the certificate has not been revoked. Intermediates exist so that the root's key can stay offline and so a compromised issuing key can be replaced without removing the anchor from every device on earth.

The root's special status is worth stating plainly, because it is where the recursion stops. Its certificate is self-signed, which proves nothing — anyone can self-sign anything. It is trusted because it is already in your device's trust store, put there by your operating system or browser vendor under a root program with audit requirements. That is an out-of-band trust decision made on your behalf, and the entire chain is a way of extending that one decision to millions of servers you have never heard of.

Verification walks the chain in reverse

When your app connects to api.cellar.example, the server presents its leaf certificate and usually the intermediate. The client then performs a sequence of checks, and knowing them in order lets you diagnose certificate errors precisely instead of by ritual.

One: does the requested hostname match a SAN entry on the leaf? Two: is the current time inside the leaf's validity window? Three: does the leaf's signature verify under the intermediate's public key, and does the intermediate permit issuance (CA:TRUE, appropriate key usage)? Four: does the intermediate's signature verify under a key in the trust store — is a root reached? Five: has anything in the chain been revoked, checked by OCSP stapling or a browser's CRL-set equivalent? And finally, the handshake itself: the server signs the transcript with the private key corresponding to the leaf's public key, proving it actually holds that key rather than merely possessing a copy of somebody's certificate.

Now the useful reframing for incidents. A certificate error is not a statement that an attack is happening. It is a statement that the identity claim could not be verified, and the causes are ranked by frequency: an expired certificate, a missing intermediate in the server's chain, a hostname that does not match the SAN, a client clock that is wrong, a corporate middlebox intercepting traffic, or — least often, but the reason all the machinery exists — an actual impersonation attempt. Diagnose which, and fix that.

Anti-pattern · turning validation off

Symptom: verify=False in a Python request, curl -k in a deploy script, rejectUnauthorized: false in a Node client, or a globally installed "trust all certificates" handler in a mobile app — typically added to unblock a release, with a comment saying "temporary." Why it is wrong: it does not weaken verification, it removes it. The connection is still encrypted, which is why it still looks fine, and it is encrypted with whoever answered — every machine on the path becomes a silent interception seat, with no error to notice later. "It's internal traffic" is not a defense; internal networks are where lateral movement happens and where service-to-service credentials flow. The corrective: for internal services, run a private CA and put its root in the trust store of the services that must talk to it, or pin the expected certificate or public key. For third-party endpoints, fix the chain. The one thing that is never the answer is off.

Wine-cellar decision · transport identity

Public API and web endpoints use publicly trusted certificates with 90-day automated renewal, so an expiry incident requires two consecutive automation failures rather than one forgotten calendar entry. Service-to-service traffic inside the VPC uses mutual TLS with certificates from a private CA whose root is distributed with each service image — both sides present certificates, so a stolen service credential alone is not enough to call the payments service. The iOS app pins the public-key hash of our leaf and one backup key, with a documented rollover procedure. CT logs are monitored for issuance of cellar.example certificates we did not request.

Honesty line: this addresses server impersonation by an attacker on the network path, including hostile Wi-Fi and DNS hijacking. It deliberately does not address a compromised or coerced CA issuing a valid certificate for our domain to someone else — for that, our control is detection through CT log monitoring and pinning in the app, not prevention.

Module 07 The real hard part: keys and randomness

Six modules have built primitives that work. This module is about the two things that decide whether any of it matters in your system: where the keys are, and where the randomness came from. Both are unglamorous, both are operational rather than mathematical, and between them they account for nearly every cryptographic failure you will ever investigate.

The thesis stated bluntly: the math held; the keys didn't. A key sitting in a repository is not protected by AES. A key readable by the same process an attacker just compromised is not protected by anything. And a key generated from a predictable source was never a secret at all — it just looked like one, which is worse, because everything downstream reported success.

Most crypto failures are key failures

A short catalogue, all of them recurring rather than exotic:

Keys in source control. A cloud access key committed during a late-night debugging session, removed in the next commit, and permanently present in the repository history. Automated scanners find these within minutes of a push to a public host, and "the repository is private" is a weak defense: it is cloned onto laptops, mirrored by CI, granted to contractors, and occasionally made public by accident.

Keys baked into images. A Docker image with an environment variable set at build time, or a mobile app binary with an API key in its constants. Both are distributed artifacts; anyone with the image or the app has the key, and mobile app decompilation is a routine exercise.

Keys readable by the compromised process. A secrets manager is a real improvement and it has a limit: once the application fetches a secret, that secret is in the memory and often the environment of the process an attacker has just taken over. This is not an argument against secrets managers; it is the boundary of what they buy.

Keys that never rotate. A signing secret shared with a partner in 2019, still in use, known to four people who have left the company since. Nothing detects this. There is no alarm for a secret whose blast radius has quietly grown for six years.

Keys in the wrong blast radius. One API key shared by every environment, so a leak from a staging box that a contractor could log into is a production compromise.

The load-bearing idea

Every one of these is an attack that went around the math, exactly as module 1 predicted. Which means key custody is not an operational chore appended to a cryptographic design — it is the cryptographic design. A review that spends twenty minutes on cipher modes and two minutes on where the key lives has its budget inverted.

Where keys live

Storage options form a spectrum, and the useful way to rank them is not "more secure" but "which attacker does this defeat?" That framing turns a dogma argument into an engineering one and lets you defend a modest choice honestly.

A five-tier spectrum of key storage from hardcoded in source to hardware security module, each tier annotated with the attacker it defeats and the attacker it does notKey storage, ranked by the attacker it defeats1 · Hardcoded in sourceDefeats: nobody. Not: anyone with repo read, repo history, CI logs, a clone, or the built artifact.2 · Environment variables / config files on the hostDefeats: repo access, casual code review exposure.Not: host access, process listing, crash dumps, image layers.3 · Secrets manager, fetched at runtimeDefeats: repo, image, backup, and host-at-rest access; adds rotation and audit.Not: a live compromised process.4 · KMS — the key never leaves; you send data, not fetch keysDefeats: everything above, plus theft of the key itself; every use is authorized and logged.Not: an attacker with your IAM role.5 · HSM / dedicated hardwareDefeats: extraction even with physical access; tamper-evident and tamper-responsive.Not: misuse via authorized calls.Wine-cellar placement — pepper and DB credentials: tier 3 · cellar master key: tier 4 · webhook secrets: tier 3 · local dev fixtures: tier 2, non-production only.
Figure 7.1 — Where keys live. Each tier is defined by the attacker it stops and the attacker it does not. Note the shape of the progression: tiers 1 through 3 progressively remove the key from artifacts an attacker might steal at rest, while tier 4 changes the model entirely — the key never leaves the service, so the attacker's target becomes your IAM permissions rather than your key material. No tier defeats an attacker who is live inside the authorized process, which is module 8's subject.

The critical shift happens between tiers 3 and 4. A secrets manager gives your application a copy of the key; a KMS never does. You send the KMS data and an instruction — encrypt this, decrypt this, sign this — and it returns the result. The key material is not in your process, not in a core dump, not in a log, and not exfiltratable by a memory-disclosure bug. What the attacker can steal instead is your permission to make those calls, which is a genuinely different problem: it is bounded in time, visible in CloudTrail-style logs, revocable with an IAM change, and rate-limitable.

Tier 2 deserves an honest word, because dogma here is unhelpful. Environment variables on a hardened host, with no shell access, no debug endpoints, and a small blast radius, are a defensible choice for a small service — they defeat the most common real-world leak, which is repository exposure. What they do not survive is any capability to read the host: a process listing, a crash dump that captures the environment, a debug endpoint that prints config, or an image layer that baked them in. Choose tier 2 knowing that list, not by default.

KMS and envelope encryption

KMS solves key custody and creates a throughput problem: you cannot send a million database rows through a network service on every page load, and cloud KMS APIs have both size limits (a few kilobytes) and request quotas. Envelope encryption resolves it, and it is module 5's hybrid pattern applied to storage instead of transport.

Generate a fresh symmetric data key per record, per object, or per tenant. Encrypt the data locally with that key using AEAD — fast, local, unlimited size. Then ask the KMS to encrypt (wrap) the data key under the master key, store the wrapped data key next to the ciphertext, and discard the plaintext data key from memory. To read a record, send the wrapped key to the KMS, get the plaintext data key back, decrypt locally, discard.

Envelope encryption: a KMS master key wraps per-record data keys, the database stores ciphertext beside wrapped keys, a thief with the database alone can open nothing, and rotation rewraps keys without touching dataKMS BOUNDARYmaster key (CMK)never exits this boxcalls: wrap / unwrapevery call authorized + loggedAPPLICATION MEMORYdata key, plaintextonly during the operationAES-GCM over the record,then discard the plaintext keyDATABASE ROWciphertextwrapped data keystored side by sideThief steals the database onlyGets: ciphertext + wrapped data keys.Can open: nothing. Unwrapping needs a KMS call.Changes if they also hold IAM permission to call decrypt.RotationNew master key version → unwrap each data key,rewrap under the new version, store it back.Record ciphertext is never touched. 40 TB stays put.The pattern is module 5's hybrid encryption applied to storage: an expensive, guarded key protects a cheap key; the cheap key does the bulk work.Per-record data keys also bound the damage from any single data-key exposure to one record.Cache unwrapped data keys briefly if KMS call volume is a concern — and state the cache lifetime in the design.
Figure 7.2 — Envelope encryption. The master key never leaves the KMS; each record gets its own data key, which exists in plaintext only in application memory during the operation and is stored wrapped alongside the ciphertext. An attacker who exfiltrates the entire database obtains ciphertext plus wrapped keys and can open none of it — unless they also hold the IAM permission to call the KMS, which is where the defense actually lives. Rotating the master key rewraps the small data keys and never rewrites the data.

Read the thief panel carefully, because it contains the honest limit. Envelope encryption converts "steal the data" into "steal the data and obtain KMS decrypt permission." Against a leaked backup, a misconfigured snapshot, a decommissioned disk, or a database-only compromise, that is decisive. Against an attacker who has taken over the application role — which holds decrypt permission by necessity — it is not, and the controls that matter there are IAM scoping, per-tenant key policies that make bulk decryption anomalous, request-rate alarms on the KMS, and the detection work of module 8.

Rotation without re-encryption

"We cannot rotate the master key; we would have to re-encrypt forty terabytes" is a sentence that reveals a missing envelope layer rather than a real constraint. In an envelope design, the master key protects only the data keys, which are 32 bytes each. Rotation unwraps each data key under the old master version and rewraps it under the new one. The record ciphertext is not read, not rewritten, and not touched. Rotating protection over 40 TB of data becomes an operation over a few hundred megabytes of keys, and cloud KMS products further reduce it by retaining old key versions so old wrapped keys stay openable while new writes use the new version.

Be precise about what rotation buys, because the compliance framing usually is not. It bounds the exposure window of an undetected key compromise: a key rotated quarterly can decrypt at most a quarter's worth of newly written data if the previous version leaked. It gives you a rehearsed procedure, which is the actual point — the first time you rotate a key should not be during an incident. It supports credential hygiene after departures and vendor changes. And it produces the evidence auditors ask for.

When it misleads

Rotation does not un-leak anything. If an attacker exfiltrated plaintext data, or copied ciphertext plus a data key, rotating the master key changes nothing about what they already hold. Rotation is a forward-looking control that limits future exposure, and treating it as a remediation for a completed breach is a category error that shows up in incident reports as "we rotated all keys" offered in place of an impact assessment.

What genuinely does require re-encryption is a change of algorithm or data-key scope, or a compromise of a specific data key. Design for that too: store a key version and algorithm identifier with each record so a migration can proceed lazily — on read, if the record is on the old scheme, re-encrypt under the new one and write it back. The same lazy-migration shape as module 3's password rehash on login.

Dice matter: randomness

Every secret in this course begins as a random number. Keys, nonces, salts, session tokens, password-reset tokens, CSRF tokens, MFA recovery codes, ephemeral key-exchange values. If the number was predictable, the secret was never secret — and nothing anywhere in the stack reports an error, because a predictable random number is indistinguishable from a good one to every layer that consumes it. This is the quietest failure mode in the guide.

The distinction to hold: a general-purpose PRNG (Math.random(), C's rand(), random.random(), anything seeded from a clock) is designed for statistical properties — uniform distribution, long period, speed. Given some output, its future output is computable. A CSPRNG (crypto.getRandomValues, Python's secrets, /dev/urandom, java.security.SecureRandom) is additionally designed so that observing output reveals nothing about past or future output, and it is seeded from operating-system entropy. Both produce numbers that look random to a human and to a histogram; only one survives an adversary.

Two paths for generating a password-reset token: a CSPRNG produces an unpredictable token, while a time-seeded generator lets an attacker who knows the approximate timestamp regenerate the sequence and forge the tokengenerate reset tokenPOST /password/resetCSPRNGsecrets.token_urlsafe(32)seeded from OS entropy256 bits, unguessableno shortcut from prior tokenstime-seeded PRNGrandom.seed(time()); random()looks random in every testattacker regenerates itrequest a reset, note the second,try the few thousand candidatesHistorical instance — Debian OpenSSL, 2008: a patch removed entropy from seeding, so the whole population of generated SSH and TLS keyscollapsed to roughly 2¹⁵ possibilities per key type. The keys were correct in every format check, and enumerable in minutes.
Figure 7.3 — Predictable dice. The same feature, two generators. A CSPRNG yields a token an attacker cannot narrow down. A time-seeded PRNG yields one they can: request a reset for the victim, note the approximate second, enumerate the few thousand plausible seeds, and regenerate the token. Nothing in either path logs an error — which is why randomness bugs are found by reading code, not by monitoring.

Two historical failures worth carrying, because both are precise:

Debian OpenSSL, 2008. A maintainer removed lines flagged by a memory-analysis tool that happened to supply entropy to the random number generator. For roughly two years, keys generated on affected systems were drawn from a space of about 2¹⁵ possibilities determined mostly by process id. Every generated SSH host key, TLS key, and OpenVPN key from those systems was enumerable. Nothing failed visibly; the keys were structurally valid and worked perfectly.

Sony PlayStation 3, 2010. ECDSA signatures require a per-signature random value, and the requirement is as strict as module 4's nonce rule: reuse it under the same key and the private key can be recovered algebraically from two signatures. Sony's implementation used a constant. The code-signing private key was extracted and arbitrary code could be signed as genuine. Note the shape — this is module 4's nonce sin appearing at the signature layer, with the same total-failure character.

Wine-cellar decision · key custody and randomness

Cellar records are encrypted at rest with per-record data keys under AES-GCM, wrapped by a KMS master key with a key policy allowing decrypt only to the API service role, denying it to the analytics and backup roles. Data-key unwrap calls are cached for 60 seconds and rate-alarmed at ten times the p99 baseline. Master key rotates annually; the procedure is rehearsed twice a year in staging. Password pepper, database credentials, and per-customer webhook secrets live in the secrets manager, injected at boot, never written to disk. All tokens, salts, keys, and nonces come from platform CSPRNGs, with a lint rule failing any build that references Math.random or random.random in server code.

Honesty line: this addresses theft of the database, a backup, a snapshot, or a decommissioned disk — none of which yields readable cellar data — and it addresses key exposure through source control or images. It deliberately does not address an attacker who obtains the API service role, which by design can decrypt; there, the controls are IAM scoping, KMS call-rate alarms, and detection, not cryptography.

Module 08 The three "encrypted"s and the honesty test

Everything so far was mechanism. This module is judgment, and it delivers the promise the course opened with: after it, you should be unable to hear the word "encrypted" without automatically asking which of three things it means, and unable to accept a security claim that has not named the attacker it stops.

Two instruments get built. The first is the honesty matrix — attacker positions against the three states of data — which turns most encryption arguments into a table lookup. The second is a red-flag lexicon for vendor claims, ending in a reusable eight-question list and a worked teardown of a real-sounding pitch. The second instrument is the one you will use in procurement, in architecture review, and in the paragraph of an incident report where somebody wants to write "customer data was encrypted."

Three states of data

Data exists in three states, and each has its own encryption story, its own attacker, and its own limits. Saying "encrypted" without specifying which is the ambiguity that lets weak systems sound strong.

In transit. The data is moving between two machines and is encrypted for the journey — TLS, almost always. The attacker it stops is on the network path: hostile Wi-Fi, a compromised router, an ISP, a passive tap at an exchange. It stops protecting the moment the data arrives and is decrypted, which is the point of arriving.

At rest. The data is sitting on storage and is encrypted there — full-disk encryption, database or column encryption, encrypted object storage, encrypted backups. The attacker it stops is one who obtains the storage medium without the running system's authority: a stolen laptop, a decommissioned drive, an exfiltrated backup file, a public snapshot, a cloud provider's departing employee walking out with a disk. It does not stop anyone who asks the running application nicely, because the application decrypts by design.

In use. The data is in memory, decrypted, being processed — because computing on data generally requires seeing it. This is the state with the weakest protections and the highest share of real breaches. Confidential computing (enclaves, encrypted memory) and homomorphic techniques are genuine frontiers, but they are narrow, slow, and rarely deployed for general workloads; assume, in a review, that in-use data is plaintext to anyone who owns the process.

The load-bearing idea

Every wine-cellar decision in this course was qualified for this reason. Passwords are not "encrypted" at all — they are hashed, which is a different guarantee. Cellar records are encrypted at rest under envelope encryption. Mobile traffic is encrypted in transit under TLS 1.3. And while the API is serving a request, that customer's records are in use — plaintext, in memory, protected by access control and nothing cryptographic.

The honesty test

Cross the three states with the attacker positions from module 1 and the arguments resolve themselves.

Matrix of four attacker positions against in-transit, at-rest, and in-use encryption, showing which combinations are protected and which are notDoes this encryption stop this attacker?Attacker positionIn transit (TLS)At rest (disk / envelope)In use (memory)On the network pathhostile Wi-Fi, tap, ISPn/an/aHolding a stolen disk or backupsnapshot, decommissioned driven/an/aOn the application serverSQL injection, RCE, stolen service roleThe provider or an insideradmin access, subpoena, rogue employee"✕" means the control does not stop this attacker — not that the control is worthless. Read across the app-server row: that is where breaches happen.The test, in sentence form:"<Control> addresses <attacker in a position>. It deliberately does not address <a threat a reader might assume it covers>."
Figure 8.1 — The honesty matrix. Each encryption state protects against exactly one attacker position and is silent about the others. The third row carries the module's load-bearing line: an attacker on the application server defeats all three, because the application holds the authority to decrypt and the attacker is wearing the application. The last row is why end-to-end encryption is a distinct claim — provider-held keys mean provider access, however the storage is configured.

Look at the app-server row and read it as an incident answer. When an attacker exploits an injection flaw and queries data through your application, the at-rest encryption performs exactly as designed: the application asks the KMS to unwrap a data key, is authorized, decrypts, and returns the plaintext. Nothing failed. The control was never aimed at that attacker, and the only honest sentence is that it was not.

From the reader's other life

This is limitation-of-liability reading. A clause that says "we will protect your data" means nothing until you find the definitions section and the carve-outs. "Encrypted" is the same: an unqualified term whose operative meaning lives in the state, the key custody, and the threat model. A litigator does not accept the recital; do not accept the recital.

At-rest theater

The most common misuse of encryption in the industry is not a broken algorithm. It is a true statement deployed to answer a question it does not answer.

The pattern: a framework requires encryption of sensitive data at rest. The team enables full-disk encryption on the database volumes, or turns on the storage service's default encryption, and the control is marked satisfied. The auditor is satisfied too, correctly — the requirement said at rest and the data is encrypted at rest. Then the breach arrives through SQL injection in a search endpoint, and the attacker reads plaintext through the application's own credentials, because the volume is mounted and decrypted on a running host and every query returns cleartext. The post-incident statement says customer data was encrypted at rest, which is true, and irrelevant, and readers will hear it as reassurance about an event it had no bearing on.

Anti-pattern · compliance-checkbox encryption

Symptom: a control described only by the framework clause it satisfies, with no named adversary — "we encrypt data at rest per the standard." Ask what attacker it stops and nobody in the room has an answer, because the answer was never required. Why it is wrong: the control is real and its scope is unexamined, so the organization holds a belief about its coverage that no one has tested. The corrective: attach a threat statement to every control in the register — for provider-managed at-rest encryption, that is "defeats theft of the physical medium, an exposed snapshot, or an exfiltrated backup; does not defeat any attacker who can query through the application." Keep the control, shrink the claim.

Cross-reference · Guide Nº 04

Guide Nº 04 covered how controls without threat models pass audits and fail incidents. This is the canonical instance, and it is worth carrying the general form: a control register that records which clause each control satisfies, and not which adversary it defeats, will accumulate coverage without accumulating security. The remedy is one column, and it is the column nobody wants to fill in, because filling it in is where you discover that three controls all defeat the same attacker and none defeats the one you have.

To be clear about the corrective: the answer is never to skip at-rest encryption. It is cheap, it is genuinely decisive against a whole class of real incidents — the exposed snapshot, the resold drive, the backup in a public bucket — and envelope encryption with per-tenant keys meaningfully raises the bar even against some database-level compromises. The discipline is claiming it for what it does.

End-to-end, if the ends are yours

End-to-end encryption is a specific and strong claim: only the communicating endpoints hold the keys, and the provider in the middle cannot read the content — not because policy forbids it, but because they do not possess the means. Under breach, under subpoena, under a rogue administrator, the answer is the same: they have ciphertext.

That is a much stronger claim than "encrypted in transit and at rest," which nearly every service can say truthfully while retaining full access to your content. Because the phrase is valuable, it is used loosely, and the loose usage is detectable with one question:

The one-question test

Besides the endpoints, who can read this — and what capability would they need? If the honest answer includes the provider under any circumstance, it is not end-to-end encrypted, whatever the marketing page says.

Two panels comparing true end-to-end encryption, where the provider holds only ciphertext and breach or legal process yields nothing, against provider-held keys, where plaintext is visible to the provider and reachable by breach or subpoenaTRUE END-TO-ENDsenderholds keyrecipientholds keyproviderciphertext onlybreachsubpoenaboth reach ciphertext and stopmetadata is still exposedPROVIDER-HELD KEYSsenderrecipientproviderholds keys → plaintextbreachsubpoenaboth reach plaintextstill truthfully "encrypted in transit and at rest"
Figure 8.2 — End-to-end versus provider-held keys. The message flow is identical; only key custody differs, and that difference decides everything an attacker or a court can obtain. On the left the provider holds ciphertext, so breach and legal process both terminate there — though metadata (who talked to whom, when, how much) remains exposed in both designs. On the right the provider holds keys, so both paths reach plaintext, while the service can still truthfully advertise encryption in transit and at rest.

Three tells reveal provider-held keys reliably, and you can check all three from the product's own documentation:

Server-side search over content. If the provider can full-text search your messages or documents on their servers, they can read them. Encrypted-search techniques exist and are narrow, slow, and loudly advertised when actually deployed; ordinary search is decisive evidence.

Web access with no key ceremony. If you can log in from a borrowed browser with only a password and immediately read your history, the keys came from the provider. Real end-to-end products make this awkward on purpose — device linking, a recovery phrase, a key backup you must set up — and that awkwardness is the feature.

Full account recovery with content intact. "Forgot your password? No problem, everything is still here." If losing all your secrets does not lose your data, someone else has the keys. Compare with a real E2E product, where losing every device and the recovery phrase means the data is gone — a support burden they accept because it is the guarantee.

The red-flag lexicon, and rolling your own

You do not need a mathematics degree to evaluate cryptographic claims. You need to notice which claims are unfalsifiable, which are true but uninformative, and which contradict each other. Here is the lexicon, with the reusable question attached to each entry — the question is the part you take into the meeting.

PhraseWhat it actually meansThe question that resolves it
"Military-grade encryption"Marketing for AES-256, which is also what your phone, your bank, and every free messaging app use. Not a lie; not information.Which algorithm, in which mode, protecting which state of the data, with keys held by whom?
"Proprietary algorithm"An inversion of Kerckhoffs's principle: real cryptography is secure because the design is public and has survived scrutiny. Secrecy of design is a warning, not a moat.Which published standard is this, and where is the public specification?
"Unbreakable" / "impossible to hack"An unfalsifiable claim, and one no competent cryptographer makes about a system.Under what threat model, and what is the residual risk you accept?
"Patented encryption"A patent is a legal filing, not a security review. It says the design is novel and disclosed, not that it is sound.Has it been analyzed in published cryptographic literature?
"Independently audited"Potentially the most valuable claim on the page — or the emptiest, depending on four facts nobody volunteers.Which firm, what scope, what date, and may I read the report?
"Bank-level security"Undefined. Banks run some excellent systems and some 1990s mainframes.Against which specific control framework, assessed by whom?
"Zero-knowledge"Sometimes a precise cryptographic term, more often a synonym for "we say we don't look."Can you decrypt customer data if compelled by a court? A yes or no.
"Keys stored securely"Contentless without a location and a boundary.Stored where, readable by which process, and rotated how?

Then the rule that generates all the others: you do not roll your own. The rule is well known and usually understood too narrowly, as if it only prohibited inventing a cipher. Almost nobody does that. What people do constantly is invent protocols and compositions out of standard parts — a hand-rolled token format that concatenates fields and appends a truncated hash; a session scheme that encrypts a user id with AES-CBC and treats decryptability as authentication; a custom handshake between two internal services; XOR with a rotating secret because the data "just needs light protection."

These fail for exactly the reasons the primitive-inventors fail. The bugs are in composition — order of operations, what is covered by the tag, what happens on error, whether the same key does two jobs — and composition bugs are invisible to testing because the system works perfectly for well-behaved inputs. Every failure in this course is a composition failure: unauthenticated encryption, reused nonces, MACs used where signatures were needed, peppers stored beside hashes.

The load-bearing idea

The corrective is never "be more careful." It is naming the boring standard tool: TLS for transport, an AEAD library for data, argon2 for passwords, JOSE or PASETO or your framework's signed sessions for tokens, your platform's KMS for keys, libsodium when you need a primitive directly. If you cannot name a standard tool for what you are building, that is the signal to stop and ask a cryptographer — not the signal that you have found an unserved niche.

The teardown

Now put it together on a real artifact. A vendor pitching the wine-cellar a customer-data platform writes:

"All customer data is protected with military-grade proprietary
encryption. Keys are stored securely in our database. Our platform
is end-to-end encrypted and independently audited."

Four sentences, and every one of them is answerable.

"Military-grade" — most likely AES-256, which is unremarkable and fine. Not a finding on its own; a finding when it is offered as the answer, because it names an algorithm where the question was about keys and states.

"Proprietary encryption" — this contradicts the previous phrase. "Military-grade" implies a standard (AES); "proprietary" says they did not use one. Both cannot be true, and the contradiction is the most informative thing in the paragraph: either the marketing copy is unsupervised, which tells you about their review culture, or there is a homemade scheme in the product, which is the roll-your-own anti-pattern in its original form.

"Keys stored securely in our database" — this is the finding with teeth. Keys beside the data they protect defeat the purpose entirely: any compromise that reaches the data reaches the keys, so at-rest encryption stops nothing beyond physical theft of the medium. This is module 7's spectrum, failed at tier 1 or 2, and it is stated in their own words.

"End-to-end encrypted" — directly contradicted by the previous sentence. If they hold the keys, the ends are not the endpoints. Ask the one-question test and ask whether they can produce customer data under subpoena; the answer will be yes.

"Independently audited" — unassessable as written. This is the one to mark plausible, verify rather than condemn: a real audit by a named firm with a published report would be genuinely reassuring and would change several other answers. Ask for the four facts.

The reusable artifact — eight questions for any vendor

Send these before the security questionnaire; the answers tell you which questionnaire responses to disbelieve.

1. For each of in transit, at rest, and in use, what is encrypted and with what? (m8 §1)
2. Who holds the keys, where do they live, and which of your processes can read them? (m7 §2)
3. Can you produce our plaintext data if compelled by a court or a valid legal request? (m8 §4)
4. Which published standards and libraries implement each of these — name them. (m8 §5)
5. How are keys rotated, how often, and does rotation require re-encrypting data? (m7 §4)
6. How are customer passwords stored — algorithm and parameters? (m3)
7. Who audited this, what was the scope, when, and may we read the report? (m8 §5)
8. Name one attack your encryption does not defend against. (m1, m8 §2)

Question 8 is the one that separates vendors. A team with a threat model answers it immediately and specifically — "our at-rest encryption does nothing against a compromise of your own API credentials." A team without one hears an accusation and reaches for reassurance, and that reaction is the datum you were buying.

Concept index

Confidentiality
The guarantee that only authorized parties can read the data.
Integrity
The guarantee that alteration of the data since it was protected is detectable.
Authenticity
The guarantee that data actually came from the claimed source.
Non-repudiation
The guarantee that the author cannot later plausibly deny authorship — provable to a third party.
Threat model
An explicit statement of what you protect, against which attacker, in which position.
Cryptographic hash
A deterministic one-way function mapping any input to a fixed-size digest.
Digest
The fixed-size output of a hash function; the input's fingerprint.
Collision resistance
Infeasibility of finding any two inputs that share a digest.
Preimage resistance
Infeasibility of recovering an input from its digest.
Avalanche effect
A one-bit input change scrambles roughly half the output bits.
Encoding
A reversible, keyless format transformation such as base64; not protection.
Salt
A per-user random value mixed into a password hash to defeat precomputation.
Work factor
The tunable cost of each computation in a deliberately slow password hash.
Pepper
A server-side secret mixed into password hashes, stored apart from the database.
Argon2
The current-generation deliberately slow, memory-hard password-hashing family.
AES
The standard symmetric block cipher; the bulk-encryption workhorse.
Authenticated encryption (AEAD)
Encryption that fails closed on tampering, giving confidentiality and integrity together.
Nonce
A number used once per key; reuse under one key is a catastrophic failure.
HMAC
A keyed hash giving integrity and authenticity among holders of a shared secret.
Key exchange
Deriving a shared secret with a stranger over a public channel.
Forward secrecy
Past recorded traffic stays unreadable even if a long-term key is later stolen.
Hybrid encryption
Asymmetric crypto to establish a session key, symmetric crypto for the bulk data.
Digital signature
A private-key computation over a message that anyone can verify with the public key; provable to third parties.
Certificate
A signed claim binding a public key to a name for a validity period.
Certificate authority (CA)
An entity whose business is verifying and signing identity claims.
Chain of trust
Root CA to intermediate to leaf; verification walks it in reverse to a trust-store root.
KMS
A managed service holding master keys that never leave it; callers request operations, not keys.
Envelope encryption
Per-record data keys wrapped by a KMS master key; rotation rewraps keys, not data.
CSPRNG
A cryptographically secure random generator; the only acceptable source for keys, nonces, salts, and tokens.
End-to-end encryption
Only the communicating endpoints hold keys; the provider cannot decrypt, even under compulsion.

This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.