From 9f763ba0ecf10cc1a7c9927e6e34f10af492d7c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 04:27:26 +0000 Subject: [PATCH] Fix substitution breaking after sync import with at-rest encryption When at-rest encryption is enabled, injector.js detects encrypted blobs in storage, passes empty config to the content script, and waits for a vault:unlocked broadcast from the background. That broadcast was only ever triggered when the user explicitly entered their password in the popup. After a Gist/URL sync import (which writes newly-imported data in encrypted form), no page ever received the decrypted config, so substitution silently stopped working. Two fixes: 1. injector.js: when isLocked is true (encrypted blobs detected), send vault:request-unlock to the background. If the key is already cached (e.g. the user authenticated during the sync pull), the background responds immediately with vault:unlocked containing the decrypted data. This fixes every new page load after a sync import. 2. service-worker.js: add vault:request-unlock handler that checks Storage.isLocked() and, if the key is available, reads decrypted mappings/identity/settings and sends vault:unlocked back to the requesting tab. 3. options.js: after a successful Gist or URL pull, send vault:unlocked to the background so it broadcasts decrypted data to all currently- open tabs immediately, without requiring a page reload. https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53 --- src/background/service-worker.js | 17 +++++++++++++++++ src/content/injector.js | 8 +++++++- src/options/options.js | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/background/service-worker.js b/src/background/service-worker.js index f7c0963..b8d5a4b 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -184,6 +184,23 @@ const messageHandlers = { sendResponse({ locked }); }, + // Content script requests decrypted data on page load when it sees + // encrypted storage but the key is already cached (e.g. after a sync import). + async 'vault:request-unlock'(_message, sender) { + const locked = await Storage.isLocked(); + if (locked) return; // key not available — user must unlock via popup + + const mappings = await Storage.getMappings(); + const identity = await Storage.getIdentity(); + const settings = await Storage.getSettings(); + api.tabs.sendMessage(sender.tab.id, { + type: 'vault:unlocked', + mappings, + identity, + settings, + }).catch(() => {}); + }, + async 'vault:unlocked'() { // User unlocked the vault — clear the LOCK badge and refresh icon api.action.setBadgeText({ text: '' }); diff --git a/src/content/injector.js b/src/content/injector.js index 03c9dc7..9ec3f47 100644 --- a/src/content/injector.js +++ b/src/content/injector.js @@ -60,13 +60,19 @@ const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']); const settings = result.ss_settings || { enabled: true }; - // Check if data is encrypted (locked) — pass empty config + // Check if data is encrypted — pass empty config and request decryption // The background will send decrypted data via vault:unlocked when ready const isLocked = result.ss_mappings?._ssLocalEncrypted || result.ss_identity?._ssLocalEncrypted; const mappings = isLocked ? [] : (result.ss_mappings || []); const identityData = isLocked ? {} : (result.ss_identity || {}); + // If data is encrypted but the vault key may already be cached + // (e.g. after a sync import), ask the background to push decrypted data + if (isLocked) { + api.runtime.sendMessage({ type: 'vault:request-unlock' }).catch(() => {}); + } + // Merge active profiles into a flat identity object for the content script const identity = mergeProfiles(identityData); diff --git a/src/options/options.js b/src/options/options.js index 8d81e9e..9123539 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -201,6 +201,7 @@ document.addEventListener('DOMContentLoaded', async () => { setGistSyncStatus('Pull failed: ' + r2.reason, 'error'); } else if (r2.imported) { setGistSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -215,6 +216,7 @@ document.addEventListener('DOMContentLoaded', async () => { setGistSyncStatus('Pull failed: ' + r.reason, 'error'); } else if (r.imported) { setGistSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -258,6 +260,7 @@ document.addEventListener('DOMContentLoaded', async () => { setUrlSyncStatus('Pull failed: ' + r2.reason, 'error'); } else if (r2.imported) { setUrlSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -272,6 +275,7 @@ document.addEventListener('DOMContentLoaded', async () => { setUrlSyncStatus('Pull failed: ' + r.reason, 'error'); } else if (r.imported) { setUrlSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings();