/ 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
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:

  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

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:

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:

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.

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.

What forensic analysts can still learn

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

Next