Merge pull request #26 from outis1one/claude/read-repo-wA3y1
Claude/read repo w a3y1
This commit is contained in:
@@ -89,7 +89,7 @@ Import your existing data from password managers and browser autofill to pre-pop
|
|||||||
| Plain CSV (2 columns) | Real → substitute pairs |
|
| Plain CSV (2 columns) | Real → substitute pairs |
|
||||||
| Plain text (1 per line) | Auto-categorized values needing substitutes |
|
| Plain text (1 per line) | Auto-categorized values needing substitutes |
|
||||||
|
|
||||||
Passwords are imported as exact-match mappings (e.g. `MyS3cret!` → `[REDACTED-PASSWORD-1]`) so they get caught in any context — not just `password=value` patterns.
|
Passwords are imported as exact-match mappings (e.g. `MyS3cret!` → `[REDACTED-PASSWORD-1]`) so they get caught in any context — not just `password=value` patterns. Imported passwords are protected: they're shown as dots in the UI and require your vault encryption password to reveal. When at-rest encryption is enabled, imported passwords are AES-256 encrypted in storage like all other sensitive data.
|
||||||
|
|
||||||
Go to **Options** → **Transfer Data** → **Import CSV / Password Export**.
|
Go to **Options** → **Transfer Data** → **Import CSV / Password Export**.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Silent Send",
|
"name": "Silent Send",
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Silent Send",
|
"name": "Silent Send",
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "silent-send",
|
"name": "silent-send",
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "BSL-1.1",
|
"license": "BSL-1.1",
|
||||||
"description": "Browser extension that substitutes personal data before sending to AI services",
|
"description": "Browser extension that substitutes personal data before sending to AI services",
|
||||||
|
|||||||
+21
-26
@@ -15,6 +15,25 @@
|
|||||||
if (window.__silentSendInjected) return;
|
if (window.__silentSendInjected) return;
|
||||||
window.__silentSendInjected = true;
|
window.__silentSendInjected = true;
|
||||||
|
|
||||||
|
// IMMEDIATELY inject a synchronous fetch hook into the page world
|
||||||
|
// BEFORE any async operations. This must run before any page JS
|
||||||
|
// (like ChatGPT's Next.js) can store a reference to the original fetch.
|
||||||
|
const earlyHook = document.createElement('script');
|
||||||
|
earlyHook.textContent = `(function(){
|
||||||
|
window.__ssOriginalFetch = window.fetch;
|
||||||
|
window.__ssOriginalXHROpen = XMLHttpRequest.prototype.open;
|
||||||
|
window.__ssOriginalXHRSend = XMLHttpRequest.prototype.send;
|
||||||
|
window.__ssReady = false;
|
||||||
|
window.fetch = function() {
|
||||||
|
if (window.__ssReady && window.__ssInterceptFetch) {
|
||||||
|
return window.__ssInterceptFetch.apply(this, arguments);
|
||||||
|
}
|
||||||
|
return window.__ssOriginalFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();`;
|
||||||
|
(document.head || document.documentElement).appendChild(earlyHook);
|
||||||
|
earlyHook.remove();
|
||||||
|
|
||||||
// Merge active profiles into flat identity object
|
// Merge active profiles into flat identity object
|
||||||
function mergeProfiles(data) {
|
function mergeProfiles(data) {
|
||||||
const profiles = data?.profiles || [];
|
const profiles = data?.profiles || [];
|
||||||
@@ -70,32 +89,8 @@
|
|||||||
// Merge active profiles into a flat identity object for the content script
|
// Merge active profiles into a flat identity object for the content script
|
||||||
const identity = mergeProfiles(identityData);
|
const identity = mergeProfiles(identityData);
|
||||||
|
|
||||||
// STEP 1: Inject a synchronous inline script that patches fetch/XHR
|
// Load the full content.js which will use __ssOriginalFetch
|
||||||
// IMMEDIATELY, before any page JS can store a reference to the originals.
|
// (captured by the early hook above) and set __ssReady = true
|
||||||
// This thin proxy queues calls until the full content.js loads.
|
|
||||||
const earlyHook = document.createElement('script');
|
|
||||||
earlyHook.textContent = `(function(){
|
|
||||||
// Store the real fetch/XHR before any page script can
|
|
||||||
window.__ssOriginalFetch = window.fetch;
|
|
||||||
window.__ssOriginalXHROpen = XMLHttpRequest.prototype.open;
|
|
||||||
window.__ssOriginalXHRSend = XMLHttpRequest.prototype.send;
|
|
||||||
window.__ssReady = false;
|
|
||||||
window.__ssQueue = [];
|
|
||||||
|
|
||||||
// Replace fetch with a proxy that queues until content.js is ready
|
|
||||||
window.fetch = function() {
|
|
||||||
if (window.__ssReady && window.__ssInterceptFetch) {
|
|
||||||
return window.__ssInterceptFetch.apply(this, arguments);
|
|
||||||
}
|
|
||||||
// If not ready yet, call original (no substitution possible)
|
|
||||||
return window.__ssOriginalFetch.apply(this, arguments);
|
|
||||||
};
|
|
||||||
})();`;
|
|
||||||
(document.head || document.documentElement).appendChild(earlyHook);
|
|
||||||
earlyHook.remove();
|
|
||||||
|
|
||||||
// STEP 2: Load the full content.js which will use __ssOriginalFetch
|
|
||||||
// and set __ssReady = true when it's done hooking
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
||||||
script.src = api.runtime.getURL('src/content/content.js');
|
script.src = api.runtime.getURL('src/content/content.js');
|
||||||
|
|||||||
+91
-38
@@ -464,47 +464,100 @@ const SilentSendSync = {
|
|||||||
async _decryptFromSync(data) {
|
async _decryptFromSync(data) {
|
||||||
if (!data?._ssEncrypted) return { data, decrypted: false };
|
if (!data?._ssEncrypted) return { data, decrypted: false };
|
||||||
|
|
||||||
// Always use the sync envelope's salt/verificationBlob for decryption,
|
// Decrypt using the SOURCE device's salt (embedded in the sync envelope).
|
||||||
// not the local config. Different devices have different salts, so the
|
// We derive a TEMPORARY key — never modify the local config or cached key,
|
||||||
// local key won't decrypt data encrypted with another device's salt.
|
// because the local data is encrypted with the LOCAL salt.
|
||||||
let config = await this._getSyncEncryption();
|
const sourceSalt = data._encConfig?.salt;
|
||||||
|
const sourceVerification = data._encConfig?.verificationBlob;
|
||||||
if (data._encConfig) {
|
|
||||||
// Use the source's salt for this decryption, but don't overwrite
|
|
||||||
// the local config permanently yet — only if decryption succeeds
|
|
||||||
config = {
|
|
||||||
...(config || {}),
|
|
||||||
enabled: true,
|
|
||||||
salt: data._encConfig.salt,
|
|
||||||
verificationBlob: data._encConfig.verificationBlob,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config?.enabled) {
|
|
||||||
return { data: null, decrypted: false, needsAuth: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get a key using the sync envelope's salt
|
|
||||||
// First check if we have a cached key that matches
|
|
||||||
let keyInfo = await SilentSendCrypto.getCachedKey();
|
|
||||||
|
|
||||||
// If the cached key's salt doesn't match the sync data's salt,
|
|
||||||
// we need to re-derive from the password
|
|
||||||
if (keyInfo && data._encConfig && keyInfo.salt !== data._encConfig.salt) {
|
|
||||||
keyInfo = null; // force re-auth with the correct salt
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keyInfo) {
|
|
||||||
// Need the user to enter the password — save the sync salt temporarily
|
|
||||||
// so authenticate() uses it to derive the correct key
|
|
||||||
if (data._encConfig) {
|
|
||||||
await this._saveSyncEncryption(config);
|
|
||||||
}
|
|
||||||
return { data: null, decrypted: false, needsAuth: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!sourceSalt) {
|
||||||
|
// No embedded config — try local key (same-device sync like browser.storage.sync)
|
||||||
|
const keyInfo = await SilentSendCrypto.getCachedKey();
|
||||||
|
if (!keyInfo) return { data: null, decrypted: false, needsAuth: true };
|
||||||
|
try {
|
||||||
const decrypted = await SilentSendCrypto.decryptWithKey(data.payload, keyInfo.key);
|
const decrypted = await SilentSendCrypto.decryptWithKey(data.payload, keyInfo.key);
|
||||||
|
return this._handleDecryptedMeta(decrypted);
|
||||||
|
} catch {
|
||||||
|
return { data: null, decrypted: false, needsAuth: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-device: need to derive a temporary key from the source salt.
|
||||||
|
// Check if we have a stored password-derived key for this source salt.
|
||||||
|
const tempKeyStore = await this._getTempSyncKey(sourceSalt);
|
||||||
|
if (tempKeyStore) {
|
||||||
|
try {
|
||||||
|
const decrypted = await SilentSendCrypto.decryptWithKey(data.payload, tempKeyStore.key);
|
||||||
|
return this._handleDecryptedMeta(decrypted);
|
||||||
|
} catch { /* wrong key, fall through to prompt */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need the user's password to derive a key with the source salt.
|
||||||
|
// Store the source salt temporarily so the auth prompt can use it.
|
||||||
|
this._pendingSyncSalt = sourceSalt;
|
||||||
|
this._pendingSyncVerification = sourceVerification;
|
||||||
|
return { data: null, decrypted: false, needsAuth: true, syncSalt: sourceSalt };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate specifically for a cross-device sync import.
|
||||||
|
* Derives a temporary key with the source device's salt.
|
||||||
|
* Does NOT modify the local encryption config or cached key.
|
||||||
|
*/
|
||||||
|
async authenticateForSync(password) {
|
||||||
|
const salt = this._pendingSyncSalt;
|
||||||
|
const verification = this._pendingSyncVerification;
|
||||||
|
if (!salt) return { success: false, reason: 'No pending sync import.' };
|
||||||
|
|
||||||
|
// Derive key with the source salt
|
||||||
|
const { key } = await SilentSendCrypto.deriveAndReturnKey(password, salt);
|
||||||
|
|
||||||
|
// Verify password against the source's verification blob
|
||||||
|
if (verification) {
|
||||||
|
try {
|
||||||
|
await SilentSendCrypto.decryptWithKey(verification, key);
|
||||||
|
} catch {
|
||||||
|
return { success: false, reason: 'Wrong password.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store this temporary key for the source salt (NOT in the main cache)
|
||||||
|
await this._storeTempSyncKey(salt, key);
|
||||||
|
this._pendingSyncSalt = null;
|
||||||
|
this._pendingSyncVerification = null;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
async _storeTempSyncKey(salt, key) {
|
||||||
|
try {
|
||||||
|
const db = await SilentSendCrypto._openCacheDB();
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction('keys', 'readwrite');
|
||||||
|
tx.objectStore('keys').put({ key, salt, storedAt: Date.now() }, 'tempSyncKey');
|
||||||
|
tx.oncomplete = resolve;
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
});
|
||||||
|
} catch { /* non-fatal */ }
|
||||||
|
},
|
||||||
|
|
||||||
|
async _getTempSyncKey(salt) {
|
||||||
|
try {
|
||||||
|
const db = await SilentSendCrypto._openCacheDB();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const tx = db.transaction('keys', 'readonly');
|
||||||
|
const req = tx.objectStore('keys').get('tempSyncKey');
|
||||||
|
req.onsuccess = () => {
|
||||||
|
const entry = req.result;
|
||||||
|
if (entry?.key && entry.salt === salt) resolve(entry);
|
||||||
|
else resolve(null);
|
||||||
|
};
|
||||||
|
req.onerror = () => resolve(null);
|
||||||
|
});
|
||||||
|
} catch { return null; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async _handleDecryptedMeta(decrypted) {
|
||||||
// Restore full encryption config from inner metadata
|
// Restore full encryption config from inner metadata
|
||||||
if (decrypted._encMeta) {
|
if (decrypted._encMeta) {
|
||||||
const fullConfig = await this._getSyncEncryption();
|
const fullConfig = await this._getSyncEncryption();
|
||||||
|
|||||||
@@ -591,7 +591,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Silent Send v2.0.7</p>
|
<p>Silent Send v2.0.9</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+27
-9
@@ -104,15 +104,20 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
setSyncStatus('', 'neutral');
|
setSyncStatus('', 'neutral');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#btnApplySyncCode').addEventListener('click', async () => {
|
$('#btnApplySyncCode').addEventListener('click', applySyncCode);
|
||||||
|
|
||||||
|
async function applySyncCode() {
|
||||||
const code = $('#syncImportText').value.trim();
|
const code = $('#syncImportText').value.trim();
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
const force = $('#syncForce').checked;
|
const force = $('#syncForce').checked;
|
||||||
const result = await SilentSendSync.importSyncCode(code, { force });
|
const result = await SilentSendSync.importSyncCode(code, { force });
|
||||||
if (result.needsAuth) {
|
if (result.needsAuth) {
|
||||||
setSyncStatus('Authentication required to decrypt this sync code.', 'warn');
|
setSyncStatus('Authentication required — enter your encryption password, then the import will continue.', 'warn');
|
||||||
showSyncAuthPrompt();
|
showSyncAuthPrompt('decrypt');
|
||||||
|
// After auth succeeds, retry the import automatically
|
||||||
|
window.__ssPendingSyncImport = applySyncCode;
|
||||||
} else if (result.success) {
|
} else if (result.success) {
|
||||||
|
window.__ssPendingSyncImport = null;
|
||||||
setSyncStatus(`Imported successfully (data from ${result.importTime}).`, 'ok');
|
setSyncStatus(`Imported successfully (data from ${result.importTime}).`, 'ok');
|
||||||
$('#syncImportSection').style.display = 'none';
|
$('#syncImportSection').style.display = 'none';
|
||||||
$('#syncImportText').value = '';
|
$('#syncImportText').value = '';
|
||||||
@@ -120,6 +125,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
settings = await Storage.getSettings();
|
settings = await Storage.getSettings();
|
||||||
$('#browserSync').checked = settings.browserSync === true;
|
$('#browserSync').checked = settings.browserSync === true;
|
||||||
renderMappings();
|
renderMappings();
|
||||||
|
renderPasswords();
|
||||||
renderDomains();
|
renderDomains();
|
||||||
renderLog();
|
renderLog();
|
||||||
} else if (result.skipped) {
|
} else if (result.skipped) {
|
||||||
@@ -130,7 +136,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
} else {
|
} else {
|
||||||
setSyncStatus(`Failed: ${result.reason}`, 'error');
|
setSyncStatus(`Failed: ${result.reason}`, 'error');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
$('#btnCancelSyncImport').addEventListener('click', () => {
|
$('#btnCancelSyncImport').addEventListener('click', () => {
|
||||||
$('#syncImportSection').style.display = 'none';
|
$('#syncImportSection').style.display = 'none';
|
||||||
@@ -1034,22 +1040,34 @@ async function initSyncEncryptionUI() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is re-verification (key exists) or first-device (needs full auth)
|
|
||||||
const cached = await SilentSendCrypto.getCachedKey();
|
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
|
// If there's a pending sync import, use authenticateForSync
|
||||||
|
// (derives a temporary key with the source salt, doesn't touch local config)
|
||||||
|
if (window.__ssPendingSyncImport) {
|
||||||
|
result = await SilentSendSync.authenticateForSync(password);
|
||||||
|
} else {
|
||||||
|
// Normal auth: check if re-verification or first-device
|
||||||
|
const cached = await SilentSendCrypto.getCachedKey();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
// Re-verification — password alone is enough
|
|
||||||
result = await SilentSendSync.reverifyWithPassword(password);
|
result = await SilentSendSync.reverifyWithPassword(password);
|
||||||
} else {
|
} else {
|
||||||
// First device — full auth with password + TOTP if configured
|
|
||||||
result = await SilentSendSync.authenticate(password, totpCode || undefined);
|
result = await SilentSendSync.authenticate(password, totpCode || undefined);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$('#syncAuthPrompt').style.display = 'none';
|
$('#syncAuthPrompt').style.display = 'none';
|
||||||
$('#syncAuthPassword').value = '';
|
$('#syncAuthPassword').value = '';
|
||||||
$('#syncAuthTOTPForPassword').value = '';
|
$('#syncAuthTOTPForPassword').value = '';
|
||||||
setSyncEncStatus(cached ? 'Re-verified with password.' : 'Authenticated. Sync data unlocked.', 'ok');
|
setSyncEncStatus('Authenticated.', 'ok');
|
||||||
|
|
||||||
|
// If there's a pending sync import, retry it now
|
||||||
|
if (window.__ssPendingSyncImport) {
|
||||||
|
const retry = window.__ssPendingSyncImport;
|
||||||
|
window.__ssPendingSyncImport = null;
|
||||||
|
await retry();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setSyncAuthStatus(result.reason, 'error');
|
setSyncAuthStatus(result.reason, 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user