|
libkeepass 0.2.0
KeePass key database importer/exporter library
|
Status: decided — see Streaming decisions below.
This page documents why libkeepass parses KDBX databases the way it does, in particular where it streams and where it deliberately buffers, and how load performance is tracked.
See index.md for the overall architecture and encryption.md for the crypto layers.
A KeePass database can be large: thousands of entries, attachments and a long metadata block push the payload well beyond a megabyte. The import pipeline is
Each layer can in principle materialize its whole input. Before the streaming work in P2, the KDBX 4 importer copied the entire ciphertext into a std::string (ciphertext) before decrypting, so peak memory was roughly 3 × payload size (ciphertext copy + decrypted plaintext copy + DOM).
hmac_istreambuf (src/stream.cc) already reads blocks on demand. The importer now consumes that stream directly.decrypt_cbc_stream (src/cipher.cc) replaces the whole-string decrypt_cbc in the KDBX 4 CBC path. It reads the non-seekable hmac_istreambuf in 64-byte chunks, decrypts each complete 16-byte CBC block as it arrives and hands it to the output stream. Only the final block is withheld until EOF so that its PKCS #7 padding can be validated and stripped. ChaCha20 decryption (kdbx_import.cc, Import4) is chunked the same way: 64 bytes at a time, one keystream block per chunk.Open(path): the file is mapped read-only (detail::MappedFile, mmap/CreateFileMapping) and imported through a seekable mapped_istreambuf, so signature sniffing and the header rewind do not copy the file. Stream callers keep the existing behaviour.std::stringstream/std::string copy is made.Binary a detail::BufferBinarySource range. Binary::data() materializes only the requested payload; Binary::WriteTo() streams it during export.The recorded exceptions behave exactly as before: a failed per-block HMAC or a truncated block throws IoError (corruption, not a password problem); nothing is buffered long enough to mask the location of the failure.
The natural next step after streaming decryption would be to parse the XML incrementally (SAX) instead of loading the whole document. This was evaluated and rejected:
load_buffer / append_buffer (incremental feeding of chunks) but still builds a complete in-memory tree; there is no SAX-style event API. Switching to Expat/SAX would add a dependency and a second parsing path.Database object graph (groups, entries, history, custom icons, binaries). A DOM tree is already a compact stepping stone to that graph; an event parser would build the same graph but with hand-rolled state.gzip_istreambuf. The memory that actually scales badly was the pre-P2 ciphertext copy, which is now gone.test/robustness.cc), which bounds the DOM size for hostile input.The remaining full-buffer points are deliberate: the decrypted content (std::stringstream) and the pugixml DOM. Their combined size is comparable to the legacy buffer, but the streaming change removes the largest wasteful copy and decouples memory from the HMAC block size.
test/benchmark.cc is a standalone (non-GTest) executable that generates a deterministic database (N = 2000 entries by default) and measures full import time for each format/cipher/compression combination:
| Scenario | Cipher | Compression | Format |
|---|---|---|---|
kdbx3-aes-gzip | AES | gzip | KDBX 3 |
kdbx4-aes-gzip | AES | gzip | KDBX 4 |
kdbx4-aes-plain | AES | none | KDBX 4 |
kdbx4-chacha20-gzip | ChaCha20 | gzip | KDBX 4 |
It prints the on-disk size, wall-clock export and import times, the corresponding MiB/s figures and the process peak RSS. --json emits the same data machine-readably. tools/benchmark_compare.py compares a run against test/benchmark_baseline.json: absolute export/import times use a generous threshold on shared CI runners, while the export/import ratio catches directional regressions independently of the machine. Run it locally with ./bin/benchmark [<entries>] [--json].
Reproducibility notes: seeds and content are fixed (no random input), and the AES-KDF round count is a constant 8192 so the measured time is dominated by the content pipeline rather than by the key derivation.