From 0ee56898326be4239e5f53ae9319cc435036b46b Mon Sep 17 00:00:00 2001 From: MatMoul Date: Sun, 10 May 2026 01:27:39 +0200 Subject: [PATCH] feat: add secret derivation for KeePass key files --- src/kdbx/crypto.ts | 17 +++++++++++++++++ src/kdbx/header.ts | 3 ++- src/keepass.ts | 10 +++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/kdbx/crypto.ts b/src/kdbx/crypto.ts index d077ec7..74916a6 100644 --- a/src/kdbx/crypto.ts +++ b/src/kdbx/crypto.ts @@ -13,3 +13,20 @@ export function deriveKey(password: string, salt: Uint8Array, rounds: number, le return new Uint8Array(pbkdf2Sync(password, Buffer.from(salt), rounds, length, "sha256")); } + +export function normalizeKeyFileBytes(bytes: Uint8Array): Uint8Array { + return sha256(bytes); +} + +export function combineSecrets(password: string, keyFileBytes?: Uint8Array): Uint8Array { + const passwordHash = sha256(password); + if (!keyFileBytes) return passwordHash; + return sha256(Buffer.concat([Buffer.from(passwordHash), Buffer.from(normalizeKeyFileBytes(keyFileBytes))])); +} + +export function deriveMasterKey(secret: Uint8Array, salt: Uint8Array, rounds: number): Uint8Array { + if (secret.length === 0) { + throw new Error("Missing secret for key derivation"); + } + return new Uint8Array(pbkdf2Sync(Buffer.from(secret), Buffer.from(salt), rounds, 32, "sha256")); +} diff --git a/src/kdbx/header.ts b/src/kdbx/header.ts index 5df8955..8b15c9d 100644 --- a/src/kdbx/header.ts +++ b/src/kdbx/header.ts @@ -16,9 +16,10 @@ export function parseKdbxHeader(buffer: Uint8Array): { header: KdbxHeader; offse const versionMinor = reader.readUint16LE(); const versionMajor = reader.readUint16LE(); const version = { major: versionMajor, minor: versionMinor }; + const header: KdbxHeader = { version }; return { - header: { version }, + header, offset: buffer.length - reader.remaining(), }; } diff --git a/src/keepass.ts b/src/keepass.ts index 492a47c..92d1a59 100644 --- a/src/keepass.ts +++ b/src/keepass.ts @@ -7,7 +7,7 @@ import type { KeePassGroupInput, KeePassOpenOptions, } from "./types"; -import { cloneSnapshot, createEmptySnapshot, parseKdbxFile, type KdbxHeader } from "./kdbx"; +import { cloneSnapshot, combineSecrets, createEmptySnapshot, parseKdbxFile, type KdbxHeader } from "./kdbx"; const EMPTY = ""; @@ -16,6 +16,7 @@ export class KeePassDatabase { private dirty = false; private header: KdbxHeader | null = null; + private secret: Uint8Array | null = null; constructor( private path: string, @@ -85,10 +86,17 @@ export class KeePassDatabase { const buffer = new Uint8Array(await readFile(this.path)); const parsed = parseKdbxFile(buffer); this.header = parsed.header; + if (this.options.keyFile) { + const keyFile = new Uint8Array(await readFile(this.options.keyFile)); + this.secret = combineSecrets(this.options.password, keyFile); + } else { + this.secret = combineSecrets(this.options.password); + } this.snapshot = { header: parsed.header, groups: [{ name: "Racine", path: "" }], entries: [], + keyFiles: this.options.keyFile ? [this.options.keyFile] : [], }; return; } catch {