|
libkeepass 0.2.0
KeePass key database importer/exporter library
|
This document describes how libkeepass parses a KDBX database from bytes on disk to an in-memory Database object (and back). The entry points are KdbxFile::Import / KdbxFile::Export in src/kdbx_import.cc / src/kdbx_export.cc.
The legacy KDB 1.x format uses a different binary layout and is implemented in
src/kdb.cc; this document covers KDBX (KeePass 2).
KdbxFile is a thin orchestrator. The wire formats live in dedicated, stateless modules:
KdbxHeader (src/include/libkeepass/kdbx_header.hh, src/kdbx_header.cc) — file signature/version prefix, the per-version outer header field layout, cipher identifiers, compression and inner-random-stream enums, and the size caps used against malformed files.KdbxKdf (src/include/libkeepass/kdbx_kdf.hh, src/kdbx_kdf.cc) — the KDF dispatcher: (de)serializes the KDF parameters into/from the KDBX 4 variant dictionary and runs Key::Transform for the database's KDF (AES-KDF, Argon2d/id).KdbxXml (src/include/libkeepass/kdbx_xml.hh, src/kdbx_xml.cc) — the KDBX XML body parser/serializer (pugixml), including the binary/icon/group pools and the stored header hash.KdbxFile (src/kdbx_import.cc / src/kdbx_export.cc) — the crypto pipeline around them: key derivation, block/HMAC verification, (de)encryption, gzip and the KDBX 4 inner header.The version's critical mask 0xffff0000 selects the parser (KdbxFile::Import, src/kdbx_import.cc:72):
0x00030000 → Import3 (KDBX 3)0x00040000 → Import4 (KDBX 4.0)0x00040001 → Import4 (KDBX 4.1; same container, only the XML body schema differs, see "KDBX 4.1" below)FormatErrorAll header parsing lives in KdbxHeader (src/kdbx_header.cc); each field is read into its own (wiped) buffer before parsing so that a declared size never overruns into the payload ("Corrupt header field size in KDBX").
Fields are read as id(size) data (src/kdbx_header.cc:108). Recognized IDs:
| ID | Field | Content |
|---|---|---|
| 2 | Cipher ID | 16-byte UUID; only AES (31 c1 f2 e6 ...) supported in KDBX 3 |
| 3 | Compression flags | 0 = none, 1 = gzip |
| 4 | Master seed | byte vector |
| 5 | Transform seed | 32 bytes |
| 6 | Transform rounds | uint64 (capped at Database::kMaxTransformRounds) |
| 7 | Encryption IV | 16 bytes |
| 8 | Inner random stream key | 32 bytes |
| 9 | Content stream start bytes | 32 bytes |
| 10 | Inner random stream ID | must be 1 (Salsa20) |
| 0 | End of header | — |
Fields are read as id(size) data (src/kdbx_header.cc:134); each declared size is capped at 1 MiB (kMaxOuterHeaderFieldSize) to reject crafted files before any unbounded allocation runs.
| ID | Field | Content |
|---|---|---|
| 2 | Cipher ID | AES / Twofish / ChaCha20 UUID (see encryption.md) |
| 3 | Compression flags | as above |
| 4 | Master seed | byte vector |
| 7 | Encryption IV | 12 bytes (ChaCha20) or 16 bytes |
| 11 | KDF parameters | VariantDictionary (see key-derivation.md) |
| 0 | End of header | — |
Reset() shared state (delegates to KdbxXml::Reset, src/kdbx_xml.cc:38).KdbxHeader::ReadVersion, src/kdbx_header.cc:159).Import3 or Import4.Database (KdbxHeader::Parse3, src/kdbx_header.cc:172: cipher, compression, seeds, IV, inner stream key, start bytes).transformed_key = AES-KDF(transform_seed, rounds); final_key = SHA-256(master_seed || transformed_key) (see key-derivation.md).PasswordError.hashed_istreambuf, then optional gzip_istreambuf.KdbxXml::Parse.<Meta><HeaderHash>; this happens after the XML is parsed because the hash is stored inside the XML.VariantDictionary (KdbxHeader::Parse4, src/kdbx_header.cc:260; Argon2/AES parameters via KdbxKdf::ParseParameters, src/kdbx_kdf.cc:48).transformed_key = AES-KDF or Argon2; hmac_key and header_hmac_key per key-derivation.md.PasswordError).final_key = SHA-256(master_seed || transformed_key).hmac_istreambuf (HMAC framing is the outer layer, over the ciphertext) and decrypt it block by block so a large database is never fully materialized: decrypt_cbc_stream for AES/Twofish (src/cipher.cc), a chunked 64-byte keystream loop for ChaCha20.src/kdbx_import.cc:375):| ID | Field | Content |
|---|---|---|
| 1 | Inner random stream ID | 1 = Salsa20, 3 = ChaCha20 |
| 2 | Inner random stream key | 32 or 64 bytes |
| 3 | Binaries | one entry per attachment: flags u8 (0x01 = protected) then data |
| 0 | End | — |
Binaries are collected into the XML module's binary_pool_ and later added to the database meta so they survive round-trips.
Uses pugixml (parse_default | parse_trim_pcdata). Required structure:
ParseMeta (src/kdbx_xml_meta.cc:47) fills Metadata: generator, database name/description, memory protection flags, recycle bin, entry templates, history limits, custom icons and binaries, plus the stored HeaderHash.ParseGroup (src/kdbx_xml_group.cc:35) recursively builds the group tree and calls ParseEntry (src/kdbx_xml_entry.cc:92).group_pool_ (keyed by UUID string) so that cross-references such as RecycleBinUUID and the last-selected group resolve to shared objects.Entry objects including custom fields, attachments (resolved through binary_pool_) and history entries.LastSelectedGroup / LastTopVisibleGroup reference parsed groups, they are resolved only after the group tree is complete (src/kdbx_xml.cc:104).Fields marked Protected="True" are stored Base64-encoded and XORed with the inner random stream. ParseProtectedString decodes and deobfuscates them into protect<secure_string>; WriteProtectedString (src/kdbx_xml_entry.cc:82) does the reverse on export. The obfuscator is a RandomObfuscator (src/random.cc) over Salsa20 (KDBX 3) or Salsa20/ChaCha20 (KDBX 4).
KDBX 4.1 (0x00040001) is read and written via the same Import4/Export4 pipeline as KDBX 4.0; header, HMAC, cipher and inner stream are unchanged. Only the XML body schema is extended (src/kdbx_xml.cc):
<Group><Tags> — semicolon-joined group tags on the wire (KeePass 2.48+); the API surface is space-separated, the conversion happens at the XML boundary in TagsFromXml/TagsToXml (src/kdbx_xml_internal.hh:65, :82).<Entry><QualityCheck> — "false" disables the password quality warning (Entry::quality_check; parsed src/kdbx_xml_entry.cc:111, written :269).<Entry>/<Group><PreviousParentGroup> — UUID of the previous parent group (Entry::previous_parent_group/Group::previous_parent_group; parsed src/kdbx_xml_entry.cc:114/src/kdbx_xml_group.cc:67, written src/kdbx_xml_entry.cc:273/src/kdbx_xml_group.cc:131).<Icon><Name>/<LastModificationTime> — custom icon metadata (Icon::name/Icon::last_modification_time; src/kdbx_xml_meta.cc:114, :295).<CustomData><Item><LastModificationTime> — per-item modification time (Metadata::Field::last_modification_time; src/kdbx_xml_meta.cc:194, :359).<Root><DeletedObjects><DeletedObject> (UUID + DeletionTime) — deletion tombstones for groups, entries and (since 4.1) icons (Metadata::DeletedObject; src/kdbx_xml.cc:95, :156).Export4 writes 0x00040001 only when the database uses 4.1-only features, otherwise 0x00040000 (mirrors KeePass' GetMinKdbxVersion, verified against KeePass 2.57; the decision is RequiresKdbx41 in src/kdbx_export.cc:82). Two rules differ from a naive reading of the format:
PreviousParentGroup reference does not enforce KDBX 4.1. KeePass keeps the file at 0x00040000 for a database whose only 4.1 feature is such a reference (KeePass' migration rule) and then omits the element from the 4.0 output. libkeepass mirrors this: the reference is parsed whenever the element is present, but never bumps the version.<CustomData> item enforces KDBX 4.1, because every item carries a LastModificationTime. Entry-level <Tags> do not enforce KDBX 4.1 (they are part of the 4.0 schema).Writes KDBX 4 when forced via set_write_kdbx4(true) or when the database KDF is not AES; otherwise writes KDBX 3.
transformed_key and final_key.KdbxHeader::Write3 (src/kdbx_header.cc:328) into an in-memory stream, compute the SHA-256 header hash, write the header to the file.start_bytes(32) || hashed_ostreambuf( XML ). XML is optionally gzip-compressed inside the hashed stream.encrypt_cbc the content stream.transformed_key; compute final_key, hmac_key and the header HMAC.KdbxHeader::Write4 (src/kdbx_header.cc:387, incl. the KDF VariantDictionary), compute and write header SHA-256 + HMAC. The version field is 0x00040001 iff KdbxXml::kdbx41() was set.binary_pool_ (deduplicated, in a stable order).encrypt_cbc or ChaCha20) the payload, then wrap the ciphertext in hmac_ostreambuf.KdbxFile is stateful across a single import/export; it delegates the bookkeeping objects to KdbxXml (KdbxXml::Reset, src/kdbx_xml.cc:38):
binary_pool_, icon_pool_, group_pool_ — UUID-keyed caches used to preserve object identity across <Meta> fields, the group tree and the KDBX 4 inner header (owned by KdbxXml).header_hash_ — the last computed/stored header hash (written back into <Meta><HeaderHash> on export; owned by KdbxXml).kdbx4_ / kdbx41_ — the active format and whether the KDBX 4.1 XML schema should be written (owned by KdbxXml).write_kdbx4_ — the forced-format flag on KdbxFile for export.The pools are intentionally not part of the public Database API; they are bookkeeping that makes parsing and re-serialization faithful.