Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/utils/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function normalizedCredential(value: unknown): string | undefined {

const AES_ALGORITHM = "AES-GCM";
const AES_KEY_LENGTH = 256;
const VAULT_VERIFICATION_KEY = "credential_vault_verification";
const IV_LENGTH = 12;
const PBKDF2_ITERATIONS = 100_000;
const SALT_LENGTH = 16;
Expand Down Expand Up @@ -136,20 +137,26 @@ export async function unlockCredentials(passphrase: string): Promise<boolean> {

if (typeof storedSalt === "string") {
const key = await deriveKeyFromPassphrase(passphrase, base64ToArrayBuffer(storedSalt));
const encryptedLocal = await chrome.storage.local.get(CREDENTIAL_KEYS);
const encryptedCreds = unmarkEncrypted(encryptedLocal);
if (Object.keys(encryptedCreds).length > 0) {
try {
const stored = await chrome.storage.local.get([...CREDENTIAL_KEYS, VAULT_VERIFICATION_KEY]);
const encryptedCreds = unmarkEncrypted(stored);
const verificationToken = stored[VAULT_VERIFICATION_KEY];
try {
if (verificationToken && typeof verificationToken === "string") {
const combined = base64ToArrayBuffer(verificationToken);
const iv = new Uint8Array(combined.slice(0, IV_LENGTH));
const ciphertext = combined.slice(IV_LENGTH);
await crypto.subtle.decrypt({ name: AES_ALGORITHM, iv }, key, ciphertext);
} else {
const sampleKey = CREDENTIAL_KEYS.find((k) => encryptedCreds[k]);
if (sampleKey && encryptedCreds[sampleKey]) {
const combined = base64ToArrayBuffer(encryptedCreds[sampleKey]);
const iv = new Uint8Array(combined.slice(0, IV_LENGTH));
const ciphertext = combined.slice(IV_LENGTH);
await crypto.subtle.decrypt({ name: AES_ALGORITHM, iv }, key, ciphertext);
}
} catch {
return false;
}
} catch {
return false;
}
derivedKey = key;
resetAutoLockTimer();
Expand All @@ -164,8 +171,23 @@ export async function unlockCredentials(passphrase: string): Promise<boolean> {
}

const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
await chrome.storage.local.set({ [SALT_STORAGE_KEY]: arrayBufferToBase64(salt.buffer) });
derivedKey = await deriveKeyFromPassphrase(passphrase, salt.buffer);

const verificationIv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const verificationPlaintext = new TextEncoder().encode("vault-verification-token");
const verificationCiphertext = await crypto.subtle.encrypt(
{ name: AES_ALGORITHM, iv: verificationIv },
derivedKey,
verificationPlaintext,
);
const verificationCombined = new Uint8Array(verificationIv.length + verificationCiphertext.byteLength);
verificationCombined.set(verificationIv);
verificationCombined.set(new Uint8Array(verificationCiphertext), verificationIv.length);

await chrome.storage.local.set({
[SALT_STORAGE_KEY]: arrayBufferToBase64(salt.buffer),
[VAULT_VERIFICATION_KEY]: arrayBufferToBase64(verificationCombined.buffer),
});
resetAutoLockTimer();
return true;
}
Expand Down