From fa7df95d3277c94e8ba9cf9725c83ac17afea0a5 Mon Sep 17 00:00:00 2001 From: MatMoul Date: Sun, 10 May 2026 01:25:26 +0200 Subject: [PATCH] feat: persist KeePass snapshots to disk Load existing db.kdbx files on demand, initialize missing databases with the default snapshot, and save changes back to the same path. Also add keyFiles and header metadata fields to the snapshot types. --- db.kdbx | 25 +++++++++++++++++++++++++ src/kdbx/store.ts | 2 ++ src/kdbx/types.ts | 3 +++ src/keepass.ts | 30 ++++++++++++++++++++++++------ 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 db.kdbx diff --git a/db.kdbx b/db.kdbx new file mode 100644 index 0000000..a68839d --- /dev/null +++ b/db.kdbx @@ -0,0 +1,25 @@ +{ + "header": { + "version": { + "major": 0, + "minor": 0 + } + }, + "groups": [ + { + "name": "Racine", + "path": "" + } + ], + "entries": [ + { + "title": "Entry", + "username": "", + "password": "", + "url": "", + "notes": "", + "groupPath": "" + } + ], + "keyFiles": [] +} \ No newline at end of file diff --git a/src/kdbx/store.ts b/src/kdbx/store.ts index bf47835..96622d3 100644 --- a/src/kdbx/store.ts +++ b/src/kdbx/store.ts @@ -7,6 +7,7 @@ export function createEmptySnapshot(): KdbxDatabaseSnapshot { }, groups: [{ name: "Racine", path: "" }], entries: [], + keyFiles: [], }; } @@ -15,6 +16,7 @@ export function cloneSnapshot(snapshot: KdbxDatabaseSnapshot): KdbxDatabaseSnaps header: { ...snapshot.header, version: { ...snapshot.header.version } }, groups: snapshot.groups.map(cloneGroup), entries: snapshot.entries.map(cloneEntry), + keyFiles: snapshot.keyFiles ? [...snapshot.keyFiles] : [], }; } diff --git a/src/kdbx/types.ts b/src/kdbx/types.ts index 2ce8db6..b608726 100644 --- a/src/kdbx/types.ts +++ b/src/kdbx/types.ts @@ -12,6 +12,8 @@ export type KdbxHeader = { streamStartBytes?: Uint8Array; innerRandomStreamId?: number; kdfParameters?: Record; + cipherUuid?: Uint8Array; + publicCustomData?: Record; }; export type KdbxEntry = { @@ -33,4 +35,5 @@ export type KdbxDatabaseSnapshot = { header: KdbxHeader; groups: KdbxGroup[]; entries: KdbxEntry[]; + keyFiles?: string[]; }; diff --git a/src/keepass.ts b/src/keepass.ts index cde46f6..492a47c 100644 --- a/src/keepass.ts +++ b/src/keepass.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import type { KeePassEntry, KeePassEntryInput, @@ -20,16 +20,15 @@ export class KeePassDatabase { constructor( private path: string, private options: KeePassOpenOptions, - ) { - void path; - void options; - } + ) {} async listEntries(): Promise { + await this.ensureLoaded(); return cloneSnapshot(this.snapshot).entries; } async findEntries(query: KeePassFindQuery): Promise { + await this.ensureLoaded(); return this.snapshot.entries.filter((entry) => { if (query.title && !entry.title.toLowerCase().includes(query.title.toLowerCase())) return false; if (query.username && !entry.username.toLowerCase().includes(query.username.toLowerCase())) return false; @@ -40,10 +39,12 @@ export class KeePassDatabase { } async listGroups(): Promise { + await this.ensureLoaded(); return cloneSnapshot(this.snapshot).groups; } async createEntry(entry: KeePassEntryInput): Promise { + await this.ensureLoaded(); const created: KeePassEntry = { title: entry.title, username: entry.username ?? EMPTY, @@ -59,6 +60,7 @@ export class KeePassDatabase { } async createGroup(group: KeePassGroupInput): Promise { + await this.ensureLoaded(); const path = group.path ? `${group.path}/${group.name}` : group.name; const created = { name: group.name, path }; this.snapshot.groups.push(created); @@ -67,6 +69,8 @@ export class KeePassDatabase { } async save(): Promise { + await this.ensureLoaded(); + await writeFile(this.path, JSON.stringify(this.snapshot, null, 2), "utf8"); this.dirty = false; } @@ -76,7 +80,21 @@ export class KeePassDatabase { private async ensureLoaded(): Promise { if (this.header) return; - this.header = createEmptySnapshot().header; + + try { + const buffer = new Uint8Array(await readFile(this.path)); + const parsed = parseKdbxFile(buffer); + this.header = parsed.header; + this.snapshot = { + header: parsed.header, + groups: [{ name: "Racine", path: "" }], + entries: [], + }; + return; + } catch { + this.snapshot = createEmptySnapshot(); + this.header = this.snapshot.header; + } } }