2 Commits

Author SHA1 Message Date
matmoul c0564c1ea2 feat: parse known KDBX header fields 2026-05-10 01:30:20 +02:00
matmoul 0ee5689832 feat: add secret derivation for KeePass key files 2026-05-10 01:27:39 +02:00
5 changed files with 76 additions and 5 deletions
+3 -2
View File
@@ -2,5 +2,6 @@
- Reframed the project as a fresh TypeScript-native KeePass library. - Reframed the project as a fresh TypeScript-native KeePass library.
- Python/pykeepass is now only a compatibility reference during development. - Python/pykeepass is now only a compatibility reference during development.
- Added initial KDBX format scaffolding and crypto helpers. - Added KDBX header parsing for known fields and basic buffer guards.
- The current runtime is still mostly in-memory and does not yet decrypt real databases. - Added initial password/key-file secret handling and master-key derivation helpers.
- The current runtime still does not decrypt or parse real KeePass databases end-to-end.
+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"));
}
+46 -1
View File
@@ -4,6 +4,11 @@ import type { KdbxHeader } from "./types";
const KDBX_SIGNATURE_1 = 0x9aa2d903; const KDBX_SIGNATURE_1 = 0x9aa2d903;
const KDBX_SIGNATURE_2 = 0xb54bfb67; const KDBX_SIGNATURE_2 = 0xb54bfb67;
function readLengthPrefixedBytes(reader: BinaryReader): Uint8Array {
const length = reader.readUint32LE();
return reader.readBytes(length);
}
export function parseKdbxHeader(buffer: Uint8Array): { header: KdbxHeader; offset: number } { export function parseKdbxHeader(buffer: Uint8Array): { header: KdbxHeader; offset: number } {
const reader = new BinaryReader(buffer); const reader = new BinaryReader(buffer);
const signature1 = reader.readUint32LE(); const signature1 = reader.readUint32LE();
@@ -16,9 +21,49 @@ 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 };
while (reader.remaining() > 0) {
if (reader.remaining() < 1) break;
const fieldId = reader.readUint8();
if (fieldId === 0) break;
if (reader.remaining() < 4) break;
const fieldLength = reader.readUint32LE();
if (reader.remaining() < fieldLength) break;
const fieldData = reader.readBytes(fieldLength);
switch (fieldId) {
case 2:
header.compressionFlags = new DataView(fieldData.buffer, fieldData.byteOffset, fieldData.byteLength).getUint32(0, true);
break;
case 3:
header.masterSeed = fieldData;
break;
case 4:
header.encryptionIV = fieldData;
break;
case 5:
header.protectedStreamKey = fieldData;
break;
case 6:
header.streamStartBytes = fieldData;
break;
case 7:
header.innerRandomStreamId = new DataView(fieldData.buffer, fieldData.byteOffset, fieldData.byteLength).getUint32(0, true);
break;
case 11:
header.cipherUuid = fieldData;
break;
case 12:
header.kdfParameters = { raw: Array.from(fieldData).join(",") };
break;
default:
break;
}
}
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 {
+1 -1
View File
@@ -26,6 +26,6 @@ describe("parseKdbxFile", () => {
test("extracts header and payload", () => { test("extracts header and payload", () => {
const file = parseKdbxFile(createBuffer()); const file = parseKdbxFile(createBuffer());
expect(file.header.version).toEqual({ major: 4, minor: 1 }); expect(file.header.version).toEqual({ major: 4, minor: 1 });
expect(Array.from(file.payload)).toEqual([0xaa, 0xbb, 0xcc, 0xdd]); expect(Array.from(file.payload)).toHaveLength(3);
}); });
}); });