feat: add vault operations module
Bridges WASM crypto with git host API for encrypt/decrypt of entries and manifest, plus search, group filtering, and URL-based lookup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
extension/src/service-worker/vault.ts
Normal file
135
extension/src/service-worker/vault.ts
Normal file
@@ -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<typeof wasm> {
|
||||
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<VaultMeta> {
|
||||
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<Manifest> {
|
||||
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<Entry> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user