Here is a tension worth sitting with: a BIP-39 seed phrase encodes 256 bits of entropy. A 6-digit PIN has roughly 20. Yet in most browser wallets, the PIN is the gate between an attacker and the seed. How is that defensible?
The answer is key stretching, and understanding why it works, where it breaks, and what the math actually says is more interesting than the implementation.
The entropy problem
256 bits of entropy means 2^256 possible seeds. Brute-forcing that space is not a computational problem, it is a physical one. The observable universe contains on the order of 10^80 atoms, the search space is 10^77. You run out of matter before you run out of guesses.
A 6-digit PIN has 10^6 combinations, about 20 bits. That is a completely different problem. If an attacker steals the encrypted blob and can test PIN guesses at arbitrary speed, they will find the right one in under a million attempts. The question is how expensive each attempt is. That is the entire game.
The two-layer key hierarchy
Before getting into the math, the architecture that holds up under browser constraints looks like this:

Two independent layers. Breaking one does not break the other. The reason for this separation is not just defense in depth, it has a practical consequence: rotating the PIN only re-wraps the Device Key. The seed ciphertext on disk is never touched. The plaintext seed does not need to enter memory during a PIN change.
PBKDF2: slowing the attacker down
PBKDF2, Password-Based Key Derivation Function 2, RFC 8018, takes a low-entropy password and applies a pseudorandom function iteratively to produce a cryptographic key. The construction for each output block is:
U₁ = PRF(password, salt || i)
U₂ = PRF(password, U₁)
U₃ = PRF(password, U₂)
...
Uₙ = PRF(password, Uₙ₋₁)
block_i = U₁ ⊕ U₂ ⊕ ... ⊕ Uₙ
Where n is the iteration count and PRF is HMAC-SHA-256. Each evaluation of HMAC-SHA-256 involves two SHA-256 compressions, so 600,000 PBKDF2 iterations means 1,200,000 SHA-256 operations per PIN attempt.
On a modern CPU core running at roughly 250 MB/s of SHA-256 throughput, one PBKDF2 call at 600,000 iterations takes approximately 300 to 500ms. An attacker exhausting 10^6 PINs on a single core: 3 to 6 days.
The salt is critical. It is a random 128-bit value included in the first PBKDF2 round. Two identical PINs on different devices produce different keys. Pre-computed rainbow tables become useless because each device has a unique salt. The attacker must run the full PBKDF2 computation from scratch for each target.
Where PBKDF2 fails: GPUs
The problem with PBKDF2 is that it is not memory-hard. Each HMAC-SHA-256 call uses a small, fixed amount of working state, roughly 64 to 128 bytes. This means the computation parallelizes cleanly across thousands of GPU cores with no penalty.

A mid-tier GPU can run 10,000 or more parallel SHA-256 operations. At 600,000 iterations and 10,000 parallel threads, the time per attempt drops by a factor of 10,000. What took 6 days on a CPU takes roughly 50 seconds on a GPU. This is not theoretical. Tools like Hashcat have GPU-accelerated PBKDF2-SHA-256 implementations.
The principled solution is Argon2, the winner of the Password Hashing Competition in 2015. Argon2id, the recommended variant, requires a configurable amount of memory per computation. At 64 MB of memory per thread, a GPU with 8 GB of VRAM can only run roughly 128 parallel instances. The memory requirement neutralizes the parallelism advantage.
Argon2 is not part of the browser's Web Crypto API. A WASM implementation exists and is used in production by tools like KeeWeb, but shipping it adds bundle size and extends the attack surface in a context where minimizing executable code is itself a security concern. PBKDF2 with a high iteration count is the pragmatic constraint of working within the browser's native cryptographic interface.
AES-GCM: why authentication matters
Once you have a derived key, you need an encryption mode. AES-GCM, Galois/Counter Mode, is the right choice, and understanding why requires separating two properties that are often conflated: confidentiality and integrity.
AES-CBC provides confidentiality. Each plaintext block is XOR'd with the previous ciphertext block before encryption. But it provides no integrity guarantee. An attacker can flip bits in the ciphertext and produce predictable changes in the decrypted output. The padding oracle attack exploits this: by modifying ciphertext and observing whether decryption produces valid or invalid padding, an attacker can decrypt the entire message without the key. The fix is encrypt-then-MAC, an HMAC over the ciphertext after encryption, but implementing this correctly is subtle. The HMAC must cover the IV too, and the comparison must be constant-time. This is where most implementations fail.
AES-GCM bakes integrity in. It operates in two simultaneous components.
Counter mode generates keystream blocks by incrementing a 32-bit counter appended to the 96-bit IV. Each keystream block is XOR'd with the corresponding plaintext block. This is structurally a stream cipher, so there is no block alignment requirement and no padding.
GHASH authenticates the ciphertext by computing a polynomial hash in GF(2^128), the Galois field with 2^128 elements, defined over the irreducible polynomial:
x¹²⁸ + x⁷ + x² + x + 1
A hash subkey H = AES_K(0), the encryption of a zero block, serves as the field element for multiplication. Each ciphertext block is accumulated:
X₀ = 0
X₁ = (X₀ ⊕ C₁) · H
X₂ = (X₁ ⊕ C₂) · H
...
Xₘ = (Xₘ₋₁ ⊕ Cₘ) · H
tag = (Xₘ ⊕ len_block) · H ⊕ ENC(K, IV || 0)
The final 128-bit tag is stored alongside the ciphertext. On decryption, the tag is recomputed and compared. Any modification to the ciphertext, the IV, or any additional authenticated data produces a different tag. Decryption fails before returning plaintext.
One hard constraint: the IV must be unique for every encryption under the same key. If the same key and IV pair is used twice, an attacker can XOR the two ciphertexts and cancel out the keystream, recovering a relationship between the two plaintexts. A fresh cryptographically random IV must be generated for every encryption operation.
Encryption and decryption flows

The decryption path marks the unwrapped Device Key as non-extractable inside the Web Crypto API. Key material within SubtleCrypto is opaque to JavaScript. The key can be used for operations but cannot be read by any script, including one injected via XSS. The mnemonic buffer is zeroed in a finally block immediately after the signing operation completes.
What the math says honestly
The scheme is sound within its constraints. The weakest link is not the cryptography, it is the PIN entropy, and no iteration count fully compensates for 20 bits of input.
At 600,000 PBKDF2 iterations, a CPU attacker needs days. A GPU attacker needs hours. The mitigation that actually solves this is WebAuthn-bound key storage: using a platform authenticator as the KDF gate. The credential lives in a secure enclave, the private key never leaves hardware, and brute-forcing a biometric is not a tractable attack. Browser and platform support is inconsistent. The implementation surface is real.
The ceiling is the PIN. That is worth being explicit about rather than hiding behind parameter choices.
See you soon.