Changelog
Per-version release notes.
The canonical changelog lives in
CHANGELOG.md
in the source repository. This page mirrors the highlights for the
last few releases.
v0.2.2 - 2026-06-02
Critical durability fix on top of v0.2.1 plus a large performance improvement on big-vault mounts. Closes a real-user data-loss class where v0.2.1's mirror protocol could commit "intended new state" pointers to disk before the chunk-list blocks those pointers referenced were durable, so a crash in a narrow post-mirror-fsync, pre-live-fsync window left a vault whose recovery target pointed at slots that still held pre-flush bytes and failed AEAD on reopen.
Existing v0.2.1 vaults open unchanged; the fix is entirely on the write path. No on-disk format change -- same LBM5 + LUKSBOX2 + sidecar mirror envelope. Vaults that already lost chunk-list blocks to this bug are NOT recovered by upgrading; this release only prevents future occurrences and ships a tolerant recovery mode so the healthy files in a damaged vault can still be copied out.
Performance: 3-minute rm is now milliseconds
A real-user report on a 10k-file vault: a single rm of one tiny
file took ~3 minutes; nemo and bash were both unresponsive while
the vault was mounted. Root cause: every FUSE metadata op
(create, mkdir, unlink, rmdir, rename, setattr,
symlink, link, FUSE flush / release) was driving a fully
synchronous Vfs::flush that rewrote the entire 64 MiB metadata
mirror and live region AND re-spilled every spilled inode's
chunk-list chain. Two layers of fix:
-
Layer 1 -- deferred flush (default). Those ops now mark the in-memory tree dirty and return immediately. A background
luksbox-flush-timerthread (one per mount) drives a realVfs::flushevery 30 seconds when dirty, plus on unmount. Explicitfsync(2)/fsyncdir(2)syscalls remain eagerly synchronous (POSIX-required). Net effect for the user's report:rmreturns in milliseconds,cp -r 10k_files /mnt/vaultcompletes in seconds instead of hours. -
Layer 2 -- per-inode dirty tracking. When the flush DOES fire, only inodes that were actually written, truncated, or newly created since last flush get re-spilled. Untouched inodes reuse their existing on-disk chunk-list blocks byte-identically. Per-flush cost is now O(K = touched inodes), not O(N = total spilled inodes).
Durability model matches ext4 / btrfs / xfs commit intervals: a
crash without an explicit fsync can lose up to 30 seconds of
metadata changes. The on-disk encrypted bytes that ARE flushed
retain the v0.2.2 mirror-protocol durability fence (chunk-list
blocks are durable before the mirror commits), so the post-flush
state is always crash-consistent. Keyslot revocation is unaffected
(revoke() calls persist_header independently of vfs.flush).
luksbox mount --sync (also accepted on deniable-mount)
restores the pre-v0.2.2 eager-flush semantics for users whose
threat model requires every operation to be durable on return.
The wizard TUI surfaces this as an "Eager flush?" prompt before
mount; the GUI Browser view exposes it as an "Eager flush
(--sync)" checkbox next to the Mount as volume button.
Default off in all three.
Platform coverage: the deferred-flush optimisation now runs on
every mount backend in v0.2.2 -- Linux libfuse3, macOS macFUSE,
macOS FUSE-T, and Windows WinFsp. Each backend hosts its own
luksbox-flush-timer thread with identical 30 s cadence,
identical --sync opt-out, and identical final-flush guarantee
on unmount. The shared interval constant lives in
luksbox_mount::LAZY_FLUSH_INTERVAL_SECS. Earlier v0.2.2 drafts
shipped Layer 1 for libfuse3 / macFUSE only; the FUSE-T and
WinFsp adapters now match.
Fixed: v0.2.1 durability hole (CVE-class data-loss)
Vfs::flush's spill stage writes external chunk-list blocks
through the page cache, then called Container::write_metadata,
which fsyncs .lbx.meta-bak BEFORE overwriting the live metadata
region. A crash between those two fsyncs (power loss, OOM kill,
kernel panic, host hypervisor death) could leave .lbx.meta-bak
durably committed to the NEW state with fresh chunks_external
head/generation tuples, while the .lbx chunk-list-block slots
those tuples reference still held bytes from a previous flush.
On reopen, mirror recovery picked the NEW pointers as
authoritative; the reader's AEAD then verified every targeted
chunk-list block under the NEW generation in AAD, but the bytes
on disk were written under the OLD generation. Result:
crypto: AEAD failure on every chunk-list block that hadn't
been fsync'd before the crash, cascading to metadata blob deserialization failed from the VFS layer at open time.
Real-world incident: a v0.2.1 user lost 26 contiguous-in- generation chunk-list blocks in a single flush after a crash, while data chunks and the metadata blob itself were intact.
Fix: new Container::sync_data_area() method (one
self.file.sync_all()), called from Vfs::flush between the
spill stage and write_metadata. The mirror's "reader can trust
these pointers" invariant is now backed by durable bytes.
Deniable vaults are unaffected (they explicitly opt out of the
sidecar-mirror protocol). v0.2.0 and earlier vaults are also
unaffected (no mirror protocol there at all).
Regression-tested three ways:
- Compliance test: per-thread flush-op log asserts
sync_data_arearuns strictly beforewrite_metadata. - Bug-reappearance test: a thread-local
set_skip_fence_for_testhatch reproduces the v0.2.1 ordering on demand and verifies the bug recurs there, so a future refactor that drops the fence cannot silently regress. - Crash-impact test: a SimFile backing models page-cache vs
durable bytes; the fence is enabled / disabled across two
runs and the open-after-simulated-crash result matches the
real-world bug signature (
MetadataDeserialize) only in the disabled run.
Fixed: FIDO2-direct vaults rejected by --fido2
luksbox check --fido2 (and the GUI's "Open" path) refused
fido2-direct vaults with no FIDO2 keyslots in this container.
The flag-driven unlock path iterated only SlotKind::Fido2HmacSecret
slots and skipped SlotKind::Fido2DerivedMvk, even though the
format layer's UnlockMaterial::Fido2 arm has handled both kinds
since v0.2.0. Fixed to accept both. Subcommands that don't get
--fido2 / --tpm2 / --tpm2-fido2 / --pq-hybrid on a vault
with zero passphrase keyslots now also error with a clear
vault has no passphrase keyslot; rerun with --fido2 instead of
silently prompting for a passphrase that no slot will ever accept.
Fixed: GUI YubiKey "Touch your YubiKey" overlay was un-cancellable
Real-user report: during a YubiKey unlock the overlay caught the button press intermittently, sometimes after 5-6 attempts, with no way to abort and retry. v0.2.2 adds a Cancel button to the overlay and binds Esc to the same handler. The overlay redraws every frame; either input cleanly aborts the pending FIDO2 op so the user can retry without restarting the GUI.
Added: tolerant recovery for already-broken v0.2.1 vaults
For vaults that already lost chunk-list blocks to the v0.2.1 durability hole (or any other corruption class that makes chunk-list-block AEAD fail), two opt-in recovery primitives:
-
LUKSBOX_DEBUG_OPEN=1instrumentsVfs::opento print metadata head bytes, magic matched, per-inode parse outcome, andvalidate_metadata_treeresults to stderr. Lets a user with a refusing vault pinpoint the exact parse step that trips, without source modification. -
LUKSBOX_TOLERATE_BAD_CHUNK_LISTS=1(or the GUI's "Recovery mode (read-only, skips broken files)" checkbox, or the wizard's "Try opening in recovery mode?" prompt onMetadataDeserialize) installs broken inodes as zero-byte placeholders instead of refusing the whole vault. The user can then mount read-only and copy out every healthy file. A newError::ReadOnlyMountblocksVfs::flushfrom ever persisting the patched tree, so the size-zeroed broken inodes cannot overwrite the originalchunks_externalpointers. The GUI surfaces a modal listing every tolerated file's path + original size + AEAD-failure reason so the user knows what to re-source.
Used in production by the incident-affected user to recover 1885 healthy files from a 10k-file vault and identify all 26 broken paths as reproducible pentest scan output.
GUI improvements
- Recent-vaults
[x]button is now actually clickable. Previously an egui z-order issue let the parent Frame'sinteract()swallow the click before it reached the button. Clicking now opens a centered confirmation modal (Forget / Forget AND Delete / Cancel) so the action isn't accidentally destructive. - Vault file picker "All files" filter now really shows all files. The earlier filter quirk hid extensionless vaults on some platforms.
- Open form gains an editable path field with a Browse button, so users can open vaults under non-standard paths or in shells that don't surface them in the picker.
Notes
- Anyone who hit the v0.2.1 durability bug: the lost chunk-list blocks are not recoverable from the live data area (the bytes never made it to disk). Recovery mode lets you copy every healthy file out. Re-source the broken files from their original location.
v0.2.1 - 2026-05-22
Durability + correctness release on top of v0.2.0. Headline fix:
crash-safe header and metadata writes via sidecar mirrors so a
mid-write force-quit no longer destroys the vault (the
user-reported v0.2.0 data-loss class). Also restores the
executable bit when git clone-ing into a mounted vault, raises
the metadata cap, surfaces capacity warnings in CLI and GUI, and
fixes the macFUSE Apple build.
Existing v0.2.0 vaults auto-upgrade to the new envelope on the
first flush. The upgrade is one-way; pre-v0.2.1 LUKSbox binaries
can no longer open the vault after that point. Set
LUKSBOX_FORMAT_V2=1 in the environment at create time if you
need the old envelope for sharing with a pre-v0.2.1 install.
Headline: crash-safe header + metadata writes (LBM5 + LUKSBOX2)
Fixes the real-user data-loss case from v0.2.0 where a vault that
hit ENOSPC mid-copy at ~13 GB with ~5000 small files became
permanently unopenable after force-quit (blob deserialization failed on unlock; or, in a separate failure mode where the
header was the corrupt region, no keyslot accepted the provided unlock material).
Two root causes:
- The metadata region cap was 16 MiB. The encoded directory tree
for a multi-thousand-file vault overflowed it and surfaced as
MetadataBudgetExhausted-> ENOSPC. - Both the 8 KiB header (with all keyslots) and the metadata blob were written in place with a plain seek+write. A crash mid-write destroyed the only copy.
The fix adds A/B durability for both critical regions plus a higher metadata cap, all cross-platform (Linux, macOS, Windows):
MAX_METADATA_SIZEraised 16 MiB -> 64 MiB withDEFAULT_METADATA_REGION_SIZEto match.- LBM5 metadata format lowers the inline chunk-ref spill threshold from 1024 to 256 chunks per inode, so encoded trees stay compact even for very large vaults.
- LUKSBOX2 header magic with two new HMAC-authenticated flag
bits (
FLAG_HAS_HEADER_MIRROR,FLAG_HAS_METADATA_MIRROR). <vault>.lbx.header-bakand<vault>.lbx.meta-bakhold the intended new bytes for crash-safety recovery, rotated via temp+rename BEFORE every live overwrite (intended-state protocol, see Security below). On open, if the live region fails to parse, the recovery path reads + verifies the mirror under the same MVK and proceeds, then forces a flush so the live region is re-established.- Auto-upgrade on first flush: any operation against a v0.2.0 vault rewrites it as LUKSBOX2 + LBM5 with the new mirrors. No manual migration command required.
- Deniable vaults are explicitly excluded from the sidecar mirror protocol to preserve the >=7.99 bits/byte entropy property of their on-disk artefact set. Crash safety for deniable vaults stays on the existing path (the 36 KiB header has internal slot-layout redundancy).
Security: intended-state mirror protocol (closes revoke-bypass)
An initial draft of the durability fix kept "previous-good" copies in the mirrors. An independent review and an end-to-end exploit script demonstrated that an attacker who corrupts the live header could force the recovery path, and the mirror's stale keyslot table would accept a previously-revoked credential. Confirmed auth-bypass.
Fixed by switching to intended-state mirror semantics: the
mirror is committed to the NEW bytes BEFORE overwriting live, so
the mirror always reflects what the user currently wants as the
authoritative state, never a historical snapshot. After a revoke,
the mirror reflects the post-revoke keyslot table. Even forcing
the recovery path cannot resurrect the revoked credential.
Regression-tested by
v2_corrupted_live_after_revoke_does_not_unlock_via_mirror and
by the new v2_mirror_recovery libFuzzer target.
Other security hardening in the durability path:
- Mirror file reads are stat-then-bounded so an attacker-planted
symlink to
/dev/zerocannot OOM the recovery path. - MVK rotation (
begin/commit_atomic_rotation) explicitly deletes both sidecar mirrors at commit time so the post-rotation vault cannot fall back to OLD-MVK-encrypted keyslots; fresh mirrors are written on the next live overwrite. - Mirror recovery is gated strictly on
Header::from_bytesparse failure; unlock-failure on an otherwise-parseable header does NOT trigger mirror fallback.
Fixed: git clone preserves the executable bit
Files materialised by git clone into a mounted LUKSbox vault no
longer lose the executable bit on scripts and binaries. The FUSE
create(O_CREAT, mode) callback now honors the requested mode
all the way through to the persisted inode field, instead of
defaulting every newly-created file to 0o644 regardless of what
the caller passed.
- Linux libfuse and macFUSE: fixed.
- macOS FUSE-T: fixed in the same release.
- Windows WinFsp: N/A (Windows uses NTFS attributes, not POSIX modes; executable-by-file-extension is the Windows convention).
Ground-truth verified end-to-end:
git clone https://github.com/PentHertz/RF-Swift.git into a
mounted v0.2.1 vault preserves -rwxrwxr-x on the shell scripts;
unmount + remount confirms the bits round-trip through LBM5 disk
persistence.
Added: metadata-budget capacity notification
Vaults holding very large inode counts (hundreds of thousands of files) can approach the 64 MiB metadata region cap before they exhaust host disk space. v0.2.1 emits a soft notification BEFORE the hard ENOSPC:
- >=75% used: informational. CLI users see a one-line message on stderr; GUI users see a non-blocking toast.
- >=90% used: stronger warning recommending content archival
or migration to a new vault with a larger
--metadata-size.
Notifications fire at most once per level per session.
Vfs::metadata_budget_status() is exposed as a public API for
external tooling that wants to build dashboards or pre-flight
checks.
Added: tested-boundary advisory (~30 GiB)
v0.2.1 has been ground-truth validated end-to-end (real FUSE mount, write, force-quit, reopen, verify) up to roughly 30 GiB of stored content with several thousand files. The format is engineered for larger vaults but usage beyond that boundary has not yet been explicitly tested.
LUKSbox surfaces a one-shot heads-up at two moments:
- At create time:
luksbox createprints an advisory noting the tested boundary. - At runtime: when the vault file on disk crosses 30 GiB, a one-shot CLI eprintln or GUI toast asks the user to verify the vault still unlocks and report any anomalies at https://github.com/PentHertz/LUKSbox/issues.
The advisory is informational, not a hard limit. The boundary will be raised in subsequent releases as validated usage data accumulates.
Fixed: macFUSE Apple build (cfg-gated renameat2 flags)
Linux-only renameat2(2) flag constants (RENAME_EXCHANGE,
RENAME_WHITEOUT, RENAME_NOREPLACE) are exposed by the fuser
crate's RenameFlags only under #[cfg(target_os = "linux")],
so the macFUSE build broke when the shared fuse.rs file
referenced them unconditionally. The rename handler now
cfg-gates the entire flag check; on macOS / Windows the
renameat2 flags don't exist in the FUSE protocol and the
check is moot. Linux behavior unchanged.
Install note: Trixie deb + plain su root
dpkg -i luksbox_*.deb aborts on Debian 13 with
dpkg: error: 2 expected programs not found in PATH or not executable (ldconfig, start-stop-daemon) when run from a shell
where root's PATH lacks /sbin. This happens when you switch
to root via su root rather than su -. Dpkg's pre-flight
check fails BEFORE any maintainer script runs, so we cannot fix
it inside the package. Workarounds:
sudo dpkg -i luksbox_*.deb(sudo honorssecure_path)su -thendpkg -i ...(login shell loads root's PATH)export PATH="$PATH:/sbin:/usr/sbin" && dpkg -i ...
The dist/install.sh driver uses sudo and is not affected.
UI: "v3 metadata format" label clarified
The GUI checkbox and TUI wizard prompt now refer to the v0.2.1
envelope (LBM5 + LUKSBOX2 + sidecar mirrors) explicitly, instead
of the misleading "v3" label inherited from when v3 just meant
LBM3 / external chunk lists. The boolean field name
(use_v3_format) is kept for API stability across the GUI ->
ops boundary.
v0.2.0 - 2026-05-20
Bigger feature drop on top of v0.1.1: FUSE-T on macOS, deniable-
header v2 mode, private mountpoints, on-disk metadata format v3
(new default), deniable MVK rotation that actually re-encrypts
chunks, plus the usual hardening pass. The metadata-format change
is forward-incompatible -- new vaults default to v3 (LBM\x03
magic) and cannot be opened by pre-v0.2.0 LUKSbox installs. Pass
--format v2 (or untick the box in the GUI) when creating a vault
you need to share with an older install. v0.1.x vaults open
unchanged. Deniable mode is a format that did not exist before
v0.2.0 and gained v3 support in this release as well.
Added: on-disk metadata format v3
- External chunk-list blocks in the data area instead of
inline
Vec<ChunkRef>inside the fixed metadata region. Practical per-vault content ceiling goes from ~8-10 GiB (v2) to no practical limit. Measured open time on this hardware: 19 ms at 1 GiB / 262K chunks; extrapolates to ~2 s at 100 GiB. - Per-file spill threshold is 1024 chunks (~4 MiB). Files below stay inline so the small-file case pays no extra read.
- AAD isolation between data chunks and chunk-list blocks: list
blocks live under a synthetic file_id (
real_id | (1<<63)) so the AEAD AAD intrinsically distinguishes them; a data chunk's ciphertext cannot decrypt as a chunk-list block or vice versa. luksbox migrate-to-v3 <src> --dst <new>reads any v2 vault and writes a fresh v3 vault. Source untouched; new vault gets a single passphrase keyslot, other keyslots can be re-enrolled afterwards.- Default flip to v3 happened only after: deniable rotation across
all 8 credential kinds was implemented and tested; fuzz target
- corpus added for the chunk-list parser; perf measured at 1 GiB
with sub-2-s extrapolated open at 100 GiB. Opt out per-vault
with
--format v2or viaLUKSBOX_FORMAT_V2=0in the env.
- corpus added for the chunk-list parser; perf measured at 1 GiB
with sub-2-s extrapolated open at 100 GiB. Opt out per-vault
with
Fixed: filesystem-boundary hardening (security audit follow-up)
A round of fixes for an external audit report covering TOCTOU /
symlink / path-substitution surfaces. All fixes preserve existing
vault contents; the changes harden write paths and the deniable
open path. Disclose new findings to [email protected].
- Panic-destroy symlink TOCTOU (high).
luksbox panicand its wizard / GUI counterparts usedis_file()+ laterOpenOptions::open()-- racy. An attacker with write access to the vault's parent directory could swap in a symlink between the check and the destructive open, redirecting the random-bytes overwrite to a file of their choice. With--wipe-datathe blast radius was the entire file size; run as root, this was an arbitrary-file-overwrite primitive via a deliberate destructive command. Now all three callsites use a newsecure_open_existing_no_followhelper that opens withO_NOFOLLOW(Unix) /FILE_FLAG_OPEN_REPARSE_POINT(Windows) and refuses non-regular files; handles open BEFORE the confirmation prompt so the prompt itself cannot be raced. - Deniable open bypassed
LUKSBOX_NO_FOLLOW_SYMLINKS.try_open_envelope_v2_deniableandopen_with_mvk_deniableused rawOpenOptionsand skipped the hardenedopen_rw_checked- post-lock
verify_path_inodepath that the standardContainer::opentakes. Now both deniable entry points go through the same hardened path.
- post-lock
- Deniable mount mountpoint check. Standard
luksbox mountusesO_DIRECTORY | O_NOFOLLOW+ canonical-path deny-list. Deniable mount was using onlyis_dir() + canonicalize()-- same hardened check is now applied to deniable mount. - Header-backup no-clobber race.
luksbox header-backupdidout.exists()thensecure_create_or_truncate(out)-- racy. Now usesatomic_secure_create_new(POSIXlink(2)/ WindowsMoveFileExW(0)). - Mount canonicalize-before-open.
cmd_mountand the FUSE-T helper used to callpath.canonicalize()BEFORE the hardened open, so whenLUKSBOX_NO_FOLLOW_SYMLINKS=1was set the symlink was already resolved by the time the no-follow check fired. Added a preflight (viastd::fs::symlink_metadata) that runs BEFORE canonicalize for both vault and--headerpaths. - Unlock prescan honors no-follow policy. The kind-dispatch
header peek (~14 callsites across CLI + GUI) used raw
File::openwhich followed symlinks even with the policy gate set. Now routed through a newopen_existing_read_no_follow_policyhelper. Default behaviour (env var unset) still follows symlinks so legit users aren't broken.
Other
Bigger feature drop on top of v0.1.1: FUSE-T on macOS, deniable- header v2 mode, private mountpoints, plus the usual hardening pass. No breaking format changes for standard vaults, every v0.1.x vault opens unchanged. Deniable mode is a NEW format (v2) that did not exist before v0.2.0.
Added, macOS FUSE-T backend
- FUSE-T support on macOS, in addition to macFUSE. FUSE-T is
the kext-free FUSE implementation that talks to the macOS NFS
client over a localhost loopback; no kernel extension to install,
no Recovery Mode dance, no Apple Silicon-specific opt-in. Ships
as a separate
.dmgper backend so users can pick:LUKSbox.dmg(FUSE-T, the new default-preferred path) orLUKSbox-macfuse.dmg(legacy macFUSE, kept for users who already use it elsewhere). - Subprocess isolation for FUSE-T. The FUSE-T helper runs in
its own process so when libfuse-t aborts itself during unmount
teardown (a known libfuse-t behaviour we cannot intercept from
Rust), only the child dies and the GUI keeps running. MVK is
passed to the child over an anonymous stdin pipe, zeroized on
both sides immediately after transfer. Architecture in
docs/MACOS_FUSE_T.md. - Optional macOS sandbox profile for the FUSE-T helper
subprocess (
LUKSBOX_SANDBOX_HELPER=1). Restricts read/write to the vault directory + mountpoint + a small allowlist of system paths. Hard-fails if the profile file is missing rather than silently downgrading to unsandboxed.
Added, deniable-header mode (v2)
- Deniable vaults. A new on-disk format where every byte of
the 8 KiB header is indistinguishable from random output: no
magic, no version, no slot table, no parseable structure
visible to an attacker without the right credentials. Two-layer
AEAD envelope (passphrase-derived
KEK_envelopeopens the slot, then a per-slotKEK_factorsunwraps the MVK). Every failure mode collapses to a singleOpaqueUnlockFailedto preserve the no-oracle property. - All eight credential variants supported: passphrase,
passphrase + FIDO2, passphrase + TPM, passphrase + TPM + FIDO2,
- the four hybrid-PQ counterparts. Every variant carries a mandatory passphrase (the envelope-opening key) by design; pure FIDO2 / pure TPM variants do not exist in deniable mode because they would re-introduce the fingerprinting tells the v2 design set out to fix.
- GUI, CLI, and wizard support for deniable create, open, mount, slot enroll (passphrase / FIDO2 / TPM / TPM+FIDO2 / all hybrid-PQ combos), and slot revoke. The GUI hides the FIDO2-direct radio at create time when "deniable" is ticked (the variant cannot be deniable-compatible).
- Deniable anchor sidecars (256 B AEAD-encrypted) for rollback detection without leaking the existence of an anchor.
- Full design rationale in
docs/DENIABLE_HEADER.md.
Added, mount UX
- Private mount option on macOS (FUSE-T only). New
--private-mountCLI flag, matching "Mount privately" button in the GUI, and a yes/no prompt in the wizard. Mounts the vault under~/Library/LUKSbox/Mounts/<vault-name>instead of/Volumes/<vault-name>so the mountpoint name is invisible to other user accounts on shared Macs. Seeluksbox mount --private-mount. - Pre-flight check before spawning the FUSE-T helper detects the "another process already holds the vault flock" case and surfaces a friendly toast instead of letting the child exit with a cryptic non-zero status.
Added, packaging
.debRecommendstpm-udev+tpm2-toolson Debian-family..rpmRecommendstpm2-tss+tpm2-toolson Fedora-family. Installing viaapt/dnfnow brings in the/dev/tpm*udev rules and thetsssystem group by default. Post-installusermod -aG tss "$USER"is still required (Debian / Fedora convention forbids silent privileged-group additions). See Permissions.
Added, fuzz coverage
- Two new libFuzzer + AFL++ targets:
chunk_aead_decrypt(per-chunkaead::opencallsite with attacker-controlled bytes) andanchor_parse(standard + deniable readers). Added to both CI matrices (5-min smoke on PR + 30-min nightly). - AFL++ harness
auth_then_processbrought in lock-step with the libFuzzer version (was still on bincode while libFuzzer had migrated to postcard + magic-prefix dispatch).
Improved, deniable open UX
- Anchor verification error messages on the deniable open
path. A pre-flight check rejects missing / wrong-size /
symlink anchors with a specific message; a post-credential
error is translated to "Anchor file does not decrypt under
this vault's master key" instead of the generic "unlock
failed". Format-layer error stays a single
OpaqueUnlockFailedso deniability is unaffected.
Changed, security hardening
- Sandbox hard-fail.
LUKSBOX_SANDBOX_HELPER=1now refuses to spawn the FUSE-T helper unsandboxed if the profile file is missing, instead of falling back silently. The user opted in, so honour it or refuse, never silently weaken. Keyslot::fido2_cred_idzeroized on drop. The CTAP2 cred-id is a public handle (not cryptographically secret), but zeroizing the heap buffer matches the rest of the codebase's defence-in-depth posture.- O_NOFOLLOW on anchor reads. Both
anchor::read_and_verify(48 B standard) andanchor::deniable_read_and_verify(256 B deniable) now refuse to dereference symlinks at the format layer, defending against a swap between the GUI / CLI preflight and the actual open. - FD-based mountpoint validation. The CLI now opens the
mountpoint with
O_DIRECTORY | O_NOFOLLOWas an atomic "directory + not a symlink" check, replacing a small TOCTOU window betweenis_dir()andcanonicalize().
Security review
- New "Not defended" rows in SECURITY.md and on the threat-model page: filesystem snapshots while unlocked, kernel-resident attacker, hardware tampering beyond the FIDO2 device, evil-maid on the binary itself, supply-chain on dependencies.
cargo audit: one unmaintained advisory remaining,registry 1.3.0reachable only viawinfsp_wrs(Windows). No fix possible from our side; upstream-tracked.- dudect re-run on
Header::verify_hmac:|t| = 0.563over 47 K samples, well under the 3.0 leak threshold. Constant-time property preserved.
Security audit, Round 13 - findings shipped fixes same revision
Internal Round-13 sweep across local filesystem boundary races, header durability, sidecar DoS surfaces, and remaining secret-copy hygiene. 2 HIGH, 5 MEDIUM, 2 LOW, 1 INFO. No CRITICAL. ALL 9 findings fixed in the same revision. Full per-finding report + Fix-status table at docs/SECURITY_AUDIT_ROUND_13.md.
HIGH fixes
- R13-01
secure_create_or_truncate(used byluksbox getand the GUI extract path) now opens the destination throughopenat(parent_dir_fd, basename, O_RDWR|O_CREAT|O_TRUNC|O_NOFOLLOW, 0600)against a canonical parent fd. Closes the intermediate- directory symlink-swap window that the prior Round 12 fix left open as a documented follow-up. Permission narrowed viafchmodon the open fd; Windows reparse-point rejection retained. - R13-02
luksbox header restoreno longer re-opens the vault path after the HMAC verify. NewContainer::restore_header_bytesreuses the container's already-verified, already-inode-boundself.filehandle (inline) or routes throughatomic_secure_write(detached). The--no-verifydirect write also addsO_NOFOLLOWon Unix and refuses reparse points on Windows.
MEDIUM fixes
- R13-03
Vfs::real_sizeclamps the chunk-0 authenticated u64 against the inode's allocated chunk capacity. Hostile hide-size vaults can no longer panic stat / read / mount via out-of-rangeinode.chunks[idx]. - R13-04
Container::persist_headerusessync_all()on inline- deniable headers, and
atomic_secure_write(temp + fsync + rename + sync_parent_dir) on detached, then re-opens the lock handle to the new inode. A power loss mid-persist no longer leaves a half-rewritten header / sidecar.
- deniable headers, and
- R13-05
.kyberseed-file reads open withO_NOFOLLOW(Unix) / reparse-point rejection (Windows), require a regular file of exactly the fixed format length, thenread_exact. - R13-06 Hybrid sidecar reader preflights
metadata(), requires a regular file under 32 KiB, thenread_exact. Closes the unboundedread_to_endpath on both Unix and Windows. - R13-07 New
luksbox_vfs::MAX_FILE_SIZE = 1 << 44cap +Error::FileSizeExceedsCapvariant.Vfs::writeandtruncaterefuse oversize targets BEFOREpadded_chunk_countcan feednext_power_of_twoa panicking value or the chunk-allocation loop can exhaust RAM.
LOW fixes
- R13-08
luksbox-mount's FUSEreadcaps the requester- suppliedsizeat 16 MiB internally before the vec allocation. - R13-09
SecretBox::cloneallocates a fresh secret-memory backing andcopy_from_slices directly between the two allocator-owned regions. No by-value[u8; KEY_LEN]stack temporary.
INFO R13-INFO-1: cargo audit advisory RUSTSEC-2025-0026
(registry 1.3.0 via winfsp_wrs_sys) remains accepted as
documented in audit.toml.
New regression coverage: 4 test files, 14 deterministic tests
(0 ignored). Run them with cargo test --test round13_findings -p luksbox-format (and similarly per-crate), or simply
cargo test --workspace --exclude luksbox-gui.
Security audit, Round 12 - findings shipped fixes same revision
Four-axis adversarial sweep (FUSE-T subprocess + deniable v2 + filesystem TOCTOU + memory safety). All CRITICAL + HIGH + 5 of 7 MEDIUM fixed in the same revision. Full per-finding report
- Fix-status table at docs/SECURITY_AUDIT_ROUND_12.md.
Headline fix - R12-01 (CRITICAL): deniable envelope discovery
loop is now constant-time. try_open_envelope_v2 runs identical
work per slot (always-allocate fixed scratch, always-memcpy via
subtle::Choice-driven byte selection), and SlotPayload::decode
runs ONCE on the constant-time-chosen slot's plaintext - so
variable-length heap allocations are slot-position-independent.
Pinned by the new dudect bench
crates/luksbox-format/benches/dudect_deniable_envelope.rs.
HIGH fixes
- R12-02 CLI seed-file passphrase blank-= reuse envelope,
matching GUI + wizard. New
cli_pq_decap_with_fallback. - R12-03 Helper subprocess canonicalizes
--headerand the sandbox profile gains${HEADER_DIR}allow rules. - R12-04
MountBackend::Subprocesshasimpl Dropthat kills- reaps the helper child on GUI panic / force-quit.
- R12-05 Helper mountpoint validation uses the same
O_DIRECTORY|O_NOFOLLOWprobe + deny-list as the parent CLI. - R12-06 Hybrid sidecar reads use
O_NOFOLLOW(ELOOPon symlinked.hybridfiles).
MEDIUM fixes (R12-07 sandbox-subpath canonicalize; R12-09
extract-parent deny-list; R12-10 .rotating tmp O_EXCL|O_NOFOLLOW;
R12-12 helper MVK stdin Zeroizing<[u8;32]>; R12-13 deniable
cand_bytes Zeroizing).
LOW fixes (R12-16 vault-name : rejection + byte-cap; R12-18
TPM SensitiveData heap Zeroizing).
Empty-passphrase warnings (Round 12 follow-up) CLI's
read_passphrase_confirmed now mirrors the wizard's and the GUI's
empty-pass confirm modal (default no, with LUKSBOX_ACCEPT_EMPTY=1
escape hatch).
All other fixes
- R12-08
cmd_mountre-probes the canonical mountpoint inode immediately before the mount syscall and refuses if it changed. - R12-11
open_rw_checkedcaptures the canonical path at first open;verify_path_inodeopens that canonical path withO_NOFOLLOW. Legitimate symlinked-vault workflows preserved. - R12-14 Formally superseded by R12-11 - inverting
open_rw_checked's default would break legitimate workflows for no remaining security gain. - R12-15 Windows reparse-point coverage on the anchor + extract
paths via
FILE_FLAG_OPEN_REPARSE_POINT+ post-open attribute check. - R12-17 New
MasterVolumeKey::from_zeroizingandKeyEncryptionKey::from_zeroizingconstructors take a reference to aZeroizing<[u8;KEY_LEN]>, eliminating the by-valueCopystack-residence pattern at the type level. Helper subprocess MVK construction migrated. - R12-19
HmacSecretis nowpub struct HmacSecret([u8;32])withZeroize + ZeroizeOnDrop,Deref, redactedDebug, and constant-timePartialEq. All three backends updated.
Round 12 closed cleanly. All 19 findings shipped fixes.
New test infrastructure shipped this round
dudect_deniable_envelopebench atcrates/luksbox-format/benches/dudect_deniable_envelope.rs. The statistical timing regressor for R12-01. Expected to FAIL on current branch (|t| > 3.0) and pass after the fix. Reproduce:cargo bench --bench dudect_deniable_envelope -p luksbox-formatdeniable_envelope_multi_slotfuzz target (libFuzzer + AFL++). Builds a real deniable header with a fuzzer-selected slot occupancy bitmap, then drivestry_open_envelope_v2with attacker input. Catches regressions to a non-opaque error variant or a panic on the multi-slot path. Wired into both the 5-min PR CI smoke and the 30-min nightly. Reproduce:cargo +nightly fuzz run deniable_envelope_multi_slot # or AFL++: cargo afl build --release --bin deniable_envelope_multi_slot afl-fuzz -i fuzz-afl/seeds/deniable_envelope_multi_slot \ -o fuzz-afl/out/deniable_envelope_multi_slot \ -- target/release/deniable_envelope_multi_slotround12_findings.rsregression suite atcrates/luksbox-format/tests/round12_findings.rs. 7 tests: 2 always-run smoke tests + 5#[ignore]-gated placeholders, one per open HIGH finding. Each placeholder is the regression slot the matching fix PR will populate. Reproduce:# The two always-run tests (smoke + functional) cargo test --test round12_findings -p luksbox-format # The placeholders (will panic until each fix lands) cargo test --test round12_findings -p luksbox-format -- --ignored
v0.1.1 - 2026-05-08
First post-release iteration on top of v0.1.0. No breaking format changes - every v0.1.0 vault opens unchanged under v0.1.1.
Fixed
- WinFsp: files copied via Explorer disappeared after unmount /
remount. The
Cleanupcallback only flushed the VFS metadata on the DELETE path, so normalCreateFile -> WriteFile -> CloseHandleleft chunks on disk with no inode pointing at them. Fixed; integration test added to the WinFsp CI job that runs on every push. - GUI: ML-KEM-1024 TPM keyslots could not be unlocked. Hybrid PQ + TPM unlock dispatch only matched the 768 variants, silently skipping every 1024-grade slot. Fixed.
- macOS Developer ID signing pipeline failures. PBES2 vs PBES1
PKCS12 incompatibility with
security import(now handled withopenssl pkcs12 -export -legacy ...); AMFI strict-XML rejection of comments in the entitlements<dict>(comments stripped, rationale moved todist/macos/README.md). - Test pollution between parallel symlink tests. Env-var leak
between
nofollow_symlinks_env_var_refuses_symlinked_vaultandsymlink_to_real_vault_opens_cleanly; fixed with a per-fileOnceLock<Mutex<()>>serializing env-mutating tests.
Added - forensic / partial-recovery CLI toolkit
Walkthrough on the Forensics page.
header-backup- save the 8 KiB header bytes to a separate file (no unlock required, mode 0600).header-restore- restore from backup, HMAC-verified by default;--no-verifyenumerated as an operator-explicit safety bypass in SECURITY.md sec.3.header-dump- JSON dump of every inode, chunk reference, generation counter, and keyslot summary. Read-only.check- walk every used chunk, AEAD- decrypt, report per-chunk status. Exit non-zero on failure.--jsonfor tooling.extract --tolerate-errors- best-effort extraction; writes 4 KiB zeros for unrecoverable chunks. Flag is mandatory so users don't silently capture lossy output.
Added - release pipeline
- Apple Developer ID codesigning + notarization for macOS
.dmgreleases. Opens with the standard "downloaded from internet" prompt rather than the Gatekeeper block. - Windows static-CRT linking. No more
VCRUNTIME140.dll,MSVCP140.dll, orapi-ms-win-crt-*.dllruntime dependency. End users no longer need a Visual C++ Redistributable. SmartScreen still warns on first launch (EV Authenticode is on the v0.2 roadmap). - Per-Ubuntu-release
.debbuilds - separate package per supported Ubuntu line so apt resolves the matchinglibfido2-1/libfuse3-3/libssl3runtime. - GitHub Artifact Attestations (Sigstore-backed) on every
release:
gh attestation verify <file> --owner penthertz.
Changed - security hardening
Non-breaking tightenings of the safe envelope. No vault or workflow that worked under v0.1.0 is affected.
.kyberseed-file Argon2id memory cap:SAFE_M_COST_KIB_MAXlowered from 4 GiB to 512 MiB. Peak Argon2id memory request a hostile.kybercan force drops from 16 TiB to 1 TiB. All 5 existing seed-file DoS-guard regression tests still pass.- libfido2 cred-ID pointer null-check: defends the
from_raw_partsunsafe block against a hostile / firmware-buggy authenticator returning(id_len > 0, id_ptr = NULL). - WebAuthn DLL trust-boundary documentation: makes explicit why
the Windows path doesn't need the same defence (
webauthn.dllis part of Windows itself; trusting it is the same trust we place in every Win32 API call). - SECURITY.md sec.3 now enumerates every operator-explicit safety
bypass (
LUKSBOX_NO_LOCK=1,LUKSBOX_NO_FOLLOW_SYMLINKS=1,header restore --no-verify) with their preconditions and consequences.
Documentation
- CRYPTO_SPEC sec.3.9 Per-chunk encryption layering - canonical reference for the three-layer chunk-protection property (per-chunk random nonce, binding AAD, per-file derived key) with mermaid diagram, source line refs, and an explicit "what removing each layer would break" walkthrough.
- CRYPTO_SPEC sec.sec.3.4 - 3.8 complete the on-disk footprint:
detached headers, the
<file>.tmp.<16hex>transient temp-file convention,<vault>.rotatingMVK-rotation temp, GUI state files ($XDG_DATA_HOME/luksbox/{recent,preferences}.json), and the crash-orphan classification policy. - New DISCLAIMER.md + matching Disclaimer page restating Apache 2.0 sec.7-sec.8 in plain English. New "use LUKSbox for shared / backup copies, not as your only copy" notice on the docs landing page, README, Quickstart, and homepage FAQ.
Known limitations
- Windows SmartScreen still warns on first launch (EV Authenticode certificate planned for v0.2).
- Apple Silicon Macs need the one-time Recovery Mode -> Reduced
Security setup before macFUSE's kernel extension loads - only
required for
mount; CLI / GUI / extract work without it. - Format compatibility guarantee remains pre-1.0; v0.1.x reads every v0.1.x vault, format may evolve under audit guidance before v1.0.
v0.1.0 - 2026-05-06
Initial public release. The core feature set was audit-tracked through 9 internal review rounds before the cut.
- Encrypted vault containers with up to 8 keyslots
- Keyslot factors: passphrase (Argon2id), FIDO2 (CTAP2 hmac-secret
- cred-ID-derived modes), TPM 2.0 (7 variants), Windows Hello, hybrid post-quantum (ML-KEM-768 / ML-KEM-1024 + classical)
- AES-256-GCM-SIV default cipher (legacy AES-256-GCM and ChaCha20-Poly1305 also supported)
- FUSE (Linux + macFUSE on macOS) + WinFsp (Windows) mount adapters
- Anchor-based rollback detection (separate-storage sidecar)
- MVK rotation with crash-safe
.rotatingtemp + atomic commit - See the audit log for per-round summaries
Pre-1.0 status
LUKSbox is currently pre-1.0 and the on-disk format may evolve under audit guidance. Migration tools are provided for any breaking format change, but the conservative move is to let the v1.0 cut settle before relying on it for archival storage.
When the v1.0 line is cut, format compatibility becomes a hard guarantee: any v1.x release will read any vault produced by any other v1.x release.
Release verification
Every published release ships a SHA-256 manifest and a Sigstore attestation. See Download / Verify your download for the verification commands.