From 5c863b084d444ea92b1bdae90c75d8d2ed71b3e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 16:58:40 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20encrypted=20data=20breaks=20Firefox=20?= =?UTF-8?q?=E2=80=94=20injector=20can't=20decrypt=20=E2=80=94=20v2.0.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When at-rest encryption is enabled, storage.local contains encrypted blobs. The injector reads raw storage (content script world, no access to IndexedDB CryptoKey) and sees { _ssLocalEncrypted: true }. It passed empty config to content.js → no mappings → no substitution. This is why Firefox stopped working after encryption was enabled. Chrome/Brave worked because the user hadn't set up encryption there. Fixed: injector now detects encrypted data and asks the background script for decrypted config via 'get:decrypted-config' message. The background uses the Storage module (which has IndexedDB access) to decrypt and return the data. Falls back to empty config if the vault is actually locked. https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn --- manifest.firefox.json | 2 +- manifest.json | 2 +- package.json | 2 +- src/background/service-worker.js | 11 ++++++++ src/content/injector.js | 45 +++++++++++++++++++++++++------- src/options/options.html | 2 +- 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/manifest.firefox.json b/manifest.firefox.json index 74406c3..0f9077d 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.12", + "version": "2.0.13", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index 6e8f86c..9800648 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.12", + "version": "2.0.13", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index 321b0b7..0a2ff37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "2.0.12", + "version": "2.0.13", "private": true, "license": "BSL-1.1", "description": "Browser extension that substitutes personal data before sending to AI services", diff --git a/src/background/service-worker.js b/src/background/service-worker.js index 1dd59d9..98148bb 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -163,6 +163,17 @@ const messageHandlers = { await setupAutoSyncAlarm(); }, + async 'get:decrypted-config'(_message, _sender, sendResponse) { + try { + const mappings = await Storage.getMappings(); + const identity = await Storage.getIdentity(); + const settings = await Storage.getSettings(); + sendResponse({ mappings, identity, settings }); + } catch { + sendResponse(null); + } + }, + async 'org:config-changed'() { await setupOrgPolicyAlarm(); }, diff --git a/src/content/injector.js b/src/content/injector.js index 7bf78c4..4c3d5a2 100644 --- a/src/content/injector.js +++ b/src/content/injector.js @@ -57,18 +57,45 @@ // Load mappings and settings, then inject into page async function init() { + let mappings, identity, settings; + const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']); - const settings = result.ss_settings || { enabled: true }; + const isEncrypted = result.ss_mappings?._ssLocalEncrypted || + result.ss_identity?._ssLocalEncrypted; - // Check if data is encrypted (locked) — pass empty config - // 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 (isEncrypted) { + // Data is encrypted — ask the background script for decrypted config. + // The background has access to the Storage module which can decrypt. + try { + const response = await api.runtime.sendMessage({ type: 'get:decrypted-config' }); + if (response?.mappings) { + mappings = response.mappings; + identity = response.identity || {}; + settings = response.settings || { enabled: true }; + } else { + // Background couldn't decrypt (locked) — inject with empty config + // and wait for vault:unlocked message later + mappings = []; + identity = {}; + settings = result.ss_settings || { enabled: true }; + } + } catch { + mappings = []; + identity = {}; + settings = result.ss_settings || { enabled: true }; + } + } else { + // Data is plaintext — read directly + mappings = result.ss_mappings || []; + const identityData = result.ss_identity || {}; + identity = mergeProfiles(identityData); + settings = result.ss_settings || { enabled: true }; + } - // Merge active profiles into a flat identity object for the content script - const identity = mergeProfiles(identityData); + // Ensure identity is merged if it came from background + if (identity.profiles) { + identity = mergeProfiles(identity); + } // Inject the main interception script into the page's world const script = document.createElement('script'); diff --git a/src/options/options.html b/src/options/options.html index 3ea99c4..a2b68e0 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -591,7 +591,7 @@