Skip to content

Commit 98489fc

Browse files
committed
fix: prevent storage.sync data loss and harden key/backup flows
Sync merge: - Match profiles by pubKey, not array index, so a reordered or partial sync can no longer overwrite one identity's key material with another's. - Treat bunker/remote-signer profiles (privKey:'' but a real identity) as identities in fresh-install detection, so they aren't wiped as blanks. - Stop syncing isEncrypted: the verifier is never synced, so adopting a remote isEncrypted=true permanently locked the second device out. - Add startup self-heal that clears a bogus isEncrypted flag when no password verifier and no encrypted blobs exist. - Extract pure computeMergeUpdates() so the merge rules are unit-testable. Key/backup flows: - savePrivateKey now replies via sendResponse + return true (MV3 does not deliver Promise returns), so imported keys can't silently fail. - Roll back the half-created profile when a key fails to persist. - Backup export encrypts with a supplied backup password instead of the session key, so users with no master password can export, even locked. Tests: add sync-merge and backup-password regression suites (+13).
1 parent 3d7ac4a commit 98489fc

28 files changed

Lines changed: 61503 additions & 184 deletions

distros/safari/api-keys/api-keys.build.js

Lines changed: 786 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/background.build.js

Lines changed: 10691 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/background.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ const permissionRateMap = new Map(); // host → { count, resetAt }
128128
await storage.set({ isEncrypted: true });
129129
data.isEncrypted = true;
130130
}
131+
// Lockout recovery: if isEncrypted=true but there is NO password verifier AND
132+
// no actually-encrypted key blobs, encryption is bogus (e.g. a stale flag
133+
// received from an older buggy sync). Clearing it prevents a permanent lockout
134+
// where the user can never unlock because checkPassword() always fails.
135+
// We ONLY clear when no encrypted blobs exist — never when real ciphertext is
136+
// present (that would corrupt encrypted keys into "plaintext").
137+
if (data.isEncrypted && !data.passwordHash) {
138+
const { profiles = [] } = await storage.get({ profiles: [] });
139+
const hasEncryptedBlob = profiles.some(p => isEncryptedBlob(p.privKey));
140+
if (!hasEncryptedBlob) {
141+
log('[STARTUP] Lockout recovery: isEncrypted=true with no passwordHash and no encrypted blobs → clearing bogus encryption flag');
142+
await storage.set({ isEncrypted: false });
143+
data.isEncrypted = false;
144+
}
145+
}
131146
encryptionEnabled = data.isEncrypted;
132147
nostrAccessWhileLocked = !!data.nostrAccessWhileLocked;
133148
blockCrossOriginFrames = data.blockCrossOriginFrames !== false;
@@ -419,7 +434,20 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
419434
return true; // Keep message channel open for async sendResponse
420435
case 'savePrivateKey':
421436
resetAutoLock();
422-
return savePrivateKey(message.payload);
437+
// Must use sendResponse + return true (not a Promise return): Chrome MV3
438+
// does not deliver Promise-return values to sendMessage callers, so the
439+
// caller could not tell whether the key was actually saved (or whether it
440+
// threw). That made imported keys silently fail while the UI showed success.
441+
(async () => {
442+
try {
443+
await savePrivateKey(message.payload);
444+
sendResponse({ success: true });
445+
} catch (e) {
446+
console.error('savePrivateKey error:', e);
447+
sendResponse({ success: false, error: e.message || 'Failed to save key' });
448+
}
449+
})();
450+
return true;
423451
case 'getNpub':
424452
(async () => {
425453
try {
@@ -1118,8 +1146,13 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
11181146
// --- Encrypted vault backup / restore ---
11191147
case 'backup.export':
11201148
reply(sendResponse, async () => {
1121-
if (!sessionCryptoKey) {
1122-
return { success: false, error: 'Extension must be unlocked to create a backup' };
1149+
// Backups are encrypted with a dedicated backup password supplied at
1150+
// export time — NOT the in-memory session key. This lets users with no
1151+
// master password create backups, and works even while locked (the
1152+
// stored key blobs stay encrypted and get wrapped again here).
1153+
const password = message.payload?.password;
1154+
if (typeof password !== 'string' || password.length < 8) {
1155+
return { success: false, error: 'A backup password of at least 8 characters is required' };
11231156
}
11241157
const data = await storage.get({
11251158
profiles: [],
@@ -1135,7 +1168,7 @@ api.runtime.onMessage.addListener((message, _sender, sendResponse) => {
11351168
version: null,
11361169
});
11371170
const plaintext = JSON.stringify(data);
1138-
const encrypted = await encryptWithKey(plaintext, sessionCryptoKey, sessionKeySalt);
1171+
const encrypted = await encryptBlob(plaintext, password);
11391172
const version = api.runtime.getManifest?.()?.version || 'unknown';
11401173
return {
11411174
success: true,

distros/safari/content.build.js

Lines changed: 363 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/event_history/event_history.build.js

Lines changed: 5428 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/experimental/experimental.build.js

Lines changed: 245 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/nostr-keys/nostr-keys.build.js

Lines changed: 5576 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/nostr.build.js

Lines changed: 113 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

distros/safari/options.build.css

Lines changed: 1907 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)