From 800feb1d37896ae03e71c503cc270a496bcf6346 Mon Sep 17 00:00:00 2001 From: hermes-explorigin Date: Wed, 26 Aug 2026 23:38:25 +0000 Subject: [PATCH] Export can be sealed with a separate password; import accepts file with a different password - exportSelected(groupIds, vaultKey, exportPassword=''): plain JSON export unchanged when no export password; when one is set, re-keys each entry's password to a key derived from the export password and AES-256-GCM-seals the entire payload (titles/usernames/notes protected too). Returns a { format: 'encrypted-export', salt, data } envelope. - importAll() detects sealed exports and treats the supplied password as the EXPORT password, so it may differ from any vault's master password. Wrong password rejects import instead of silently skipping entries. - ImportExport.svelte: optional 'separate password' field in the export dialog; import dialog's field reworded as a generic file password covering both plain and sealed files. - Tests for sealed export/import round-trip incl. wrong-password & missing-password rejects. --- AGENTS.md | 9 ++- src/components/ImportExport.svelte | 27 +++++-- src/lib/storage/db.js | 110 ++++++++++++++++++++++------- tests/lib/storage/db.test.js | 76 +++++++++++++++++++- 4 files changed, 187 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 375f47a..5e2d45f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,10 +81,13 @@ Password verification uses a test payload (random string encrypted at vault crea - Clipboard auto-clears after 15 seconds. - No browser fingerprinting or anti-keylogger protections. -## Export +## Export / Import -- `exportSelected(groupIds)` replaces the old `exportAll()` — accepts an array of group IDs to export. Pass `null` or `[]` for a full export. Vault meta (salt, test payload) is always included for import decryption. -- `ImportExport.svelte` fetches groups/entries on modal open and shows a checkbox list for group selection with live entry count. +- `exportSelected(groupIds, vaultKey, exportPassword)` — group IDs to export; `null`/`[]` = full export (include `''` for ungrouped). Vault meta (salt, test payload) is always included for plain import decryption. + - **Plain export** (no export password): unchanged — entries keep their source-vault-encrypted passwords; import needs the source vault's master password. + - **Password-protected export**: pass a `vaultKey` (in-memory) plus an independent `exportPassword`. Re-keys every exported entry's password to the export-derived key and AES-256-GCM-seals the entire payload (titles/usernames/notes included). Returns `{ format: 'encrypted-export', salt, data }`. +- `importAll(data, mode, sourcePassword, targetKey)` detects a sealed export (`data.format === 'encrypted-export'`): `sourcePassword` is then the EXPORT password and may differ from any vault's master password. A wrong export password rejects the import (it never silently skips entries). The entry loop decrypts with the source key and re-encrypts under `targetKey`. +- `ImportExport.svelte` fetches groups/entries on modal open and shows a checkbox list for group selection with live entry count. The export dialog has an optional "separate password" field; the import dialog's field is a generic file password (export password for sealed files, source vault master for plain files). ## Known Bug Fixes diff --git a/src/components/ImportExport.svelte b/src/components/ImportExport.svelte index 6f9a206..d02cc16 100644 --- a/src/components/ImportExport.svelte +++ b/src/components/ImportExport.svelte @@ -22,6 +22,7 @@ let exporting = $state(false) let sourcePassword = $state('') let parsedFileData = $state(null) + let exportPassword = $state('') // Group selection for export let allGroups = $state([]) @@ -37,7 +38,12 @@ async function handleExport() { exporting = true try { - exportData = await exportSelected(selectedGroupIds.length === allGroups.length ? null : selectedGroupIds) + exportData = await exportSelected( + selectedGroupIds.length === allGroups.length ? null : selectedGroupIds, + app.encryptionKey, + exportPassword.trim() + ) + exportPassword = '' const json = JSON.stringify(exportData, null, 2) const blob = new Blob([json], { type: 'application/json' }) const url = URL.createObjectURL(blob) @@ -137,7 +143,7 @@