37c409102a5d

M6/COR-004: make changePassword single atomic writeSlot call

time
2026-05-16 02:48 UTC · 5 months ago
machine
koh2
parent
6b7be6db17e9
1 file src/background/vault.ts+15 −8
src/background/vault.ts+15 −8
@@ -2073,6 +2073,7 @@20732073 20742074   const slotA = await loadSlot(SLOT_A);20752075   if (!slotA) return false;2076+  if (!rootKey) return false;20762077 20772078   // Derive fresh key from new password + new random salt20782079   const newSalt = generateSalt();@@ -2082,30 +2083,36 @@20822083   const plaintext = encodePayload(entries, duressConfigured);20832084   const { nonce, ciphertext, tag } = await encrypt(newKey, plaintext);20842085 2085-  const freshSlotA: StoredVault = {2086+  // Pre-wrap root key under the new derived key. Recovery fields wrap rootKey2087+  // under recovery words and remain untouched — recovery phrase keeps working2088+  // across password changes (H4/COR-002).2089+  const pwRoot = await wrapRootKeyForDerivedKey(newKey, rootKey);2090+2091+  const finalSlotA: StoredVault = {20862092     ...slotA,20872093     salt: hex(newSalt),20882094     nonce,20892095     ciphertext,20902096     tag,2097+    ...pwRoot,20912098   };2099+20922100   // Snapshot the OLD-key blob before overwriting it. If the user later20932101   // restores from this snapshot they will need the OLD password — the20942102   // post-restore lock surfaces that immediately.20952103   await takeVaultSnapshot("local_save");2096-  await writeSlot(SLOT_A, freshSlotA);20972104 2105+  // Single atomic write: ciphertext, salt, and pw_root_* fields all land in2106+  // one storage transaction. An SW kill before this point leaves the old2107+  // slot intact; after this point everything tied to the new password is2108+  // already on disk together (M6/COR-004).2109+  await writeSlot(SLOT_A, finalSlotA);2110+20982111   // Swap the in-memory key synchronously before any further await. If we20992112   // threw between the Slot A write and this swap, the next persistEntries()21002113   // would overwrite Slot A with old-key ciphertext, locking the user out.21012114   zeroKey(derivedKey);21022115   derivedKey = newKey;2103-2104-  // Preserve the existing root key: re-wrap pw_root_* under the new derived2105-  // key so the same recovery words continue to decrypt the vault after a2106-  // password change (H4/COR-002). Rotating the root key here would silently2107-  // invalidate every previously-saved recovery phrase.2108-  await rewrapRootKeyForPassword(newKey, newSalt);21092116 21102117   // Regenerate noise for slot B when duress is not in use. Runs after the21112118   // key swap so a failure here is a disposable noise regeneration, not a

view file