Stop the ciphertext leaking its own output filename - #14
Merged
Conversation
`gzip.GzipFile(fileobj=args.outfile, ...)` was given only a file object, so gzip
derived the header FNAME field from `fileobj.name`. The ciphertext therefore
recorded, in cleartext, the path it was written to:
$ lenticrypt -o my-secret-plans.enc -e key1 p1 -e key2 p2
$ dd if=my-secret-plans.enc bs=1 skip=10 count=19
my-secret-plans.enc
For a cryptosystem whose stated purpose is plausible deniability -- one that goes
out of its way not to reveal even how many plaintexts a ciphertext holds -- an
attacker recovering the output name from the first bytes defeats the point before
any cryptanalysis begins.
Passing `filename=''` suppresses FNAME. Two things fall out of it:
* Output no longer depends on the output path, which on its own prevented
`--seed` from producing byte-identical results: the same inputs written to
a.enc and to a-considerably-longer-name.enc differed in length.
* `mode='wb'` is now explicit, silencing the Python 3.14 FutureWarning that
inferring write mode from `fileobj` will stop working -- without it this
would eventually become a read-mode open.
Ciphertexts written by earlier versions still carry FNAME and still decrypt; the
format fixtures cover that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy
Two CI problems, one real.
**The lockfile was environment-specific.** It was generated on a machine with a
site-wide `/etc/uv/uv.toml` setting `exclude-newer = "1 week"`, and uv records
that in the lock:
exclude-newer = "2026-07-30T15:33:53.483359Z"
exclude-newer-span = "P1W"
CI has no such policy, so uv re-resolved, found the lock stale, and `--locked`
failed every job it managed to start:
Resolving despite existing lockfile due to removal of global exclude newer
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.
The lock is regenerated with `uv --no-config lock`, so it carries no policy and
matches a vanilla environment. `--locked` is dropped as well: pinning CI to a lock
that a contributor's local policy can invalidate trades a real failure mode for
drift detection that a zero-runtime-dependency project barely needs.
Note for regenerating it under that policy: use `uv --no-config lock`, or a plain
`uv lock` will re-add the exclude-newer fields.
**macOS is dropped from the matrix.** For pure Python it duplicates Linux --
same POSIX semantics, same path handling, same availability of the modules this
package touches. The leg that earns its place is windows-latest, which catches
the `fcntl`/`termios` import class of bug, and that joins once progress.py stops
importing them at module scope.
The other 15-20 minute "failures" in that run were not failures: every
ubuntu-latest and build job was cancelled with zero steps, having never been
assigned a runner. A smaller matrix should help there too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy
ESultanik
force-pushed
the
fix-gzip-filename-leak
branch
from
August 7, 2026 19:23
6a6e634 to
d89f488
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #13.
gzip.GzipFile(fileobj=args.outfile, ...)was given only a file object, so gzip derived the header FNAME field fromfileobj.name. The ciphertext therefore recorded, in cleartext, the path it was written to:For a cryptosystem whose stated purpose is plausible deniability — one that goes out of its way not to reveal even how many plaintexts a ciphertext holds — an attacker recovering the output name from the first bytes defeats the point before any cryptanalysis begins.
Change
Passing
filename=''suppresses FNAME. Two things fall out of it:--seedfrom producing byte-identical results: the same inputs written toa.encand toa-considerably-longer-name.encdiffered in length.mode='wb'is now explicit, silencing the Python 3.14FutureWarningthat inferring write mode fromfileobjwill stop working — without it this would eventually become a read-mode open.Verification
tests/test_gzip_container.pycovers the FNAME flag, filename independence, the pinned mtime, and cross-process reproducibility under differingPYTHONHASHSEED. The first two were confirmed failing before the fix.Ciphertexts written by earlier versions still carry FNAME and still decrypt; the format fixtures cover that.
🤖 Generated with Claude Code
https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy