Compare commits
2 Commits
fa7df95d32
..
nopy
| Author | SHA1 | Date | |
|---|---|---|---|
| c0564c1ea2 | |||
| 0ee5689832 |
+3
-2
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user