security: stop caching the raw vault key in sessionStorage

A memory dump of the WebView2 renderer (vault unlocked) showed the AES vault
key sitting in sessionStorage as a clean base64 string — a plaintext copy of
the master secret, trivially grep-able, present the whole session.

persistCryptoKey/restoreCryptoKey existed only to survive a page reload, but
the shell has no reload path (F5/Ctrl+R blocked, every navigation is a cmd://
bridge call), so the branch that consumed it was unreachable. Removed both
functions, their 7 call sites, and collapsed the init restore branch into the
plain cold-start path (Quick Unlock → PIN → master-pw, all already there).

Also scrub the derived key bytes from the heap (keyBytes.fill(0)) once the
CryptoKey and verifier are built — the CryptoKey keeps its own copy.

Not fixable here, documented: the key stays extractable:true (Quick Unlock,
PIN, recovery and master-pw change all exportKey it), and decrypted password
strings still linger in the V8/Blink heap after lock — immutable strings the
GC never zeroes, inherent to a JS/WebView2 password manager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-24 23:40:56 +01:00
parent de50a5f71f
commit b542106c76
3 changed files with 21 additions and 49 deletions
-3
View File
@@ -401,7 +401,6 @@ async function loginViaPin(pin) {
sessionStorage.setItem('hashAlgo', state.hashAlgo);
sessionStorage.setItem('authToken', state.token);
sessionStorage.setItem('csrfToken', state.csrf);
await persistCryptoKey();
state.locked = false;
state.justRecovered = false;
@@ -547,7 +546,6 @@ async function tryQuickUnlock() {
sessionStorage.setItem('hashAlgo', state.hashAlgo);
sessionStorage.setItem('authToken', state.token);
sessionStorage.setItem('csrfToken', state.csrf);
await persistCryptoKey();
state.locked = false;
return true;
@@ -891,7 +889,6 @@ async function doRecoveryRedeem() {
// so master-pw change can later re-export and re-wrap as needed).
state.cryptoKey = await crypto.subtle.importKey(
'raw', rawKey, { name: 'AES-GCM' }, true, ['encrypt', 'decrypt']);
await persistCryptoKey();
const remaining = (typeof r.remainingUses === 'number') ? r.remainingUses : 0;
if (remaining <= 0) {