feat: add secret derivation for KeePass key files

This commit is contained in:
2026-05-10 01:27:39 +02:00
parent fa7df95d32
commit 0ee5689832
3 changed files with 28 additions and 2 deletions
+17
View File
@@ -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")); 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"));
}
+2 -1
View File
@@ -16,9 +16,10 @@ export function parseKdbxHeader(buffer: Uint8Array): { header: KdbxHeader; offse
const versionMinor = reader.readUint16LE(); const versionMinor = reader.readUint16LE();
const versionMajor = reader.readUint16LE(); const versionMajor = reader.readUint16LE();
const version = { major: versionMajor, minor: versionMinor }; const version = { major: versionMajor, minor: versionMinor };
const header: KdbxHeader = { version };
return { return {
header: { version }, header,
offset: buffer.length - reader.remaining(), offset: buffer.length - reader.remaining(),
}; };
} }
+9 -1
View File
@@ -7,7 +7,7 @@ import type {
KeePassGroupInput, KeePassGroupInput,
KeePassOpenOptions, KeePassOpenOptions,
} from "./types"; } from "./types";
import { cloneSnapshot, createEmptySnapshot, parseKdbxFile, type KdbxHeader } from "./kdbx"; import { cloneSnapshot, combineSecrets, createEmptySnapshot, parseKdbxFile, type KdbxHeader } from "./kdbx";
const EMPTY = ""; const EMPTY = "";
@@ -16,6 +16,7 @@ export class KeePassDatabase {
private dirty = false; private dirty = false;
private header: KdbxHeader | null = null; private header: KdbxHeader | null = null;
private secret: Uint8Array | null = null;
constructor( constructor(
private path: string, private path: string,
@@ -85,10 +86,17 @@ export class KeePassDatabase {
const buffer = new Uint8Array(await readFile(this.path)); const buffer = new Uint8Array(await readFile(this.path));
const parsed = parseKdbxFile(buffer); const parsed = parseKdbxFile(buffer);
this.header = parsed.header; 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 = { this.snapshot = {
header: parsed.header, header: parsed.header,
groups: [{ name: "Racine", path: "" }], groups: [{ name: "Racine", path: "" }],
entries: [], entries: [],
keyFiles: this.options.keyFile ? [this.options.keyFile] : [],
}; };
return; return;
} catch { } catch {