Everything runs in this tab through Web Crypto. Nothing you type is stored or sent, only your progress is saved, and every key and passphrase here is a throw-away demo value. Attacker speeds are illustrative.
Vault lab
Password managers and encrypted disks all make you the same deal: lose the passphrase and the bytes are noise, and nobody edits them without you finding out. We'll build that deal from parts in six exhibits, using the crypto that's already in your browser.
Lab exploredThat's the whole vault. If you want to see why all that ciphertext looks like noise, the Entropy lab is next door.
A passphrase becomes a key
A cipher wants 256 random bits and people remember words. PBKDF2 bridges the gap by running HMAC-SHA-256 over the passphrase and a salt many thousands of times. The output looks random, and every guess has to pay for all those rounds again.
The derivation below is Web Crypto's real PBKDF2. We time it on this device, then price it for four kinds of secret people use.
- One derivation
- measuring
- How it was timed
- Honest defender
A made-up factor for many fast machines working at once. It isn't a measurement of any real hardware.
| Secret | Entropy | This device | Attacker |
|---|
Length x log2(character set) is an upper bound. A passphrase you pick yourself 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: fill N blocks of 64 bytes by hashing, then read N of them in an order the data decides.
| 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 the cost is linear. Every iteration is one HMAC-SHA-256 that needs the previous one's output, so a guess costs c HMACs and you can't parallelise inside a single 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. More iterations only scale the cost by a constant, while every extra bit of entropy doubles it.
In practiceOWASP's number for PBKDF2-HMAC-SHA-256 is about 600,000 iterations right now. Starting fresh, use Argon2id or scrypt. They eat memory too, and memory is what GPU cracking rigs run short of.
DefenceUse a unique random salt per secret, set the cost from your slowest supported device, and store the parameters beside the hash. Put the strength in the passphrase: four random words beat any iteration tuning (random as in dice, so the words on your desk don't count lol).
Encryption that notices tampering
AES-GCM does two jobs at once. Counter mode hides the message: AES turns a counter into a keystream and you XOR the message onto it. GHASH then folds the header and the ciphertext into a 128-bit tag. Change one bit anywhere and the tag stops matching, so decryption refuses and releases nothing.
Seal a message under a demo key, flip a bit, and try to open it. Then try counter mode on its own: it has no tag, so it quietly hands back the changed message.
Seal a message to begin.
Same key and counter blocks, so the ciphertext bits are identical to GCM's. Flip the byte and bit you chose above, then 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 undoes itself: P = C ⊕ keystream, so flipping bit j of C flips bit j of P and nothing else. Counter mode hides data but doesn't protect it. Why GCM catches it. The blocks B1 … Bm (the AAD, the ciphertext, then one block holding both lengths) are the coefficients of a polynomial, evaluated at the secret point H by Horner's rule. Change any block and the polynomial changes. 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 whenever the degree reaches 128 the field polynomial folds it back. The code here does that 128 times per block and has to land on the browser's tag exactly.
In practiceReal vaults seal each record with AES-GCM or ChaCha20-Poly1305 and put the vault id, version and record position in the associated data. Nothing is released until the tag checks.
Watch outNever decrypt with counter or CBC mode alone and "check later". Never truncate the tag. Treat any authentication failure as final, and give an error that says nothing about which part was wrong.
One nonce, one message
GCM's keystream depends only on the key and the nonce. Seal two messages with the same pair and the keystream cancels when you XOR the ciphertexts, leaving the XOR of the two plaintexts. The page checks that by counting matching bytes, 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 has to 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 you need 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 harmless or vanishingly rare.
In practiceReusing a nonce under GCM costs you more than confidentiality: two records under one nonce also leak enough about H to forge tags. The usual fix is a library that picks the nonce for you, plus keys rotated long before 232 messages.
DefenceLet the library generate nonces, never derive them from a clock or a counter that can restart, and rotate keys by message count. Where two writers could collide, use a misuse-resistant mode.
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, while any k−1 points fit every possible secret equally well. 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 are enough. Two different polynomials of degree below k agree on at most k−1 points, so k shares allow exactly one, and Lagrange's formula gives it to you. Why k−1 tell you nothing. Add any candidate point (0, s) to k−1 shares and exactly one polynomial of degree k−1 goes through all k. Every s is possible, each by exactly one polynomial, and the random coefficients make each of them equally likely.
The field. In GF(256) a byte is a polynomial over bits. Adding (and subtracting) is XOR, and multiplying is shift-and-XOR reduced by x8 + x4 + x3 + x + 1 (0x11B). 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 check of their own, so one bad share silently gives you a wrong secret. Pair them with a MAC of the secret, and never store two on one device.
A real passphrase and a decoy
This is a design study in what deniability can and can't 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 doesn't say how many passphrases exist. Each slot is sealed with AES-GCM under a key that PBKDF2 derives 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 can't 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 hidden slots are possible, and may not believe there are none.
- Coercion doesn't end when the decoy opens. A duress passphrase protects one moment, and it can put the person using it at greater risk.
- 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 it took would tell you which slot it was. Try all four slots and compare tags in constant time, and every case costs the same. 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 compares against an unrelated 128-bit value, and that passes with probability 2−128 per slot.
What crypto-shred does. Each slot starts with 60 bytes that wrap its own random data key. Overwrite those 60 bytes and the 4,036 bytes of sealed content stay in place but useless, even to someone who knows the passphrase. That's how a phone wipes in a second.
In practiceLUKS2 keeps up to 32 key slots that wrap one volume key. VeraCrypt hides a second volume in the free space of the first, and some phones offer a duress PIN. They all depend on equal work per attempt and key material you can erase.
Watch outDeniability belongs to the whole system and the situation, never to a file format alone. Keep the decoy believable and in use, and never promise anyone that a decoy will keep them safe.
One hash for every file
A Merkle tree hashes each file, then pairs of hashes, up the tree to a single root. Keep the root somewhere safe and a handful of hashes can later prove any one file unchanged, or 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 is enough. 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 you only reach the root if the file and every sibling are the originals, short of a SHA-256 collision. Why the prefixes. Without them, an interior node's 64-byte input could pass as a leaf's data and prove a "file" that was never in the tree. With 0x00 for leaves and 0x01 for nodes, a leaf hash and a node hash never share an 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. 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 update systems all use hash trees: a client keeps one root and checks any item with a logarithmic proof.
DefenceHash leaves and nodes with different prefixes and keep the full hash length. Publish the root where the storage can't rewrite it, and check consistency between roots so a log can't quietly fork.