diff --git a/dist/index.html b/dist/index.html index 641a962..baed6a5 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,9 +5,9 @@ Password Vault - - diff --git a/src/components/ImportExport.svelte b/src/components/ImportExport.svelte index b1a6950..09f1222 100644 --- a/src/components/ImportExport.svelte +++ b/src/components/ImportExport.svelte @@ -1,6 +1,7 @@ @@ -78,7 +101,7 @@ @@ -246,4 +300,24 @@ font-size: 0.85rem; padding: 8px; } + + .form-group { + margin-bottom: 16px; + } + + .form-group input[type="password"] { + width: 100%; + padding: 8px 12px; + font-size: 0.85rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-surface); + color: var(--color-text); + box-sizing: border-box; + } + + .form-group input[type="password"]:focus { + outline: none; + border-color: var(--color-primary); + } diff --git a/src/lib/storage/db.js b/src/lib/storage/db.js index b3f3cea..599b2f4 100644 --- a/src/lib/storage/db.js +++ b/src/lib/storage/db.js @@ -12,6 +12,7 @@ */ import { openDB } from 'idb' +import { deriveKey, decrypt, encrypt } from '../crypto/crypto.js' const DB_NAME = 'password-vault' const DB_VERSION = 1 @@ -281,7 +282,9 @@ export async function getEntryCountsByGroup() { /** * Export all data (entries + groups + meta) as a JSON object. - * Entries remain encrypted — the importer needs the same master password. + * Entries remain encrypted with the source vault's key. The import function + * requires the source vault's master password to decrypt and re-encrypt + * entries under the target vault's key. * * @returns {Promise} */ @@ -307,32 +310,58 @@ export async function exportAll() { } } +/** + * Convert a base64 string back to Uint8Array. + * @param {string} base64 + * @returns {Uint8Array} + */ +function base64ToUint8Array(base64) { + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + return bytes +} + /** * Import data from a previously exported JSON object. * + * Requires the source vault's master password to decrypt entries, then + * re-encrypts them under the target vault's current encryption key. + * The target vault's meta (salt, test payload) is never overwritten. + * * @param {Object} data * @param {'merge'|'replace'} mode - 'merge' adds to existing, 'replace' clears first + * @param {string} sourcePassword - Master password of the source vault + * @param {CryptoKey} targetKey - Current encryption key of the target vault * @returns {Promise<{ imported: { entries: number, groups: number }, skipped: number }>} */ -export async function importAll(data, mode = 'merge') { +export async function importAll(data, mode = 'merge', sourcePassword = '', targetKey = null) { if (!data || !Array.isArray(data.entries) || !Array.isArray(data.groups)) { throw new Error('Invalid import data format') } + // Derive the source vault's key from its salt + password + let sourceKey = null + if (data.meta?.salt && sourcePassword) { + const sourceSalt = base64ToUint8Array(data.meta.salt) + sourceKey = await deriveKey(sourcePassword, sourceSalt) + } + const db = await getDb() if (mode === 'replace') { await db.clear('entries') await db.clear('groups') - // Clear meta so user can re-setup - await db.clear('meta') + // Do NOT clear meta — preserve the target vault's identity } let skipped = 0 let importedEntries = 0 let importedGroups = 0 - // Import groups + // Import groups (groups are not encrypted) for (const group of data.groups) { try { await db.put('groups', group) @@ -342,26 +371,32 @@ export async function importAll(data, mode = 'merge') { } } - // Import entries + // Import entries — decrypt with source key, re-encrypt with target key for (const entry of data.entries) { try { - await db.put('entries', entry) + let reencryptedEntry = { ...entry } + + if (sourceKey && targetKey && entry.encryptedPassword) { + // Decrypt password with source key + const plaintext = await decrypt(entry.encryptedPassword, sourceKey) + // Re-encrypt under target vault's key + reencryptedEntry.encryptedPassword = await encrypt(plaintext, targetKey) + } else if (!sourceKey || !targetKey) { + // Can't re-encrypt — skip this entry with a warning + console.warn('Skipping entry (missing source password or target key):', entry.title) + skipped++ + continue + } + + await db.put('entries', reencryptedEntry) importedEntries++ - } catch { + } catch (e) { + console.warn('Failed to import entry:', entry.title, e) skipped++ } } - // Restore meta if present - if (data.meta?.salt) { - await db.put('meta', { key: 'salt', value: data.meta.salt }) - } - if (data.meta?.testEncrypted) { - await db.put('meta', { key: 'testEncrypted', value: data.meta.testEncrypted }) - } - if (data.meta?.testPlaintext) { - await db.put('meta', { key: 'testPlaintext', value: data.meta.testPlaintext }) - } + // Never overwrite the target vault's meta return { imported: { entries: importedEntries, groups: importedGroups },