Vault lab
A password manager or an encrypted volume makes one promise: without the passphrase, the bytes say nothing, and nobody can change them without it showing. Six exhibits build that promise from its parts with the browser's own cryptography: stretch a passphrase into a key, seal with authenticated encryption, never reuse a nonce, split a key between people, give a vault two doors, and prove that nothing changed.
Everything runs in this tab through Web Crypto (PBKDF2, HMAC, SHA-256, AES-GCM and AES-CTR); nothing you type is stored or sent, and only your progress is saved. Every key, passphrase and file on the page is a throw-away demo value made here. Where a lesson needs fixed numbers it uses a seeded generator, and it says when a value is random per visit. Attacker speeds are illustrative, never a benchmark.
Lab exploredYou have stretched a passphrase and priced the guessing, sealed and tampered, watched a reused nonce cancel out, split a key so that too few shares say nothing, built a vault with two doors and shredded one, and proved a file's place in a tree. The Entropy lab shows why every sealed byte on this page reads as noise.
A passphrase becomes a key
A cipher wants 256 random bits; people remember words. PBKDF2 bridges the two by running HMAC-SHA-256 over the passphrase and a salt many thousands of times. The result looks random, and every guess an attacker makes has to pay for all of those rounds again.
Below, the derivation is real (Web Crypto's PBKDF2) and so is the timing: the page measures it on this device, then prices guessing for the four kinds of secret people actually use.
- One derivation
- measuring
- How it was timed
- Honest defender
A made-up factor for many fast machines working at once. It is not a measurement of any real hardware.
| Secret | Entropy | This device | Attacker |
|---|
Length x log2(character set) is an upper bound. A passphrase a person picks is far weaker than a random one of the same length.
- Ana's salt
- Ana's key
- not yet
- Ben's salt
- Ben's key
- not yet
A toy ROMix-style chain, the idea behind scrypt but not scrypt: fill N blocks of 64 bytes by hashing, then read N of them in an order that depends on the data.
| N | Memory held | SHA-256 calls | Time |
|---|
The maths
A secret chosen uniformly from A possibilities, L times over, carries
H = L log2 A bits, expected guesses = 2H−1, time = guesses × cost per guess
Why half the space. If the secret is equally likely to be any of 2H values, a searcher who tries them in any order finds it, on average, halfway through. Why PBKDF2's cost is linear. Each of its c iterations is one HMAC-SHA-256 on 32 bytes, and each depends on the one before, so the work is c times one HMAC and cannot be split up for one guess.
Words against rounds. One more diceware word multiplies the space by 7,776, which is log2 7,776 = 12.925 bits. Doubling the iterations doubles the cost of every guess, which is worth exactly 1 bit. Iterations buy a constant factor; entropy buys an exponent.
In practiceOWASP's current guidance for PBKDF2-HMAC-SHA-256 is about 600,000 iterations; memory-hard functions such as scrypt and Argon2id are preferred for new designs because they also cost memory, which is scarce on the fast parallel hardware that makes guessing cheap.
DefenceUse a unique random salt for every secret, set the cost from a measurement on your slowest supported device, store the parameters next to the hash so they can be raised later, and put the strength in the passphrase: four random words beat any amount of iteration tuning.
Encryption that notices tampering
AES-GCM does two jobs at once. Counter mode hides the message: AES turns a counter into a keystream and the message is XORed onto it. GHASH then folds the header and the ciphertext into a 128-bit tag. Change one bit anywhere and the tag no longer matches, so decryption refuses and releases nothing.
Seal a message under a demo key, flip a bit, and try to open it. Then compare counter mode on its own, which has no tag and quietly hands back a changed message.
Seal a message to begin.
The same key and counter blocks, so the ciphertext bits are identical to GCM's. Flip the byte and bit chosen above and decrypt.
The maths
Ci = Pi ⊕ EK(counteri), counter1 = J0 + 1, J0 = IV ‖ 0311
tag = EK(J0) ⊕ GHASHH(A, C), H = EK(0128)
X0 = 0, Xi = (Xi−1 ⊕ Bi) · H in GF(2128) modulo x128 + x7 + x2 + x + 1
Why a flipped bit survives counter mode. XOR is its own inverse: P = C ⊕ keystream, so flipping bit j of C flips bit j of P and nothing else. Counter mode hides, it does not protect. Why GCM catches it. The blocks B1 … Bm (the AAD, the ciphertext, then one block of both lengths) are the coefficients of a polynomial evaluated at the secret point H by Horner's rule. A change to any block changes the polynomial, and a polynomial of degree m has at most m roots, so a forged record passes with probability at most about m / 2128.
Multiplication in the field. Blocks are polynomials over bits; adding is XOR, multiplying is shift-and-XOR, and every time the degree reaches 128 the field polynomial folds it back. The code here does that 128 times per block and must land on the browser's tag exactly.
In practiceReal vaults seal each record with AES-GCM or ChaCha20-Poly1305 and put what must not be moved or replayed (the vault id, a version, the record's position) in the associated data. Nothing of a record is released until its tag checks.
Watch outNever decrypt with counter or CBC mode alone and "check later", never truncate the tag to save space, and treat any authentication failure as final: an error that says which part was wrong teaches an attacker as much as a released plaintext.
One nonce, one message
GCM's keystream depends only on the key and the nonce. Seal two messages with the same pair and they are XORed onto the same keystream, so XORing the two ciphertexts cancels it: what is left is the XOR of the two plaintexts, with no key involved. That is a property to measure, not a tool: the page counts how many bytes agree and stops there.
Random 96-bit nonces avoid repeats by sheer size, until you send enough messages for the birthday bound to bite. The simulator shrinks the nonce so the effect shows up in seconds.
Seal the two messages to compare.
| Messages | n² / 2⁹⁷ | About |
|---|
The maths
C1 ⊕ C2 = (P1 ⊕ S) ⊕ (P2 ⊕ S) = P1 ⊕ P2
P(repeat) = 1 − ∏i=0n−1 (1 − i / 2b) ≈ 1 − exp(−n(n−1) / 2b+1) ≈ n2 / 2b+1
Why the product. The i-th nonce must miss the i already drawn, which it does with probability 1 − i/2b, and the draws are independent. Why the exponential. 1 − x ≈ e−x for small x, and the exponents add up to n(n−1)/2 over 2b. With b = 96 and n = 232 that is 264 / 297 = 2−33, which is why guidance caps a key at about 232 random-nonce messages.
The alternative. A counter nonce never repeats (zero risk) but needs state that survives crashes and is never shared between two writers. Misuse-resistant modes (AES-GCM-SIV derives the nonce from the message) and XChaCha20's 192-bit random nonces make an accidental repeat either harmless or vanishingly rare.
In practiceNonce reuse under GCM costs more than confidentiality: two records under one nonce also leak enough about H to forge tags. Libraries that pick the nonce for you, and keys rotated long before 232 messages, are the usual answer.
DefenceLet the library generate nonces, never derive them from a clock or a per-process counter that can restart, count messages per key and rotate, and prefer a misuse-resistant mode where two writers could collide.
k of n people hold the key
Shamir's scheme hides a secret as the value at zero of a random polynomial of degree k−1, and hands each person one point on it. Any k points pin the polynomial down; any k−1 points fit every possible secret equally well, so they say nothing at all. The arithmetic runs byte by byte in GF(256), the same field AES uses.
Split a key first. The panel then takes the first k−1 shares and asks, for one byte, which of the 256 possible secret values they allow.
| Secret byte | Implied next share | Polynomial |
|---|
The maths
f(x) = a0 + a1x + … + ak−1xk−1, a0 = secret, sharei = (xi, f(xi))
f(0) = ∑i yi ∏j≠i xj / (xj − xi)
Why k points suffice. Two different polynomials of degree below k can agree on at most k−1 points (their difference would have too many roots), so k shares allow exactly one, and Lagrange's formula names it. Why k−1 say nothing. Add any candidate point (0, s) to k−1 shares and there is exactly one polynomial of degree k−1 through all k. Every s is possible, each by exactly one polynomial, and the random coefficients make each of those equally likely.
The field. In GF(256) a byte is a polynomial over bits: adding is XOR (so minus is XOR too), multiplying is shift-and-XOR reduced by x8 + x4 + x3 + x + 1 (0x11B), and the inverse of a is a254, because every nonzero element satisfies a255 = 1.
In practiceShamir sharing guards recovery keys and the unseal keys of secret stores (HashiCorp Vault unseals with 3 of 5 by default). The split is only as good as its randomness and its dealer: the machine that did the split saw the whole secret.
Watch outShares carry no integrity of their own, so one bad share silently yields a wrong secret: pair them with a checksum or a MAC of the secret, keep shares in separate hands and places, and never store two on one device.
A real passphrase and a decoy
A design study in what deniability can and cannot promise. The toy container below has a header with a random salt and four equal slots. One slot holds the real contents, one a decoy, and two are random bytes, so the file itself does not say how many passphrases exist. Each slot is sealed with AES-GCM under a key derived by PBKDF2 from a passphrase, the salt and the slot's number.
Unlocking tries every slot with whatever you type. The real passphrase opens the real slot, the duress passphrase opens the decoy, anything else opens nothing, and all three do exactly the same work.
| Part | Bytes | Entropy |
|---|
The same measurement as the Entropy lab: sealed slots and random padding all read close to the ceiling for their size, so this test cannot tell them apart.
| Tried | Opened | Derivations | Time |
|---|
- Someone who copies the file twice sees which slots changed in between, and a slot that changes is a slot in use.
- The software is evidence. Anyone who knows the format knows that hidden slots are possible, and may simply not believe that there are none.
- Coercion does not end when the decoy opens. A duress passphrase protects one moment, not a policy, and it can put the person using it at more risk, not less.
- Timing, file sizes, backups, logs and thumbnails elsewhere on the device can all say more than the container does.
The maths
Ki = PBKDF2(passphrase, salt ‖ i, c), work per attempt = slots × c HMACs, P(random bytes pass a 128-bit tag) = 2−128
Why every slot, every time. If unlocking stopped at the first slot that opens, the time taken would say which slot it was, and a wrong passphrase would be visibly quicker than a right one. Trying all four slots and comparing tags in constant time makes the three cases cost the same, so neither the clock nor the error says which door, if any, was used. Why a wrong passphrase never opens anything by luck. Under a wrong key the tag check is a comparison with an unrelated 128-bit value, which passes with probability 2−128 per slot.
Erase keys, not data. Each slot starts with 60 bytes that wrap its own random data key. Overwriting those 60 bytes leaves the 4,036 bytes of sealed content exactly as they were, and exactly as useless, even to someone who still knows the passphrase. That is how a phone wipes in a second: it forgets one key.
In practiceLUKS2 keeps up to 32 key slots that each wrap one volume key, VeraCrypt hides a second volume in the free space of the first, and some phones and wallets offer a duress PIN. All of them lean on the same two ideas shown here: equal work per attempt, and erasable key material.
Watch outDeniability is a property of the whole system and the situation, not of the file format. Design for the adversary you expect, keep the decoy believable and in use, and never promise a person that a decoy will keep them safe.
One hash for every file
A Merkle tree hashes each file, then hashes pairs of hashes, up to a single root. Keep only the root somewhere safe and you can later prove that any one file is unchanged with a handful of hashes, and that a log only ever grew. Leaves are hashed as H(0x00 ‖ data) and nodes as H(0x01 ‖ left ‖ right), the construction certificate transparency uses (RFC 6962).
| Bits | Work for a collision | Example |
|---|
The maths
MTH({d}) = H(0x00 ‖ d), MTH(Dn) = H(0x01 ‖ MTH(D[0:k]) ‖ MTH(D[k:n])), k = the largest power of 2 below n
proof length = ⌈log2 n⌉ hashes (or one fewer), expected trials to a collision of a b-bit hash ≈ √(π/2 × 2b)
Why a path suffices. The root depends on a file only through the nodes on its path, so the verifier needs just the sibling at each level: hash upward, and the result equals the root only if the file and every sibling are the ones the root was made from (anything else would be a SHA-256 collision). Why the prefixes. Without them, an interior node's input (left ‖ right, 64 bytes) could be presented as a leaf's data, proving a "file" that was never in the tree against the same root: a second-preimage attack on the tree's shape. With 0x00 for leaves and 0x01 for nodes, a leaf hash and a node hash can never come from the same input.
Why only half the bits. Among t random b-bit values there are t(t−1)/2 pairs, each equal with probability 2−b, so a repeat becomes likely near t ≈ 2b/2 (the birthday bound again, the same sum as Exhibit III). SHA-256's 256 bits give 128 bits of collision resistance.
In practiceCertificate transparency logs, git, backup tools and software-update systems all use hash trees: a client keeps one small root and checks any item, or the growth of the whole log, with a logarithmic proof.
DefenceSeparate leaf and node hashing, keep the full hash length (a hash cut to b bits resists collisions for only about 2b/2 tries), sign or publish the root somewhere the storage cannot rewrite, and verify consistency between the roots you have seen so a log cannot quietly fork.