3867c5eae0a1

docs: PCH-01-005 document recovery-escrow 128-bit prefix-KEK construction at rootKeyToWords, wordsToRootKeyMaterial, generateAndWrapRootKey (v3.8.1)

time
2026-06-10 20:18 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
pch-01 fix integration
parent
750fbd091ff3
2 files src/background/vault.ts+8 −0src/shared/recovery-words.ts+12 −1
src/background/vault.ts+8 −0
@@ -285,6 +285,14 @@285285  * fields are stored in slot A alongside the password-wrapped ciphertext. The286286  * root key itself is kept in memory for the session so getRecoveryWords() can287287  * encode it without re-deriving.288+ *289+ * SECURITY MODEL (PCH-01-005): the recovery KEK is derived from the 12-word290+ * material, which encodes only rootKey[0..16] (recovery-words.ts). The KEK is thus291+ * derived from a PREFIX of the very plaintext it wraps, and effective recovery292+ * security is 128-bit (standard for a 12-word phrase), NOT 256-bit. This is safe —293+ * unwrapping requires the words, and the GCM ciphertext leaks nothing without them —294+ * but it is intentional, not an oversight. A future escrow version wanting 256-bit295+ * recovery must derive the words from INDEPENDENT entropy, not from the root key.288296  */289297 async function generateAndWrapRootKey(): Promise<{290298   rawRootKey: string; // hex of the plaintext root key — caller stores in rootKey

view file

src/shared/recovery-words.ts+12 −1
@@ -17,6 +17,13 @@1717 /**1818  * Convert a 32-byte root key to 12 recovery words.1919  * Encodes 128 bits of entropy + 4-bit BIP39 checksum as 12 eleven-bit word indices.20+ *21+ * SECURITY MODEL (PCH-01-005): the words encode ONLY rootKey[0..16]. The second22+ * 16 bytes of the root key are NOT represented in the phrase. Recovery security is23+ * therefore 128-bit — the standard for a 12-word BIP39 phrase — NOT 256-bit. The24+ * 12 words are a 128-bit prefix, not independent secret material; do not assume the25+ * full root key can be reconstructed from them. (See generateAndWrapRootKey in26+ * vault.ts and wordsToRootKeyMaterial below.)2027  */2128 export async function rootKeyToWords(rootKeyBytes: Uint8Array): Promise<string[]> {2229   if (rootKeyBytes.length !== 32) {@@ -106,7 +113,11 @@106113     throw new Error("invalid recovery words — checksum mismatch");107114   }108115 109-  // Pad to 32 bytes (the full root key is 32 bytes, only first 16 are entropy).116+  // Pad to 32 bytes. The phrase carries only the 128-bit prefix (PCH-01-005), so117+  // bytes 16..31 are ZERO here — this is word *material*, not the recovered root118+  // key. It is fed straight into deriveRecoveryKey as HKDF IKM; the resulting KEK119+  // unwraps the full 32-byte root key from the escrow blob. The padding is part of120+  // the security model (128-bit recovery), not a placeholder to be "filled in".110121   const rootKey = new Uint8Array(32);111122   rootKey.set(entropy);112123   return rootKey;

view file