Skip to content

Commit d451836

Browse files
author
Jarvis
committed
Fix: Critical vault persistence bug
PROBLEM: - Groups and keys disappearing after extension restart - Password becoming 'wrong' after certain operations - Data loss when storage operations fail ROOT CAUSES: 1. No error handling on chrome.storage.local.set() - failures were silent 2. New salt generated on every save - if save failed, old data unrecoverable 3. No verification after write - corrupted data went undetected FIXES: 1. Write-ahead backup pattern: backup before overwriting 2. Post-write verification: decrypt and validate after save 3. Automatic rollback: restore backup if verification fails 4. Error surfacing: alert user when save fails 5. Console logging: track storage operations for debugging 6. Storage quota check utility for diagnostics Now if a save fails for ANY reason (quota, corruption, race condition), the vault automatically restores from backup and alerts the user.
1 parent ece6e41 commit d451836

File tree

2 files changed

+143
-17
lines changed

2 files changed

+143
-17
lines changed

src/popup/App.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,21 @@ function App() {
158158
}
159159

160160
async function handleVaultUpdate(updatedVault: VaultData) {
161-
setVaultData(updatedVault);
162-
// Save to storage
163-
const { saveVault } = await import('@/storage/vault');
164-
await saveVault(updatedVault, masterPassword);
165-
// Update background cache
166-
await cacheVaultInBackground(updatedVault, masterPassword);
161+
try {
162+
// Save to storage FIRST before updating UI state
163+
const { saveVault } = await import('@/storage/vault');
164+
await saveVault(updatedVault, masterPassword);
165+
166+
// Only update state after successful save
167+
setVaultData(updatedVault);
168+
169+
// Update background cache
170+
await cacheVaultInBackground(updatedVault, masterPassword);
171+
} catch (error) {
172+
console.error('Failed to save vault update:', error);
173+
alert('⚠️ Failed to save changes. Please try again. If this persists, export your vault backup from Settings.');
174+
// Don't update UI state if save failed - keep showing old data
175+
}
167176
}
168177

169178
if (screen === 'loading') {

src/storage/vault.ts

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ export { isPersonalKey, isContactKey } from '@/types';
3636

3737
const CURRENT_VAULT_VERSION = 2;
3838

39+
/**
40+
* Check available storage space
41+
* Returns approximate bytes available (chrome.storage.local has ~5MB-10MB limit)
42+
*/
43+
export async function checkStorageQuota(): Promise<{ bytesUsed: number; quotaBytes: number; percentUsed: number }> {
44+
const bytesUsed = await chrome.storage.local.getBytesInUse();
45+
// Chrome local storage quota is typically 5MB for extensions, 10MB with unlimitedStorage permission
46+
const quotaBytes = 5 * 1024 * 1024; // 5MB default
47+
const percentUsed = Math.round((bytesUsed / quotaBytes) * 100);
48+
49+
console.log(`📊 Storage: ${bytesUsed} bytes used (${percentUsed}% of ~${quotaBytes} bytes)`);
50+
51+
return { bytesUsed, quotaBytes, percentUsed };
52+
}
53+
3954
// ============================================================================
4055
// Vault Lifecycle
4156
// ============================================================================
@@ -59,7 +74,34 @@ export async function createVault(masterPassword: string): Promise<void> {
5974
data: encrypted,
6075
};
6176

62-
await chrome.storage.local.set({ [STORAGE_KEYS.VAULT]: encryptedVault });
77+
try {
78+
await chrome.storage.local.set({ [STORAGE_KEYS.VAULT]: encryptedVault });
79+
80+
// Verify the write succeeded
81+
const verifyResult = await chrome.storage.local.get(STORAGE_KEYS.VAULT);
82+
if (!verifyResult[STORAGE_KEYS.VAULT]) {
83+
throw new Error('Vault creation failed: verification failed');
84+
}
85+
86+
// Verify we can decrypt
87+
const testDecrypt = await decryptVault(
88+
encryptedVault.data,
89+
encryptedVault.iv,
90+
encryptedVault.salt,
91+
masterPassword
92+
);
93+
94+
if (!testDecrypt) {
95+
throw new Error('Vault creation failed: decryption verification failed');
96+
}
97+
98+
console.log('✅ Vault created successfully');
99+
} catch (error) {
100+
console.error('❌ Vault creation failed:', error);
101+
// Clean up any partial write
102+
await chrome.storage.local.remove(STORAGE_KEYS.VAULT);
103+
throw error;
104+
}
63105
}
64106

65107
/**
@@ -117,22 +159,97 @@ async function migrateVaultV1ToV2(_oldVault: VaultData): Promise<VaultData> {
117159

118160
/**
119161
* Save vault data (re-encrypt with password)
162+
*
163+
* SAFETY: Uses write-ahead pattern with verification:
164+
* 1. Backup current vault before overwriting
165+
* 2. Write new vault
166+
* 3. Verify the write succeeded by re-reading
167+
* 4. If verification fails, attempt to restore backup
120168
*/
121169
export async function saveVault(
122170
vaultData: VaultData,
123171
masterPassword: string
124172
): Promise<void> {
125-
const vaultJson = JSON.stringify(vaultData);
126-
const { salt, iv, encrypted } = await encryptVault(vaultJson, masterPassword);
173+
const BACKUP_KEY = 'vault_backup';
127174

128-
const encryptedVault: EncryptedVault = {
129-
version: CURRENT_VAULT_VERSION,
130-
salt,
131-
iv,
132-
data: encrypted,
133-
};
134-
135-
await chrome.storage.local.set({ [STORAGE_KEYS.VAULT]: encryptedVault });
175+
try {
176+
// Step 1: Backup current vault before overwriting
177+
const currentResult = await chrome.storage.local.get(STORAGE_KEYS.VAULT);
178+
const currentVault = currentResult[STORAGE_KEYS.VAULT];
179+
if (currentVault) {
180+
await chrome.storage.local.set({ [BACKUP_KEY]: currentVault });
181+
}
182+
183+
// Step 2: Encrypt and save new vault
184+
const vaultJson = JSON.stringify(vaultData);
185+
const { salt, iv, encrypted } = await encryptVault(vaultJson, masterPassword);
186+
187+
const encryptedVault: EncryptedVault = {
188+
version: CURRENT_VAULT_VERSION,
189+
salt,
190+
iv,
191+
data: encrypted,
192+
};
193+
194+
await chrome.storage.local.set({ [STORAGE_KEYS.VAULT]: encryptedVault });
195+
196+
// Step 3: Verify the write succeeded by attempting to decrypt
197+
const verifyResult = await chrome.storage.local.get(STORAGE_KEYS.VAULT);
198+
const savedVault = verifyResult[STORAGE_KEYS.VAULT] as EncryptedVault | undefined;
199+
200+
if (!savedVault) {
201+
throw new Error('Vault save verification failed: vault not found after save');
202+
}
203+
204+
// Verify we can decrypt with the password
205+
const testDecrypt = await decryptVault(
206+
savedVault.data,
207+
savedVault.iv,
208+
savedVault.salt,
209+
masterPassword
210+
);
211+
212+
if (!testDecrypt) {
213+
throw new Error('Vault save verification failed: decryption test failed');
214+
}
215+
216+
// Parse and verify data integrity
217+
const parsedVault = JSON.parse(testDecrypt) as VaultData;
218+
if (!parsedVault.keys || !parsedVault.groups) {
219+
throw new Error('Vault save verification failed: data structure invalid');
220+
}
221+
222+
// Verify counts match
223+
if (parsedVault.keys.length !== vaultData.keys.length ||
224+
parsedVault.groups.length !== vaultData.groups.length) {
225+
throw new Error('Vault save verification failed: data count mismatch');
226+
}
227+
228+
// Step 4: Clear backup on success
229+
await chrome.storage.local.remove(BACKUP_KEY);
230+
231+
console.log('✅ Vault saved successfully:', {
232+
keys: vaultData.keys.length,
233+
groups: vaultData.groups.length
234+
});
235+
236+
} catch (error) {
237+
console.error('❌ Vault save failed:', error);
238+
239+
// Attempt to restore backup
240+
try {
241+
const backupResult = await chrome.storage.local.get(BACKUP_KEY);
242+
const backup = backupResult[BACKUP_KEY];
243+
if (backup) {
244+
await chrome.storage.local.set({ [STORAGE_KEYS.VAULT]: backup });
245+
console.log('🔄 Restored vault from backup after save failure');
246+
}
247+
} catch (restoreError) {
248+
console.error('❌ Failed to restore backup:', restoreError);
249+
}
250+
251+
throw error; // Re-throw so caller knows save failed
252+
}
136253
}
137254

138255
// ============================================================================

0 commit comments

Comments
 (0)