|
libkeepass 0.2.0
KeePass key database importer/exporter library
|
This document explains how libkeepass turns a user password / key file into the cryptographic keys that encrypt and authenticate a KDBX database. The implementation lives in src/key.cc (with an SIMD fast path in src/aes_ni.cc).
keepass::Key (src/include/libkeepass/key.hh) holds two 32-byte sub keys:
| Component | Set via | Derivation |
|---|---|---|
password_key_ | SetPassword() | SHA-256(password) |
keyfile_key_ | SetKeyFile() | see below |
Both components may be present at the same time (a composite key).
Two formats are accepted:
<KeyFile><Key><Data>...</Data></Key> is Base64-decoded and must be exactly 32 bytes.Anything else raises FormatError: Unknown key file format.
SubKeyResolution selects how the sub keys are combined before the KDF runs:
kHashSubKeys — the present sub keys are always hashed together: SHA-256(password_key || keyfile_key) (skipping empty ones).kHashSubKeysOnlyIfCompositeKey — a single sub key is used as-is; both sub keys are combined with SHA-256 only when the key is truly composite.Import uses kHashSubKeys for both KDBX 3 and KDBX 4 (src/kdbx_import.cc:115, :222).
Key::Transform(seed, rounds, resolution) (src/key.cc:107):
The 32-byte plaintext is encrypted with AES-256-ECB where seed (the header's transform seed) is the key. Each iteration feeds back into the next. The final iteration is hashed with SHA-256.
Two implementations of the core loop exist:
src/key.cc:119): one EVP_CIPHER_CTX is reused across all rounds; padding is disabled.src/aes_ni.cc:154): when the binary is built with LIBKEEPASS_AES_NI and the CPU advertises AES-NI, aes_ni_supported() is true and aes_ni_transform_aes_kdf runs the identical algorithm using the hardware AES instructions (_mm_aesenc_si128). The result is byte-for-byte equal to the OpenSSL path.Key::TransformArgon2(kdf, salt, iterations, memory_bytes, parallelism, version, resolution) (src/key.cc:150):
type is Argon2_d or Argon2_id, selected by the Kdf argument.memory_bytes is converted to KiB before calling argon2_hash.version accepts 0x10 (Argon2 1.0) or 0x13 (Argon2 1.3).The parameters come from the KDBX 4 header's KDF variant dictionary (VariantDictionary in src/variantdictionary.cc). Its $UUID entry selects the KDF and the remaining entries carry the parameters:
| Key | Type | AES-KDF | Argon2d / Argon2id |
|---|---|---|---|
$UUID | ByteArray | 7c 02 bb 82 79 a7 4a c0 92 7d 11 4a 00 64 82 38 (AES, KDBX4) | ef 63 6d df 8c 29 44 4b 91 f7 a9 a4 03 e3 0a 0c (d) / 9e 29 8b 19 56 db 47 73 b2 3d fc 3e c6 f0 a1 e6 (id) |
S | ByteArray | transform seed | salt |
R | UInt64 | rounds | — |
I | UInt64 | — | iterations |
M | UInt64 | — | memory in bytes |
P | UInt32 | — | parallelism |
V | UInt32 | — | Argon2 version |
Parsed in KdbxKdf::ParseParameters (src/kdbx_kdf.cc:48) and written back in KdbxKdf::WriteParameters (src/kdbx_kdf.cc:91).
The roadmap tracked a hypothesis that KeePass uses a BLAKE2-based Argon2 variant. The evaluation against the KeePass documentation and source (KDBX 4 format, KDBX file format specification) and against KeePassXC's KeePass2.cpp concludes:
c9 d9 f3 9a … aliasing the KDBX 4 UUID 7c 02 bb 82 …) plus the Argon2d / Argon2id OIDs. No fourth "Argon2-with-BLAKE2b" OID exists, so nothing needs to be added to the KdbxKdf dispatcher.argon2 library, which implements BLAKE2b internally, and passes the Argon2d and Argon2id imports of every fixture (including the KeePass-2.57-generated KDBX 4.1 files) as well as version 0x10/0x13 handling.K (secret key) and A (associated data) are supported by the format but never written by KeePass itself; unknown variant dictionary entries are ignored on import (tolerance policy, see TODO "Entscheidungen").Starting from transformed_key, two keys are computed:
Used to instantiate the payload cipher (src/kdbx_import.cc:130 for KDBX 3, :278 for KDBX 4).
hmac_key authenticates the HMAC blocks of the payload (src/kdbx_import.cc:292).header_hmac_key authenticates the header itself; the stored 32-byte header HMAC is HMAC-SHA256(header_hmac_key, header_bytes) (src/kdbx_import.cc:249).src/stream.cc as SHA-512(block_index || hmac_key).Database::transformed_key() / has_transformed_key() cache the derived key (src/kdbx_import.cc:116, :223). Export3 / Export4 reuse the cached value when present, so re-exporting an imported database does not rerun an expensive KDF unless the KDF parameters changed (src/kdbx_export.cc:127, :211).