Describe the bug
src/utils/credentials.ts unlockCredentials() has two distinct branches:
- Salt missing → first-time setup, creates salt, derives key, returns
true
- Salt present → existing vault, derives key, tries to decrypt a sample credential to verify the passphrase
The verification step is guarded by this check on line 141:
const encryptedCreds = unmarkEncrypted(encryptedLocal);
if (Object.keys(encryptedCreds).length > 0) {
// Passphrase verification via decryption
try {
...
await crypto.subtle.decrypt(...);
...
} catch {
return false;
}
}
// ← If no encrypted creds exist, falls through and accepts ANY passphrase
derivedKey = key;
return true;
When encryptedCreds is empty — i.e. the vault has been initialized (salt is stored) but the user hasn't saved any API keys yet — the verification block is skipped entirely. Any passphrase is accepted and used to derive derivedKey.
This creates a silent, irreversible lock-out:
1. User sets up vault with passphrase "correct-horse"
→ Salt written to chrome.storage.local
→ No API keys saved yet (user closes popup before saving)
2. Next session: user misremembers, types "correct-horse-battery"
→ storedSalt found → no encryptedCreds → verification skipped
→ derivedKey = PBKDF2("correct-horse-battery", salt)
→ returns true ← vault "unlocks" silently with WRONG passphrase
3. User saves their OpenAI key
→ encrypted with wrong-passphrase-derived key
→ stored to chrome.storage.local
4. Next session: user types "correct-horse" (actual passphrase)
→ storedSalt found → encryptedCreds found → verification runs
→ decrypt("correct-horse", ciphertext) fails — was encrypted with "correct-horse-battery"
→ returns false ← locked out permanently
5. User tries both passphrases, neither works. Data is unrecoverable.
There is no recovery path. The only escape is clearing all extension storage, losing any saved credentials.
Steps to reproduce
// 1. Initialize vault (first-time setup)
await unlockCredentials("correct-passphrase"); // true — salt created
await lockCredentials();
// 2. Unlock with WRONG passphrase before saving any credentials
const result = await unlockCredentials("WRONG-passphrase");
console.log(result); // true ← BUG: should be false or undefined
// 3. Save a credential (now encrypted with wrong key)
await saveApiCredentials({ openai_api_key: "sk-abc123" });
await lockCredentials();
// 4. Try to unlock with correct passphrase
const result2 = await unlockCredentials("correct-passphrase");
console.log(result2); // false — locked out permanently
Expected behavior
unlockCredentials() must be able to verify the passphrase even when no API credentials are stored yet.
The standard fix is a vault sentinel — a small known-plaintext value encrypted with the derived key during first-time setup, stored separately from API credentials, and always used for passphrase verification:
const VAULT_SENTINEL_KEY = "vault_sentinel";
const VAULT_SENTINEL_PLAINTEXT = "late-meet-vault-v1";
// During first-time setup (no storedSalt):
const sentinelCiphertext = await encrypt(VAULT_SENTINEL_PLAINTEXT); // uses newly derived key
await chrome.storage.local.set({
[VAULT_SENTINEL_KEY]: sentinelCiphertext,
});
// During subsequent unlocks (storedSalt present):
const { [VAULT_SENTINEL_KEY]: sentinel } =
await chrome.storage.local.get([VAULT_SENTINEL_KEY]);
if (sentinel) {
try {
const plaintext = await decrypt(sentinel); // uses newly derived key
if (plaintext !== VAULT_SENTINEL_PLAINTEXT) return false;
} catch {
return false; // Wrong passphrase
}
}
// Sentinel verified → safe to set derivedKey
derivedKey = key;
return true;
This guarantees passphrase verification regardless of whether API credentials have been saved, and costs only one small extra storage write during vault initialization.
Additional context
- The window where this bug is exploitable is any time between vault initialization and the first
saveApiCredentials() call — which is exactly the new-user onboarding flow.
- The bug is silent — no error is thrown and
true is returned, giving the user no indication anything went wrong.
- Recovery requires manually clearing
chrome.storage.local via DevTools, which is not documented anywhere in the extension's UI.
I'd like to fix this under GSSoC 2026 — please assign it to me if triaged.
Describe the bug
src/utils/credentials.tsunlockCredentials()has two distinct branches:trueThe verification step is guarded by this check on line 141:
When
encryptedCredsis empty — i.e. the vault has been initialized (salt is stored) but the user hasn't saved any API keys yet — the verification block is skipped entirely. Any passphrase is accepted and used to derivederivedKey.This creates a silent, irreversible lock-out:
There is no recovery path. The only escape is clearing all extension storage, losing any saved credentials.
Steps to reproduce
Expected behavior
unlockCredentials()must be able to verify the passphrase even when no API credentials are stored yet.The standard fix is a vault sentinel — a small known-plaintext value encrypted with the derived key during first-time setup, stored separately from API credentials, and always used for passphrase verification:
This guarantees passphrase verification regardless of whether API credentials have been saved, and costs only one small extra storage write during vault initialization.
Additional context
saveApiCredentials()call — which is exactly the new-user onboarding flow.trueis returned, giving the user no indication anything went wrong.chrome.storage.localvia DevTools, which is not documented anywhere in the extension's UI.I'd like to fix this under GSSoC 2026 — please assign it to me if triaged.