Deniable mode
Vaults whose on-disk bytes are indistinguishable from random output. Create, open, mount, enroll.
A deniable LUKSbox vault is a vault whose entire on-disk
representation is computationally indistinguishable from random
output: no magic bytes, no version field, no slot table, no
parseable structure visible to anyone without the right credential.
A forensic analyst staring at the file with file(1), libmagic,
yara, or a hex editor sees uniform random bytes.
This is an opt-in mode chosen at vault-creation time. Standard (non-deniable) vaults are untouched: their plaintext header magic
- version + slot table all stay where they are. Deniable vaults use a separate format (deniable v2, 36 KiB header; new ones use header format v3 since 0.6.0) and a separate set of CLI subcommands so the two paths cannot be confused.
The full cryptographic design lives at
docs/DENIABLE_HEADER.md
in the source repository. This page is the operator-facing summary.
When you want this
- You need to store a vault somewhere a generic forensic sweep will happen and "owns a LUKSbox vault" is itself sensitive information.
- You want a credential mix (passphrase + FIDO2 + TPM) but do NOT
want a
.lbx.tpmor recovery-card sidecar to advertise which factors a vault uses. - You are willing to remember the cipher choice and Argon2id parameters as part of the secret: forgetting them is permanent lockout, by design.
When you DON'T
- Vaults that need to be opened by colleagues who only know
"double-click the .lbx file". Deniable vaults will not associate
with the LUKSbox file-type because they have no recognisable
bytes; the user has to invoke
luksbox deniable-mount(or use the GUI / TUI deniable-open path) by hand. - Single-user setups where nobody is going to perform a forensic sweep. The standard format already protects the contents under AEAD; the deniable mode hides the existence of the vault on top of that, at the cost of more setup discipline.
- Backup archives indexed by automated tooling that needs to see the magic bytes.
Trade-offs vs the standard format
| Property | Standard vault | Deniable vault |
|---|---|---|
| On-disk magic / version | Plaintext | Random |
File-type detection (file(1), libmagic) |
Identifies as LUKSbox | Identifies as data / random |
| Header size | 8 KiB | 36 KiB |
| Slots | 8, each 512 B | 8, each 4 KiB |
| Pure FIDO2 / pure TPM | Supported | NOT supported (passphrase mandatory, see below) |
| External sidecars | .lbx.tpm, recovery card with cred_id / hmac_salt |
None (everything in-slot) for FIDO2 / TPM; .lbx.kyber retained for ML-KEM |
| Failure modes | WrongPassphrase / WrongDevice / ... |
Single OpaqueUnlockFailed (no oracle) |
| Cipher + Argon2 params | Stored in header | Must be remembered by the user |
| GUI recent-vaults list | Recorded | Never recorded (0.6.0+) |
| Giving back free space | luksbox size --shrink, on lock / unmount |
Only from the GUI's Vault size dialog, after you confirm (see below) |
Passphrase is mandatory
Every deniable credential variant carries a passphrase as the
envelope factor. The slot envelope is sealed by
KEK_envelope = Argon2id(passphrase, per_vault_salt, params); no
slot can be opened without it. The eight supported variants are:
- Passphrase only
- Passphrase + FIDO2
- Passphrase + TPM
- Passphrase + TPM + FIDO2
- Passphrase + hybrid-PQ (ML-KEM)
- Passphrase + hybrid-PQ + FIDO2
- Passphrase + hybrid-PQ + TPM
- Passphrase + hybrid-PQ + TPM + FIDO2
On macOS builds with Secure Enclave support, the four TPM combinations (3, 4, 7, 8) also exist with the Secure Enclave in place of the TPM.
Pure FIDO2, pure TPM, and pure hybrid-PQ are NOT available in
deniable mode. They would re-introduce the external cred_id /
hmac_salt / TPM-blob sidecars that the v2 design exists to
eliminate. If you want a FIDO2-only vault, use the standard
(non-deniable) format.
The GUI's create form hides the FIDO2-direct variant whenever Use a deniable header is ticked, for the same reason.
CLI
Create
# Passphrase-only deniable vault, default cipher / Argon2 params.
luksbox deniable-init secret.bin
# Explicit cipher + Argon2 (you MUST remember these to reopen)
luksbox deniable-init secret.bin \
--cipher aes \
--argon2-m 262144 --argon2-t 3 --argon2-p 4 \
--credential passphrase
# Passphrase + FIDO2 (slot embeds cred_id + hmac_salt; no sidecar)
luksbox deniable-init secret.bin --credential fido2
# Passphrase + TPM + FIDO2
luksbox deniable-init secret.bin --credential tpm-fido2
# Hybrid PQ + passphrase (writes a .kyber seed sidecar)
luksbox deniable-init secret.bin \
--credential pq-passphrase \
--kyber-path secret.bin.kyber
# Hybrid PQ-1024 + passphrase + TPM + FIDO2 + anchor sidecar
luksbox deniable-init secret.bin \
--credential pq-tpm-fido2 --pq-1024 \
--kyber-path secret.bin.kyber \
--anchor /run/media/usb/secret.anchor
Supported --credential values: passphrase, fido2,
pq-passphrase, pq-fido2, tpm, tpm-fido2, pq-tpm,
pq-tpm-fido2 (Linux and Windows TPM builds), and sep,
sep-fido2, pq-sep, pq-sep-fido2 (macOS Secure Enclave builds).
Every value is implicitly passphrase-bearing.
New deniable vaults use header format v3, which needs LUKSbox 0.6.0+
to open. Add --header-v2 when LUKSbox 0.2.x-0.5.x must still open
the vault (see Header format v3).
Inspect (does it unlock?)
luksbox deniable-info secret.bin \
--cipher aes --argon2-m 262144 --argon2-t 3 --argon2-p 4 \
--credential passphrase
Prints the decrypted inner header (cipher / KDF / flags / offsets) and the header format on success. Any failure (wrong passphrase, wrong cipher, wrong Argon2 params, corrupted header) collapses to a single opaque error to preserve the no-oracle property.
Since 0.6.0 deniable-info no longer prints which slot the
credential opened: someone made to show this output would otherwise
reveal that lower-numbered slots (the creator's) exist.
Mount
luksbox deniable-mount secret.bin /mnt/v \
--cipher aes --argon2-m 262144 --argon2-t 3 --argon2-p 4 \
--credential passphrase
Same opaque-failure semantics as deniable-info. Like
luksbox mount, it returns once the vault is mounted (on Windows too
since 0.6.1); -f keeps it in the foreground. With an anchor
sidecar:
luksbox deniable-mount secret.bin /mnt/v \
--credential pq-tpm-fido2 --pq-1024 \
--kyber-path secret.bin.kyber \
--anchor /run/media/usb/secret.anchor
A wrong anchor or a missing anchor file fails with the same opaque
error. An anchor that does verify, but has seen a newer state of the
vault, refuses the mount with rollback detected (or
header rollback detected for an older header on a header-v3
vault).
GUI
In the Create form, tick Use a deniable header. The form collapses to the variants the deniable format supports (pure FIDO2 / pure TPM disappear; everything left carries a passphrase field). The cipher dropdown and Argon2id sliders stay visible, and the GUI warns you that forgetting these values bricks the vault.
On the unlock screen, tick This vault uses a deniable header (advanced) and the form asks for the cipher / Argon2 params / credential variant the vault was created with (Identify keyslot, below, finds the credential type from the envelope passphrase). Wrong values produce the same opaque "unlock failed" toast as wrong credentials; the GUI never distinguishes the two.
Anchor errors on the deniable open path
If an anchor sidecar is attached and the GUI cannot read it as a
deniable anchor at all (wrong size, symlink, missing), it now
fails the pre-flight check before asking the format layer to
unlock. A post-credential anchor failure is translated to
"Anchor file does not decrypt under this vault's master key" rather
than the generic "unlock failed". The format layer still returns a
single OpaqueUnlockFailed so an attacker calling the library
directly sees no oracle, but the user-facing GUI gets enough
information to know which file to swap or where to look.
Identify keyslot (unlock view)
A deniable vault's slot table is encrypted, so the keyslot panel of
the unlock view cannot list it. Enter the envelope passphrase (with
the cipher and KDF strength) and click Identify keyslot: the GUI
lists the slots that passphrase opens and their credential type, and
switches to the matching unlock method. Nothing is unlocked or
written. If you pick the wrong credential type, the error names the
type the slot actually has (the CLI names the --credential value to
rerun with).
Keyslots view
Once a deniable vault is open, the Keyslots view shows a Deniable keyslots section instead of the slot list:
- It says which slot this session was unlocked with.
- Find slot finds the slot(s) a credential uses, from its envelope passphrase and Argon2id strength. Its FIDO2 key, TPM or Secure Enclave is not needed for that.
- Revoke... revokes a slot by number (the one picked when it was added), after a confirmation. The slot is overwritten with random bytes. The slot this session was unlocked with cannot be revoked.
The slots themselves still cannot be listed: an unused slot is random bytes and each used one is sealed under its own passphrase, which is the point of a deniable vault. A credential whose slot you do not know cannot be shut out in place: LUKSbox 0.6.1 has no way to rotate a deniable vault's master key (not from the CLI, the wizard or the GUI). Create a new deniable vault and copy the files into it instead.
Recent-vaults list
The GUI does not add deniable vaults to its recent-vaults list
(0.6.0+), so their paths are not written to recent.json. If you
created deniable vaults with an earlier GUI, remove them from the list
(right-click > Forget from history).
TUI wizard
luksbox wizard includes a Create deniable vault path under
its create menu. Same questions as the GUI: cipher, Argon2id
params, credential variant, optional .kyber seed path for
hybrid-PQ variants, optional anchor path. The wizard refuses to
proceed without a confirmed passphrase (which is mandatory in
deniable mode).
Anchor sidecars (rollback detection)
Standard anchors are 48 bytes (56 bytes for header-v3 vaults) with a visible magic; deniable anchors are 256 bytes where every byte is also indistinguishable from random output, so the anchor itself does not reveal that it belongs to a deniable LUKSbox vault. On a header-v3 vault the deniable anchor also records the header generation, inside its encryption. The verification path uses an HKDF-derived subkey of the MVK so a deniable vault and its anchor prove their pairing only when unlocked together.
Both anchor readers (anchor::read_and_verify for 48 B standard
and anchor::deniable_read_and_verify for 256 B deniable) refuse
to dereference symlinks at the format layer (O_NOFOLLOW), so a
swap between the GUI / CLI preflight and the actual open is
rejected with ELOOP.
Hybrid-PQ deniable variants: one or two passphrases
For the four pq-* variants, the ML-KEM seed sidecar
(.lbx.kyber) is wrapped under its own passphrase. At create time
the user picks one of:
- Reuse the envelope passphrase for the seed file. Default behaviour. The GUI and TUI present the seed-file passphrase field as optional with the hint "leave blank to reuse the envelope passphrase above for both roles". One passphrase opens both.
- Set a distinct seed-file passphrase for defence-in-depth. The envelope passphrase opens the slot envelope but the seed file refuses to decrypt without the second passphrase. Both must be supplied at every unlock.
At open time the unlock form shows the envelope passphrase (required) plus a seed-file passphrase field that defaults to blank. Blank means "reuse the envelope passphrase". The same strength meter, "Generate strong passphrase" button, and empty-passphrase confirmation modal apply to both fields.
Header format v3
New deniable vaults (deniable-init, the wizard, the GUI) use header
format v3 since 0.6.0. Nothing changes on disk that anyone could see:
the additions live inside the encrypted inner header.
- The inner header carries a header generation, bumped by every
slot change (enroll, revoke, rotation). The deniable anchor records
it, so an older copy of the 36 KiB header put back over the vault is
refused at an open with
--anchor. - The inner header also commits to the slot table (all eight slots, empty ones included). A cleared slot filled again with its old envelope, to bring a revoked credential back, is refused at open even without an anchor, with the usual opaque unlock failure.
A deniable vault on header v3 needs LUKSbox 0.6.0+ to open; older
versions report the usual opaque unlock failure. Create it with
deniable-init --header-v2 (or untick the option in the GUI, or
answer no in the wizard) when LUKSbox 0.2.x-0.5.x must still open it.
Existing deniable vaults move to v3 with deniable-upgrade-header
(one-way), the wizard's vault menu, or the GUI's
Upgrade header to v3 button in the Keyslots view. The command
takes the same cipher / Argon2 / credential arguments as
deniable-info, and --anchor to verify the anchor first and move
it to the new header generation:
luksbox deniable-upgrade-header secret.bin \
--cipher aes --argon2-m 262144 --argon2-t 3 --argon2-p 4 \
--credential passphrase \
--anchor /run/media/usb/secret.anchor
deniable-info shows which header format a vault uses. See
upgrade-header for the standard-vault
equivalent.
File size and free space
A deniable vault grows as you add files and never shrinks on its own: anyone who sees the file at two points in time (backups, sync history, disk images) would learn from a smaller file that data was deleted, about how much, and when. Deleted files' space stays inside the vault and is reused by the next files.
- The CLI never shrinks a deniable vault. The GUI's lock / unmount
prompt and
--shrink-on-closedo not apply to it either. - The GUI's Vault size dialog can give back the free space at the end of the file (0.6.1+), on request only: tick I understand that a smaller file reveals deletions, then click Give back ... anyway.
What forensic analysts can still learn
- The file exists and has a size. The deniable format does not hide that you have a 5 GiB file sitting on disk. If 5 GiB of random-looking bytes is itself suspicious in your context, pair with a steganographic carrier (out of scope for LUKSbox today) or store the file inside a larger container the analyst expects to be opaque (e.g. an encrypted disk image).
- Header size is 36 KiB rather than 8 KiB. The exact size is not aligned to any well-known multiple, but a sufficiently motivated analyst with a corpus of suspected vaults could correlate. LUKSbox has no option to pad the file to a target size.
- Changes in size over time, if the analyst has older copies of the file: it grows when you add data, and shrinks only if you give back free space from the GUI (see File size and free space).
- Vaults created before 0.6.0 store the length of the encrypted file tree in the clear at a fixed offset (36876): four zero bytes plus a small value, a 1-in-2^32 fingerprint for "this random file is a deniable vault" that also hints at the size of the file tree. Vaults created with 0.6.0+ mask it. Upgrading the header does not change an existing vault; to drop the field, create a new deniable vault and copy the files into it.
.lbx.kyberand.anchorsidecars are not themselves deniable beyond not declaring "LUKSbox" in their bytes. A forensic analyst who sees three randomly-named files together may guess they form a triple even if no individual file is identifiable.
The full enumeration of what is and is not defended lives on the threat model page.
Next
- Security architecture - where the deniable format sits in the trust graph
- Threat model - what deniable mode defends against, and what it explicitly does not
- CLI reference: deniable-init and the matching
deniable-mount/deniable-infoentries