feat: at-rest encryption for all sensitive data + vault unlock flow

All sensitive data (identity, mappings, activity log) is now AES-256
encrypted in browser.storage.local when sync encryption is enabled.
TOTP secret is also encrypted at rest using the derived key.

Vault unlock flow:
- On browser restart, extension detects locked state (encrypted data,
  no cached CryptoKey) and shows LOCK badge in red
- Popup shows a full-screen unlock prompt with password field,
  optional TOTP, and biometric button
- After unlock, background decrypts and broadcasts data to all tabs
- Content scripts start with empty config when locked; receive
  decrypted config via vault:unlocked message after unlock
- Injector skips encrypted blobs in storage change events

Storage module changes:
- _readSecure / _writeSecure transparently encrypt/decrypt
- encryptExistingData() migrates plaintext → encrypted on setup
- decryptAllData() restores plaintext when encryption is disabled
- isLocked() checks for encrypted data + missing key

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-26 19:03:54 +00:00
parent 39e609a14e
commit a22ba549a2
7 changed files with 418 additions and 41 deletions
+46
View File
@@ -153,6 +153,44 @@ const messageHandlers = {
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
},
async 'get:locked-state'(_message, _sender, sendResponse) {
const locked = await Storage.isLocked();
sendResponse({ locked });
},
async 'vault:unlocked'() {
// User unlocked the vault — clear the LOCK badge and refresh icon
api.action.setBadgeText({ text: '' });
const settings = await Storage.getSettings();
await updateIcon(settings);
// Now that we're unlocked, try syncing
if (settings.browserSync) {
await SilentSendSync.pullFromSyncStorage();
}
// Read decrypted data via Storage module and send to all content scripts
const mappings = await Storage.getMappings();
const identity = await Storage.getIdentity();
const allPatterns = [...BUILTIN_URL_PATTERNS];
const customDomains = settings.customDomains || [];
for (const domain of customDomains) {
allPatterns.push(domain + '/*');
}
for (const urlPattern of allPatterns) {
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
for (const tab of tabs) {
api.tabs.sendMessage(tab.id, {
type: 'vault:unlocked',
mappings,
identity,
settings,
}).catch(() => {});
}
}
},
async 'update:settings'(message) {
await Storage.saveSettings(message.settings);
@@ -372,6 +410,14 @@ api.runtime.onInstalled.addListener(async () => {
const settings = await Storage.getSettings();
await updateIcon(settings);
// Check if extension is locked (encrypted data, no cached key)
const locked = await Storage.isLocked();
if (locked) {
api.action.setBadgeText({ text: 'LOCK' });
api.action.setBadgeBackgroundColor({ color: '#dc2626' });
return; // don't try to sync while locked
}
// Restore the SYN badge if the user hasn't opened Options since the last sync
const stored = await api.storage.local.get('ss_sync_notification');
if (stored.ss_sync_notification) {