passchmop/src/background/fs.ts
schmop c965d0f4ad feat: encrypted git-backed vault with idle auto-lock
- Master password derives an AES-GCM-256 key via PBKDF2-SHA256; the key never leaves the device and the remote only sees ciphertext blobs.
- One file per entry under entries/; conflicts produce a sidecar the user resolves in the UI.
- Vault stays unlocked across background-script suspensions via storage.session; auto-locks after 5 min of inactivity.
2026-04-23 01:26:32 +02:00

85 lines
2.5 KiB
TypeScript

// Filesystem wrapper around lightning-fs (IndexedDB-backed). isomorphic-git
// needs the instance itself; our code uses the Promises-style helpers.
import LightningFS from '@isomorphic-git/lightning-fs';
const inst = new LightningFS('passchmop-repo');
export const fs = inst;
export const pfs = inst.promises;
export const dir = '/repo';
export const entriesDir = `${dir}/entries`;
interface FsError extends Error { code?: string }
export async function ensureDir(path: string): Promise<void> {
try {
await pfs.mkdir(path);
} catch (e) {
if ((e as FsError).code !== 'EEXIST') throw e;
}
}
export async function listEntryFiles(): Promise<string[]> {
try {
const names = await pfs.readdir(entriesDir);
return names.filter(n => n.endsWith('.enc'));
} catch (e) {
if ((e as FsError).code === 'ENOENT') return [];
throw e;
}
}
export async function readEntryFile(filename: string): Promise<Uint8Array> {
const buf = await pfs.readFile(`${entriesDir}/${filename}`);
if (typeof buf === 'string') throw new Error('unexpected string read for entry file');
return new Uint8Array(buf);
}
export async function writeEntryFile(filename: string, bytes: Uint8Array): Promise<void> {
await ensureDir(entriesDir);
await pfs.writeFile(`${entriesDir}/${filename}`, bytes);
}
export async function removeEntryFile(filename: string): Promise<void> {
try {
await pfs.unlink(`${entriesDir}/${filename}`);
} catch (e) {
if ((e as FsError).code !== 'ENOENT') throw e;
}
}
export async function readJson<T = unknown>(path: string): Promise<T | null> {
try {
const buf = await pfs.readFile(`${dir}/${path}`, { encoding: 'utf8' });
const text = typeof buf === 'string' ? buf : new TextDecoder().decode(buf);
return JSON.parse(text) as T;
} catch (e) {
if ((e as FsError).code === 'ENOENT') return null;
throw e;
}
}
export async function writeJson(path: string, obj: unknown): Promise<void> {
await ensureDir(dir);
await pfs.writeFile(`${dir}/${path}`, JSON.stringify(obj, null, 2), 'utf8');
}
export async function wipeRepo(): Promise<void> {
async function rm(path: string): Promise<void> {
try {
const stat = await pfs.stat(path);
if (stat.isDirectory()) {
const kids = await pfs.readdir(path);
for (const k of kids) await rm(`${path}/${k}`);
await pfs.rmdir(path);
} else {
await pfs.unlink(path);
}
} catch (e) {
if ((e as FsError).code !== 'ENOENT') throw e;
}
}
await rm(dir);
}