From 3c786366fcaa5fceac4bfa8d94827f8e497e8d96 Mon Sep 17 00:00:00 2001 From: Zaki <18zaki18@gmail.com> Date: Sat, 23 May 2026 05:35:03 +0100 Subject: [PATCH] feat(export): encrypt vault backups with an independent password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the plaintext JSON exporter with an encrypted container. The previous plaintext flow was a known security gap — a backup file on disk or in a cloud sync folder gave full plaintext access to every password if accessed by anyone (or anything) other than the user. Container format ================ Self-describing JSON: { "format": "pm-encrypted-export-v1", "kdf": "pbkdf2-sha256", "kdf_iterations": 600000, "kdf_salt": "", "iv": "", "ciphertext": "", "created_at": "" } payload = same shape as the legacy plaintext exporter (entries array with site, username, password, folder, tags, favorite, totp_secret, timestamps), so the round-trip through the JSON importer works without a separate code path. Export password =============== User-chosen, INDEPENDENT of the master password — the export modal explicitly explains this. Rationale: - A master-password change doesn't invalidate old backups. - The backup file can be shared with another person without revealing the master pw. - Trade-off: one more password for the user to remember. We assume they're storing the backup intentionally and can record the pw. Minimum length 6 enforced client-side. Flow ==== Export: 1. askReauth(master pw) → server /reauth verifies (defense against someone reaching the unlocked laptop and dumping the vault). 2. promptDialog(password: true) → export password. 3. Walk state.entries, decrypt each password + TOTP with the vault key, assemble payload. 4. encryptExportPayload(payload, exportPwd) — random 32B salt, random 12B IV, PBKDF2 600k, AES-GCM-256. 5. Download the container as vault-export-YYYY-MM-DD.json. Import: 1. Read file, detect format. JSON with format === "pm-encrypted- export-v1" → prompt for the export password. 2. decryptExportContainer → plaintext payload, then JSON.stringify back into the existing parseEntriesFromJSON path so the rest of the import flow (preview confirm, bulk encrypt, /entries/bulk- import) is unchanged. 3. Wrong password → AES-GCM tag fails → "Decryption failed" toast, user retries. Other changes ============= - promptDialog gains a `password: true` option that flips the confirm input's type so the value is masked on screen. - Export modal copy in the Settings panel updated to mention the encrypted format and the independent password. - The 429-lockout path on /reauth is now handled explicitly in doExport (was previously falling through to "wrong password"). Backward compatibility ====================== Plaintext JSON exports produced by the previous version still import — parseEntriesFromJSON doesn't care whether the input came from a fresh decryption or directly from a plaintext file. The exporter no longer produces plaintext though; users with old backups should re-export after upgrading. --- index.html | 4 +- js/app.js | 180 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 168 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 635a13e..944fdce 100644 --- a/index.html +++ b/index.html @@ -344,7 +344,9 @@
Export

- Download all your passwords as JSON. Requires your master password — file is plaintext. + Download an encrypted backup of your vault. You'll + choose a password independent of your master password — + save it carefully, you need it to restore.