fix: cross-browser sync decryption + WebAuthn on extension pages

Sync code import between browsers was failing with "Wrong key or
corrupted data" because each browser derives a different AES key
from the same password (different salt). The decrypt function was
using the local device's salt instead of the source device's salt
embedded in the sync envelope.

Fixed _decryptFromSync to always use the sync envelope's salt for
decryption. If the cached key's salt doesn't match, forces re-auth
so the password is re-entered and a new key is derived with the
correct salt.

Also fixed WebAuthn "device can't be used" error on extension pages.
chrome-extension:// and moz-extension:// origins are not valid for
WebAuthn. isWebAuthnAvailable() now returns false on extension pages.

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-27 04:38:25 +00:00
parent f284238792
commit d1bc43d02d
2 changed files with 30 additions and 8 deletions
+3
View File
@@ -374,6 +374,9 @@ const SilentSendCrypto = {
* Check if WebAuthn is available in this browser.
*/
isWebAuthnAvailable() {
// WebAuthn doesn't work on extension pages (chrome-extension:// origin)
if (typeof location !== 'undefined' && location.protocol === 'chrome-extension:') return false;
if (typeof location !== 'undefined' && location.protocol === 'moz-extension:') return false;
return !!(window.PublicKeyCredential && navigator.credentials);
},
+27 -8
View File
@@ -460,23 +460,42 @@ const SilentSendSync = {
async _decryptFromSync(data) {
if (!data?._ssEncrypted) return { data, decrypted: false };
// If this device has no encryption config yet, bootstrap from the sync envelope
// Always use the sync envelope's salt/verificationBlob for decryption,
// not the local config. Different devices have different salts, so the
// local key won't decrypt data encrypted with another device's salt.
let config = await this._getSyncEncryption();
if (!config?.enabled && data._encConfig) {
// Save minimal config so authenticate() can work
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,
authMethod: 'password', // will be updated from _encMeta after decryption
ttlDays: 90,
webauthn: false,
};
await this._saveSyncEncryption(config);
}
const keyInfo = await this._getEncryptionKey();
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 };
}