05/22 Enhance codes and fix bugs 2

This commit is contained in:
2026-05-22 10:46:43 -04:00
parent 3231c8eb56
commit f5dc6660c5
7 changed files with 80 additions and 29 deletions
+22 -10
View File
@@ -103,9 +103,10 @@ const Recover = (() => {
/**
* Derive an AES-256-GCM key from the recovery code using PBKDF2.
* Salt is fixed to 'passkeeper-recovery' — the recovery code itself is the secret.
* `salt` should be the user's email (per-user uniqueness).
* Falls back to the legacy fixed salt for backward compatibility.
*/
async function deriveRecoveryKey(recoveryCode) {
async function deriveRecoveryKey(recoveryCode, salt = "passkeeper-recovery") {
const baseKey = await subtle.importKey(
"raw",
strToBytes(recoveryCode),
@@ -116,7 +117,7 @@ const Recover = (() => {
return subtle.deriveKey(
{
name: "PBKDF2",
salt: strToBytes("passkeeper-recovery"),
salt: strToBytes(salt),
iterations: 200_000,
hash: "SHA-256",
},
@@ -216,15 +217,26 @@ const Recover = (() => {
}
const data = await res.json();
// Attempt to decrypt enc_key_salt using the recovery code
const recoveryKey = await deriveRecoveryKey(rawCode);
// Attempt to decrypt enc_key_salt using the recovery code.
// Try email-as-salt first (new format), fall back to the legacy fixed salt
// so that recovery codes generated before this fix still work.
let decryptedEncKeySalt;
try {
decryptedEncKeySalt = await decryptEncKeySalt(
recoveryKey,
data.recovery_enc_salt,
data.recovery_iv,
);
const keyWithEmail = await deriveRecoveryKey(rawCode, email);
try {
decryptedEncKeySalt = await decryptEncKeySalt(
keyWithEmail,
data.recovery_enc_salt,
data.recovery_iv,
);
} catch {
const keyLegacy = await deriveRecoveryKey(rawCode);
decryptedEncKeySalt = await decryptEncKeySalt(
keyLegacy,
data.recovery_enc_salt,
data.recovery_iv,
);
}
} catch {
showError(
"recover-error-1",
+12 -2
View File
@@ -3442,13 +3442,23 @@ const Vault = (() => {
return;
}
// Use the user's email as a per-user PBKDF2 salt so precomputed tables
// cannot attack multiple users at once.
const meRes = await apiFetch("/api/auth/me");
if (!meRes || !meRes.ok) {
showToast("Session error. Please reload.", "error");
return;
}
const meData = await meRes.json();
const userEmail = meData.email;
// Generate a random 128-bit (16-byte) recovery code displayed as hex
const rawBytes = window.crypto.getRandomValues(new Uint8Array(16));
const recoveryCode = Array.from(rawBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// Derive recovery key from the code
// Derive recovery key from the code using the user's email as PBKDF2 salt
const recoveryKeyMaterial = await window.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(recoveryCode),
@@ -3459,7 +3469,7 @@ const Vault = (() => {
const recoveryKey = await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: new TextEncoder().encode("passkeeper-recovery"),
salt: new TextEncoder().encode(userEmail),
iterations: 200_000,
hash: "SHA-256",
},