Commit: 2f26d91 · Finding: SEC-W-05
Problem
save_identity_bytes at crates/client/src/storage.rs:96-101 writes the raw 32-byte Ed25519 secret key to localStorage["willow_identity"] base64-encoded in plaintext. Every same-origin script can read it with localStorage.getItem("willow_identity").
Combined with the absence of CSP (#175) and the normalized js_sys::eval pattern (#171), any single XSS — or a compromised npm-delivered CSS/font/build asset — exports the user's long-term identity for every server they're joined to.
Fix
Migrate identity storage to IndexedDB with crypto.subtle.importKey(..., extractable=false, ['sign']), storing only the non-extractable CryptoKey handle. For backward compatibility:
- On startup, if
willow_identity localStorage key exists: read it, import as non-extractable, delete the legacy entry.
- Use the non-extractable handle for all subsequent signing.
- Optional (Phase 2): wrap at rest with a user passphrase (argon2 → AES-KW).
Obvious? Yes — storing a long-term signing key in localStorage is a canonical anti-pattern. Requires some refactor of the signing path; not a one-line fix.
Commit:
2f26d91· Finding:SEC-W-05Problem
save_identity_bytesatcrates/client/src/storage.rs:96-101writes the raw 32-byte Ed25519 secret key tolocalStorage["willow_identity"]base64-encoded in plaintext. Every same-origin script can read it withlocalStorage.getItem("willow_identity").Combined with the absence of CSP (#175) and the normalized
js_sys::evalpattern (#171), any single XSS — or a compromised npm-delivered CSS/font/build asset — exports the user's long-term identity for every server they're joined to.Fix
Migrate identity storage to IndexedDB with
crypto.subtle.importKey(..., extractable=false, ['sign']), storing only the non-extractableCryptoKeyhandle. For backward compatibility:willow_identitylocalStorage key exists: read it, import as non-extractable, delete the legacy entry.Obvious? Yes — storing a long-term signing key in localStorage is a canonical anti-pattern. Requires some refactor of the signing path; not a one-line fix.