diff --git a/extension/src/service-worker/vault.ts b/extension/src/service-worker/vault.ts new file mode 100644 index 0000000..3732a50 --- /dev/null +++ b/extension/src/service-worker/vault.ts @@ -0,0 +1,135 @@ +/// Vault operations module. +/// +/// Bridges the WASM crypto functions with the git host API to provide +/// high-level vault operations: fetch/decrypt manifest, fetch/decrypt entries, +/// encrypt/write entries, search, and URL matching. + +import type { GitHost } from './git-host'; +import type { Entry, Manifest, ManifestEntry } from '../shared/types'; + +// WASM module reference — set once during init. +let wasm: typeof import('idfoto-wasm') | null = null; + +/// Store the WASM module reference after dynamic import. +export function setWasm(w: typeof import('idfoto-wasm')): void { + wasm = w; +} + +function requireWasm(): NonNullable { + if (!wasm) throw new Error('WASM module not initialized'); + return wasm; +} + +/// Vault metadata: salt and KDF params stored unencrypted in the repo. +export interface VaultMeta { + salt: Uint8Array; + paramsJson: string; +} + +/// Read the vault salt and KDF params from the git repo. +export async function fetchVaultMeta(git: GitHost): Promise { + const saltBytes = await git.readFile('salt'); + const paramsRaw = await git.readFile('params.json'); + const paramsJson = new TextDecoder().decode(paramsRaw); + return { salt: saltBytes, paramsJson }; +} + +/// Fetch and decrypt the manifest from the git repo. +export async function fetchAndDecryptManifest( + git: GitHost, + masterKey: Uint8Array, +): Promise { + const w = requireWasm(); + const ciphertext = await git.readFile('manifest.enc'); + const json = w.decrypt_manifest(ciphertext, masterKey); + return JSON.parse(json) as Manifest; +} + +/// Fetch and decrypt a single entry from the git repo. +export async function fetchAndDecryptEntry( + git: GitHost, + masterKey: Uint8Array, + id: string, +): Promise { + const w = requireWasm(); + const ciphertext = await git.readFile(`entries/${id}.enc`); + const json = w.decrypt_entry(ciphertext, masterKey); + return JSON.parse(json) as Entry; +} + +/// Encrypt an entry and write it to the git repo. +export async function encryptAndWriteEntry( + git: GitHost, + masterKey: Uint8Array, + id: string, + entry: Entry, + message: string, +): Promise { + const w = requireWasm(); + const entryJson = JSON.stringify(entry); + const ciphertext = w.encrypt_entry(entryJson, masterKey); + await git.writeFile(`entries/${id}.enc`, ciphertext, message); +} + +/// Encrypt the manifest and write it to the git repo. +export async function encryptAndWriteManifest( + git: GitHost, + masterKey: Uint8Array, + manifest: Manifest, + message: string, +): Promise { + const w = requireWasm(); + const manifestJson = JSON.stringify(manifest); + const ciphertext = w.encrypt_manifest(manifestJson, masterKey); + await git.writeFile('manifest.enc', ciphertext, message); +} + +/// Filter manifest entries by group (case-insensitive). If no group given, returns all. +export function listEntries( + manifest: Manifest, + group?: string, +): Array<[string, ManifestEntry]> { + const entries = Object.entries(manifest.entries); + if (!group) return entries; + const g = group.toLowerCase(); + return entries.filter(([, e]) => + e.group?.toLowerCase() === g + ); +} + +/// Case-insensitive substring search on name, url, and username. +export function searchEntries( + manifest: Manifest, + query: string, +): Array<[string, ManifestEntry]> { + const q = query.toLowerCase(); + return Object.entries(manifest.entries).filter(([, e]) => { + if (e.name.toLowerCase().includes(q)) return true; + if (e.url?.toLowerCase().includes(q)) return true; + if (e.username?.toLowerCase().includes(q)) return true; + return false; + }); +} + +/// Find entries whose URL matches the given page URL by hostname. +export function findByUrl( + manifest: Manifest, + url: string, +): Array<[string, ManifestEntry]> { + let hostname: string; + try { + hostname = new URL(url).hostname; + } catch { + return []; + } + + return Object.entries(manifest.entries).filter(([, e]) => { + if (!e.url) return false; + try { + const entryHost = new URL(e.url).hostname; + return entryHost === hostname; + } catch { + return false; + } + }); +}