/ Documentation / CLI reference / luksbox header-dump

luksbox header-dump

Decrypt the metadata blob and emit a JSON tree of every inode and chunk reference.

luksbox header-dump <PATH> [--pretty] [--unlock-args]

Decrypts the metadata blob and emits a JSON tree of every inode, chunk reference, generation counter, and keyslot summary. Read-only (never writes). Strictly forensic: this exposes the metadata that the format normally only handles internally.

Requires unlock material (passphrase, FIDO2, TPM, or hybrid). The output goes to stdout; pipe it into jq for queries.

Examples

Pretty-printed full dump

luksbox header-dump my.lbx --pretty

Quick query: every chunk's slot offset for one file

luksbox header-dump my.lbx \
  | jq '.inodes[] | select(.path == "/payload") | .chunks[] | {chunk_idx, slot_offset, generation}'

Quick query: which keyslot kinds are populated

luksbox header-dump my.lbx | jq '.keyslots | map(select(.is_empty == false))'

Output shape

{
  "vault": "my.lbx",
  "header_storage": "my.lbx",
  "header": {
    "cipher": "Aes256GcmSiv",
    "kdf": "Argon2id",
    "chunk_size": 4096,
    "flags": 0,
    "metadata_offset": 8192,
    "metadata_size": 1048576,
    "data_offset": 1056768,
    "header_salt_prefix": "09ec28b5"
  },
  "keyslots": [
    { "index": 0, "kind": "Passphrase", "is_empty": false },
    { "index": 1, "kind": "Empty",      "is_empty": true  }
  ],
  "tree_counters": {
    "next_chunk_id": 3,
    "next_chunk_gen": 7,
    "next_file_id": 3,
    "free_chunk_count": 0
  },
  "inodes": [
    {
      "id": 1,
      "path": "/",
      "kind": "dir",
      "size_raw": 0,
      "children": [
        { "id": 2, "name": "payload" }
      ]
    },
    {
      "id": 2,
      "path": "/payload",
      "kind": "file",
      "size_raw": 12000,
      "chunks": [
        { "chunk_idx": 0, "chunk_id": 0, "generation": 4, "slot_offset": 1056768 },
        { "chunk_idx": 1, "chunk_id": 1, "generation": 5, "slot_offset": 1060892 },
        { "chunk_idx": 2, "chunk_id": 2, "generation": 6, "slot_offset": 1065016 }
      ]
    }
  ]
}

Field reference

Field Meaning
cipher AEAD primitive: Aes256GcmSiv, Aes256Gcm, or ChaCha20Poly1305
kdf KDF identifier (currently always Argon2id)
chunk_size Chunk plaintext size in bytes (always 4096 today)
flags Bitfield: bit 0 = FLAG_PAD_FILES_POW2, bit 1 = FLAG_HIDE_SIZE_HEADER
metadata_offset / metadata_size Offset and capacity of the AEAD-encrypted metadata region
data_offset Offset where the data region (4 KiB chunks) begins
header_salt_prefix First 4 bytes of the per-vault HKDF salt, hex. The full 32 bytes are not exposed: a public dump that included the salt would let an attacker reproduce subkey derivations if they ever recovered the MVK

keyslots

One row per slot (always 8 rows; empty ones too). kind is the SlotKind enum: Passphrase, Fido2HmacSecret, Fido2DerivedMvk, Tpm2Sealed, Tpm2Pin, Tpm2Fido2, HybridPq*, or Empty.

tree_counters

Field Meaning
next_chunk_id Next chunk_id to be allocated for a fresh write
next_chunk_gen Next chunk-generation counter (monotonic, used in chunk AAD for replay protection)
next_file_id Next file_id to be allocated
free_chunk_count Number of chunk_ids on the LIFO free-list (freed and reusable)

inodes

Every inode in the vault, sorted by id for stable diffs. path is the full vault-internal path. size_raw is the raw stored size: in FLAG_HIDE_SIZE_HEADER mode, this is the padded chunk capacity, not the real file length (the real length lives in chunk 0's first 8 bytes). For directories, children lists (name, id). For files, chunks lists every chunk reference with its computed slot_offset (so you can dd if=vault.lbx bs=1 skip=<offset> count=4124 to inspect the raw nonce + ciphertext + tag).

Security notes

See the forensics page for the full recovery flow this subcommand fits into.