fix: cross-browser sync import corrupting local encryption + v2.0.9

Root cause: when importing an encrypted sync code from another device,
_decryptFromSync was replacing the local encryption config (salt) with
the source device's salt. This caused:
1. Local key cache derived from wrong salt
2. Local data (encrypted with local salt) became unreadable
3. _applyData tried to write with the wrong key

Fixed with a complete refactor of cross-device decryption:
- authenticateForSync() derives a TEMPORARY key using the source salt
- Temporary key stored separately as 'tempSyncKey' in IndexedDB
- Local encryption config and cached key are NEVER modified
- After decryption, _applyData writes via _writeSecure using the
  LOCAL key (which uses the local salt)
- _handleDecryptedMeta extracted for code reuse

Also:
- Options.js auth handler detects pending sync import and routes to
  authenticateForSync instead of regular authenticate
- After auth success, automatically retries the import
- README updated: imported passwords are protected (dots in UI,
  vault password to reveal, AES-256 encrypted at rest)
- Bumped to v2.0.9

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-27 05:23:50 +00:00
parent f2fa4befdd
commit 97e37e65ac
7 changed files with 108 additions and 50 deletions
+14 -9
View File
@@ -1016,24 +1016,29 @@ async function initSyncEncryptionUI() {
return;
}
// Check if this is re-verification (key exists) or first-device (needs full auth)
const cached = await SilentSendCrypto.getCachedKey();
let result;
if (cached) {
// Re-verification — password alone is enough
result = await SilentSendSync.reverifyWithPassword(password);
// 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 {
// First device — full auth with password + TOTP if configured
result = await SilentSendSync.authenticate(password, totpCode || undefined);
// Normal auth: check if re-verification or first-device
const cached = await SilentSendCrypto.getCachedKey();
if (cached) {
result = await SilentSendSync.reverifyWithPassword(password);
} else {
result = await SilentSendSync.authenticate(password, totpCode || undefined);
}
}
if (result.success) {
$('#syncAuthPrompt').style.display = 'none';
$('#syncAuthPassword').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 that auth succeeded
// If there's a pending sync import, retry it now
if (window.__ssPendingSyncImport) {
const retry = window.__ssPendingSyncImport;
window.__ssPendingSyncImport = null;