Forensics and partial recovery
Investigate a damaged vault, salvage what is still readable, and rebuild a corrupted header from a backup.
This page covers the five forensic / partial-recovery subcommands.
They exist for the case the recovery page does not
cover: you still have the unlock material (passphrase, FIDO2, TPM),
but the on-disk bytes themselves got damaged. A single bit flip in a
chunk's ciphertext is enough to fail AEAD verification and make
luksbox get or luksbox mount refuse to read the affected file.
Without dedicated tooling, the user is left with dd and a
hex editor. With it, the workflow is :
| Question | Command |
|---|---|
| Is the vault even openable today ? | info (header parse) then ls |
| What does the metadata tree actually look like, decrypted ? | header-dump |
| Which chunks are still AEAD-valid and which ones are not ? | check |
| Can I recover this one file with the bad chunks zero-filled ? | extract --tolerate-errors |
| The header itself is corrupted; can I roll back to a backup ? | header-restore |
| Should I keep a header backup as a routine pre-rotation step ? | header-backup |
All five subcommands are read-only with the single exception of
header-restore, which rewrites the 8 KiB header bytes (in place
for inline mode, atomic temp+rename for detached mode).
1. The mental model
A LUKSbox vault is a single .lbx file (plus optional .hdr,
.anchor, .hybrid, .kyber sidecars). On disk, that one file is
laid out as :
flowchart LR
A["bytes 0..8192
HEADER
(8 KiB, HMAC at the tail)"] --> B
B["bytes 8192..1056768
METADATA REGION
AEAD-encrypted file tree"] --> C
C["bytes 1056768..EOF
DATA REGION
4 KiB chunks, each AEAD-sealed independently"]
Three classes of damage map to three classes of recovery :
| Damage | Symptom | Recovery |
|---|---|---|
| Bytes 0..8192 (header) flipped | luksbox info may still parse, but unlock fails with header authentication failed. |
header-restore from a previously-saved header-backup |
| One AEAD chunk flipped (anywhere in the data region) | mount works, ls works, but reading the affected file fails with AEAD failure. Other files in the same vault are untouched. |
check to locate, extract --tolerate-errors to salvage the rest of that file |
| Metadata region flipped | The vault opens through the keyslot, but the encrypted file-tree blob fails AEAD or structural validation, so the whole tree is gone. | No automatic recovery yet. The chunks are still on disk and AEAD-valid, but you have no map. This is the gap LUKSbox does not yet close (LUKS2 has a redundant header for the same reason). |
2. The forensic flow
The order in which you run the subcommands matters when you are investigating a damaged vault you have never opened before :
flowchart TD
Start([damaged vault]) --> Info["luksbox info vault.lbx
(does the header even parse ?)"]
Info -->|no, magic missing| Restore["luksbox header-restore
--no-verify"]
Info -->|yes| Open{can you unlock ?}
Open -->|no, header HMAC bad| Restore
Open -->|yes| Dump["luksbox header-dump --pretty
(decrypted tree, chunk refs, generations)"]
Dump --> Check["luksbox check vault.lbx --json
(per-chunk AEAD status)"]
Check -->|all OK| Done([no chunk damage])
Check -->|N chunks bad| Extract["luksbox extract --tolerate-errors
per affected file"]
Restore --> Open
Extract --> Done
3. The five subcommands at a glance
header-backup
Reads the 8 KiB header bytes (offsets, keyslots, salts, HMAC) and
writes them to a separate file. No unlock material required:
this is just a dd of the first 8 KiB, packaged as a CLI with the
right permissions (mode 0600 on Unix) and with a structural sanity
parse before saving.
luksbox header-backup vault.lbx /media/usb/vault.hdrbak
# wrote 8192 bytes from vault.lbx to /media/usb/vault.hdrbak
# cipher: Aes256GcmSiv metadata at offset 8192 (1048576 B) data at offset 1056768
# populated keyslots: 1 / 8
Run it after every luksbox enroll or luksbox rotate-mvk. Keep
the backup on separate trusted storage: anyone who has both the
.lbx and a backup of its header has the same offline brute-force
surface as anyone with the original vault.
For detached-header vaults, point the backup at the sidecar :
luksbox header-backup vault.lbx vault.hdrbak --header /media/usb/vault.hdr
See the header-backup page for the
full flag reference.
header-restore
Replaces the on-disk header bytes with the contents of a previously
saved backup. By default it requires unlock material so it can
HMAC-verify the new header against the current MVK before
writing. That check matters: without it, an attacker who can replace
your backup file could install a header that authenticates under
their MVK on the next restore. Skip it only with --no-verify
when the on-disk header is itself too damaged to unlock with.
# default: HMAC-verify the backup against the live MVK before writing
luksbox header-restore vault.lbx vault.hdrbak
# only when the on-disk header is too damaged to unlock
luksbox header-restore vault.lbx vault.hdrbak --no-verify
In inline mode this rewrites bytes 0..8192 of the vault file in
place (then fsync); in detached mode (--header /path/to.hdr) it
atomically replaces the sidecar via temp + rename + parent-dir
fsync.
See the header-restore page for the
full flag reference and a step-by-step damaged-header walkthrough.
header-dump
Decrypts the metadata blob and emits a JSON tree of every inode, chunk reference, generation counter, and keyslot summary. Read-only. Requires unlock material, so this is post-unlock forensics: you already have the MVK and want to inspect what the format normally hides.
luksbox header-dump vault.lbx --pretty | jq
The output includes :
- header: cipher, KDF, flags, header salt prefix, region offsets
- keyslots: one row per slot with kind and emptiness
- tree_counters:
next_chunk_id,next_chunk_gen,next_file_id, free-list size - inodes: every inode in the vault (file or dir) with full path,
raw stored size, and (for files) every chunk reference with
chunk_idx,chunk_id,generation, and computedslot_offsetso you canddthe bytes by hand if you ever need to.
This is the command you pipe into jq when an audit asks "what
does the encrypted file tree actually look like decrypted?".
See the header-dump page for output
shape and example queries.
check
Walks every used chunk in the vault, AEAD-decrypts it, and reports
per-chunk status (ok or aead_fail). Surfaces the exact
(file_path, chunk_idx, on-disk slot_offset, generation) of every
chunk the runtime would refuse to decrypt at mount time. Read-only.
Exit code is non-zero if any chunk fails.
luksbox check vault.lbx
# BAD /payload chunk_idx=1 chunk_id=1 gen=5 off=1060892 : crypto: AEAD failure
#
# checked 1 file(s), 3 chunk(s): 2 OK, 1 BAD
Or, for tooling consumption :
luksbox check vault.lbx --json | jq '.failures[] | {path, chunk_idx, slot_offset}'
check does not modify the vault. It is safe to run on a vault you
suspect has been tampered with: at worst it tells you which chunks
will fail at mount time.
See the check page for output shape, exit
codes, and the JSON schema.
extract --tolerate-errors
Pulls a single file out of the vault like luksbox get, but
tolerates per-chunk AEAD failures by writing 4 KiB of zeros in
place of each unrecoverable chunk and continuing. Prints the
chunk_idx and on-disk offset of every failure to stderr (and to
--report <path> as JSON, if set).
# Fails on a vault with one bad chunk: standard get is strict
luksbox get vault.lbx /payload broken.bin
# error: crypto: AEAD failure
# Forensic recovery: lossy mode, must be acknowledged with --tolerate-errors
luksbox extract vault.lbx /payload recovered.bin --tolerate-errors --report fail.json
# chunk_idx=1 chunk_id=1 gen=5 off=1060892 FAILED (crypto: AEAD failure); writing 4096 zero bytes
# wrote 12000 bytes to recovered.bin (2 chunks OK, 1 chunks zero-filled)
# warning: 1 chunk(s) were unrecoverable. The output file has 4096-byte zero ranges at the corresponding offsets.
The --tolerate-errors flag is mandatory to opt into the lossy
path: without it, extract refuses to run. This is deliberate. A
user who runs the command on autopilot should never silently get a
zero-padded file back thinking it is the real one.
extract does not modify the vault. The only side-effect is the
output file (and the optional report file).
See the extract page for the full flag
reference and post-recovery validation tips.
4. What this tooling does NOT do
It does not invent ciphertext. The recovery story is bounded by what is still authenticated on disk :
- Chunks that fail AEAD are gone.
extract --tolerate-errorspads them with zeros, but it cannot reconstruct the lost plaintext. - A header whose HMAC bytes match the rest of the header is the
only header LUKSbox will trust. If you do not have a backup
somewhere else (USB key, off-site backup, paper printout of the
hex),
header-restorehas nothing to restore from. - The metadata blob has no built-in redundancy yet. If it fails
AEAD, the file tree is unrecoverable from the
.lbxalone, even though every chunk's bytes are still on disk.
The honest summary: LUKSbox now ships an investigation toolkit that
is on par with what cryptsetup gives you for a damaged LUKS2
volume (luksHeaderBackup, luksDump, repair). It does not
yet provide redundant on-disk metadata; that is on the roadmap and
documented as a known limitation in the
security architecture page.
5. Operational habits that prevent the worst cases
- Run
luksbox header-backupimmediately after everyenrollorrotate-mvk. The backup is 8 KiB, takes one command, and turns "header bit-rot" from a vault-loss event into a one-command fix. - Keep the header backup on different storage from the vault. An attacker with both has the same offline brute-force surface as with the original vault.
- For high-value vaults: combine detached-header mode
(
--header) with the backup. Now there are three locations (.lbx, current.hdr,.hdrbackup) and any two surviving copies recover the vault. - Run
luksbox checkperiodically against vaults stored on unreliable media (cloud sync, old USB drives, SD cards). It is read-only, so adding it to a cron job has no risk of mutating state.