/ Documentation / Operations / Deniable mode

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

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

When you DON'T

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

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:

  1. Passphrase only
  2. Passphrase + FIDO2
  3. Passphrase + TPM
  4. Passphrase + TPM + FIDO2
  5. Passphrase + hybrid-PQ (ML-KEM)
  6. Passphrase + hybrid-PQ + FIDO2
  7. Passphrase + hybrid-PQ + TPM
  8. Passphrase + hybrid-PQ + TPM + FIDO2

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 hides the "FIDO2 direct" radio whenever Deniable is ticked in the create dialog 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. Every value is implicitly passphrase-bearing.

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) 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.

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. 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, a missing anchor file, or rollback detection (anchor_gen > metadata_gen) all fail with the same opaque error.

GUI

In the Create vault dialog, tick Deniable. 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 Open screen, switch the source to Deniable and the form asks for the cipher / Argon2 params / credential variant the vault was created with. 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.

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 with visible AEAD framing; 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. 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:

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.

What forensic analysts can still learn

The full enumeration of what is and is not defended lives on the threat model page.

Next