Repository navigation
Releases: drsamonline/lockhtml
Release list
Latest app build
Automated build from df6f004. Assets: lockhtml-portable.exe, lockhtml.apk.
LockHTML v2.1.0
LockHTML v2.1.0
age interoperability — LockHTML now reads and writes standard age files.
The item I deferred in v2.0 is done, and done the way it should have been: verified against the reference implementation rather than against my reading of the spec.
What you can do now
lockhtml keygen --type age -o key.txt # an age identity; prints the recipient
lockhtml age-encrypt report.pdf -r age1abc… # -> report.pdf.age
lockhtml age-decrypt report.pdf.age -i key.txt -o report.pdfThe key file is in the standard age format, so age -i key.txt works on it too — and files either tool produces, the other can open.
Supported
- age v1 header and its MAC
X25519recipients and identities (Bech32age1…/AGE-SECRET-KEY-1…)- the 64 KiB ChaCha20-Poly1305 STREAM payload
- multiple recipients per file
- unknown stanza types are skipped on decrypt, as the spec requires
Not implemented: scrypt passphrases, the ML-KEM / X-Wing hybrid and P-256 tagged recipients, and ASCII armor.
Why it is CLI-only
age uses ChaCha20-Poly1305, which the browser's Web Crypto API does not expose — only AES-GCM. So the HTML viewer and the GUI cannot open age files. This is stated plainly in the docs rather than left as a surprise.
How it was verified
- The official vector.
testdata/example.ageand its identity from FiloSottile/age are embedded in the test suite, so CI decrypts a real reference-produced age file with no network access. - Bidirectional interop against the
agev1.2.1 binary — empty input, 1 byte, 64 KiB−1, exactly 64 KiB, exactly 128 KiB, 64 KiB+1, multi-chunk, and binary data — both directions, plus multi-recipient files.
That exercise caught a real bug. A payload whose length is an exact multiple of 64 KiB decrypted incorrectly: the final chunk is full-sized, so a naive reader treats it as non-final and the tag check fails. The spec sentence that matters ("the final chunk MAY be shorter than 64 KiB") does not make that case obvious — only the reference binary surfaced it. Fixed, and covered by tests at 65536 and 131072 bytes.
Honest caveat
This is a from-scratch implementation of a published format and it has not had an independent security review. It interoperates with the reference implementation on the cases above, and it refuses low-order X25519 shares and length-checks stanza bodies as the spec requires — but for anything that matters, prefer the reference age tool. That caveat is in SECURITY.md and docs/AGE.md, not just here.
Verification
111 unit tests (24 of them new for age), 35/35 headless-Chrome checks, 9/9 browser→Python cross-implementation checks.
Live GUI
LockHTML v2.0.1
LockHTML v2.0.1
A post-release audit pass. Every fix here was found by running the code, not reading it.
Fixed
gui.shdid not work at all. Itexec'dlockhtml.sh, which has no executable bit, so it died withPermission denied. It now runs the launcher throughsh.- The GUI's Lock button stayed disabled when you used a recipient public key or a share split instead of a PIN — only the PIN field had an input listener, so the button never re-evaluated.
- The GUI silently ignored a hidden volume combined with recipient or split mode, which could leave you believing a hidden payload existed when it did not. It is now an explicit error, matching the CLI.
lockhtml.batbroke on any Python path containing a space (C:\Program Files\…) —%PY%was used unquoted.unlock()/decrypt()on a streaming payload raised a bareKeyError; they now say the sidecar is needed.
Changed
--auto-kdfnow says so instead of silently doing nothing when--kdf pbkdf2is also given; mismatched--hidden-pin/--hidden-filecounts print a note.make minifyis now an alias formake build. Minifying an already-minifiedindex.htmlis not idempotent, so the old target could leave an asset CI rejects with a confusing diff.
Test suite
- The launcher test now executes
lockhtml.sh(locking and unlocking a real file) andgui.sh, instead of only syntax-checking them — a syntax check is exactly what missed thegui.shbug. - The browser suite no longer waits the full 25 s stage timeout on checks that expect a rejection; ten of them were burning ~250 s between them. It now finishes in about 40 s, down from over 290 s.
- Browser fixtures use cheap-but-real Argon2id parameters; production strength stays covered by the Python unit tests.
Verification
87 unit tests, 35/35 headless-Chrome checks, 9/9 browser→Python cross-implementation checks.
Note
v2.0.0 is left exactly as published — this is a patch on top, not a rewrite.
LockHTML v2.0.0
LockHTML v2.0.0
A hidden volume, a KDF that tunes itself to the machine, and launcher scripts that just run.
Highlights
- 🕳️ Hidden volume — a locked file can carry a second (or third…) payload behind its own PIN. The cover PIN opens the cover document; the hidden PIN opens entirely different bytes. The same idea as the duress PIN, generalised.
- 🎛️ Auto-calibrated KDF —
lockhtml calibratetimes one Argon2id pass on this machine and picks the strongest time cost that still unlocks in about half a second.--auto-kdfstores those parameters in the file, so it always opens at the cost it was made with. - 🎭 Streaming decoy — the duress PIN now works on streaming containers: it renders a small embedded page instead of ever decrypting the sidecar (whose MAC covers that page, so swapping it is detected).
- 🐢 Constant-memory save — the viewer can decrypt a streaming container straight to disk, chunk by chunk, via the File System Access API (Chromium). Other browsers keep the Blob download.
- 📁 Folders —
lock --recursive DIRon the CLI, and a folder picker plus directory drag-and-drop in the GUI. - 🚀 Launcher scripts —
lockhtml.sh/lockhtml.batandgui.sh/gui.batrun the CLI straight from a checkout, find Python 3.9+, prefer a local.venv, and tell you exactly what is missing. - 📦 PyPI-ready — a trusted-publishing
publish.ymlworkflow is included.
Usage
./lockhtml.sh calibrate # what can this machine afford?
./lockhtml.sh lock cover.html --pin 4821 --hidden-pin 0000 --hidden-file real.html --auto-kdf
./lockhtml.sh unlock cover.locked.html --pin 0000 # the hidden content
./lockhtml.sh lock ./contracts --recursive --pin 4821 --outdir ./lockedLive GUI
👉 https://drsamonline.github.io/lockhtml/
Verification
86 unit tests, a 35-check headless-Chrome run covering embedded, streaming, recipient, shares, hidden-volume and streaming-decoy paths, and 9 cross-implementation checks where the browser locks files the Python core then opens. All of it runs in CI.
What is not in this release, and why
ageinterop is deferred. It is a separate file format with its own chunked-ChaCha20-Poly1305 STREAM construction;cryptography's ChaCha20-Poly1305 has no Web Crypto equivalent, so it would be CLI-only, and it needs validating against the official age test vectors before it can be trusted. Shipping unverified crypto interop would be worse than not shipping it — it stays the top roadmap item.- PyPI publication needs one thing on your side: a trusted publisher configured for this repo. The workflow is ready; the account access is not something CI has.
Security note
A hidden volume is plausible deniability, not proof of absence — the extra slots are visible in the file's JSON to anyone who looks, they simply cannot be read without their PIN. See SECURITY.md and the self-audit in docs/AUDIT.md.
LockHTML v1.4.0
LockHTML v1.4.0
Split the key. Shamir secret sharing, so any k of n holders can open a file — and no fewer.
Highlights
- 🧩 Shamir secret sharing — a random 32-byte key seed is split with Shamir's scheme over GF(256); any k of n shares reconstruct it, and k−1 reveal nothing.
- 📄 Shares ship separately —
lock --shares 5:3writes a.shares.txtto distribute however you like. The locked file alone cannot be opened. - 🗂️ Works with the streaming container too, and combines with a PIN and a keyfile.
- 🖥️ GUI support — a share-split field on the lock tab, and a shares box on unlock.
- 🔒 Hardened reconstruction — duplicate indices, index 0 and ragged shares are rejected, and only the first k shares are used so a stray extra share can't corrupt a valid set.
Usage
lockhtml lock report.html --shares 5:3 # -> report.locked.html + report.locked.shares.txt
lockhtml unlock report.locked.html --shares some-three.txt
lockhtml unlock report.locked.html --share <b64> --share <b64> --share <b64>
# large file, split across holders
lockhtml lock big.mp4 --shares 5:3 --streamLive GUI
👉 https://drsamonline.github.io/lockhtml/
Verification
74 unit tests — including GF(256) checked against the FIPS-197 AES vector and all 255 inverses — plus a 30-check headless-Chrome run across all four modes (embedded, streaming, recipient, shares) and 9 cross-implementation checks where the browser locks files the Python core then opens. The Python and JavaScript share arithmetic are proven to agree. All of it runs in CI.
Security note
Shares move the secret, not the trust: any k holders can reconstruct the seed, and a reconstructed seed cannot be revoked — re-lock with a fresh split if shares are exposed. See SECURITY.md and the self-audit in docs/AUDIT.md.
LockHTML v1.3.0
LockHTML v1.3.0
Lock a file to a person, not a password. X25519 recipient mode.
Highlights
- 🔑 Recipient mode (X25519) — lock a file to someone's public key so only the holder of the matching private key can open it. No shared secret is ever exchanged.
- 🧬 Hybrid encryption, done properly: a fresh ephemeral keypair per file, the ECDH shared secret fed through HKDF-SHA256, then AES-256-GCM. Because the secret is already 256 bits of entropy, no slow KDF is applied on top.
- ➕ Two factors if you want them — layer a PIN (and a keyfile pepper) on top of the recipient key.
- 🗂️ Works with the streaming container too, so a multi-gigabyte file can be sent to one specific person.
- 🖥️ GUI support — a recipient public-key field on the lock tab, and a private-key prompt on unlock.
- 🔒 The decoy PIN is rejected in recipient mode rather than silently ignored.
Usage
lockhtml keygen --type x25519 -o alice.pem # writes alice.pem (keep) + alice.pub (hand out)
lockhtml lock report.html --to alice.pub
lockhtml unlock report.locked.html --key alice.pem
# two factors, and for large files
lockhtml lock big.mp4 --to alice.pub --pin 4821 --streamLive GUI
👉 https://drsamonline.github.io/lockhtml/
Verification
58 unit tests, plus a real headless-Chrome end-to-end run of 25 checks covering the embedded container, the streaming container and recipient mode — including wrong key, missing key, PIN-required, tampered sidecar and truncated container. Files locked by the browser are decrypted by the Python core and vice versa (6 cross-implementation checks). All of it runs in CI.
Security note
Recipient mode moves the secret, not the trust: whoever holds the private key can open every file locked to it, and a leaked private key cannot be revoked — rotate it. See SECURITY.md and the self-audit in docs/AUDIT.md.
LockHTML v1.2.0
LockHTML v1.2.0
Large files, solved. A chunked streaming container that works at constant memory.
Highlights
- 🗂️ Streaming container (
--stream) — the ciphertext goes into a sidecar.bin, encrypted in fixed-size chunks, so reading and decryption cost ~O(chunk size) instead of O(file size). Multi-gigabyte files now work. - 🧮 Why it matters: the embedded container holds ~3.7× the file size in memory and breaks around 250–300 MB. The streaming sidecar is raw ciphertext (no base64), so it is only ~16 bytes per chunk larger than the original.
- 🔒 Sound chunking, not naive chunking: per-chunk nonces (
base_iv || uint32_be(index)), the chunk index bound as Additional Authenticated Data, and a whole-file HMAC over the metadata and every chunk tag — so reordering, duplication, truncation and metadata tampering are all detected. - 🖥️ GUI support — tick “Streaming bundle (large files)” to create one; the unlock tab prompts for the
.bin. - 📖
docs/DESIGN.md— the size limits, the memory arithmetic and the two containers, written out in full.
Neither container touches the network. “Streaming” here means memory, not the internet — you can lock a 20 GB file on a plane with Wi-Fi off.
Usage
# Large file -> viewer + sidecar
lockhtml lock holiday.mp4 --pin 4821 --stream
lockhtml unlock holiday.locked.html --pin 4821 --data holiday.locked.bin -o holiday.mp4| Embedded (default) | Streaming (--stream) |
|
|---|---|---|
| Files the recipient gets | 1 | 2 (.html + .bin) |
| Practical size ceiling | ~250–300 MB | Limited by disk |
| Memory to encrypt/decrypt | ~3.7× file size | ~O(chunk size) |
Live GUI
👉 https://drsamonline.github.io/lockhtml/
Verification
43 unit tests, plus an end-to-end run in a real headless Chrome covering both containers — Argon2id/PBKDF2, keyfiles, signatures, watermark, decoy PIN, view limits, and for streaming: multi-chunk, tampered sidecar, truncated container and missing data file. Files locked by the browser are decrypted by the Python core and vice versa. All of this runs in CI.
Security note
The PIN (and keyfile) provide real encryption. The streaming MAC is keyed by the PIN-derived key, so it protects integrity but not authenticity — only an Ed25519 signature proves who produced the file. View limits, expiry, watermarking and one-time view remain browser-enforced deterrents. See SECURITY.md and docs/DESIGN.md.
LockHTML v1.1.0
LockHTML v1.1.0
Now with Argon2id, keyfiles, signing, duress PINs, view limits, watermarking — and any file type.
Highlights
- 🔐 Argon2id key derivation (memory-hard) — now the default. PBKDF2-SHA256 remains available.
- 🧂 Keyfile second factor — a pepper mixed into the key, so the PIN alone is not enough.
- ✍️ Ed25519 signing (
lockhtml keygen,--sign) — verified in the browser before the content is shown. - 🎭 Duress PIN (
--decoy-pin) — a second PIN that opens a harmless decoy document. - 👁️ View limits —
--one-time,--max-views N,--not-before,--expires. - 💧 Watermarking (
--watermark) — overlay recipient-specific text to trace a leak. - 📦 Any file type — HTML renders inline; PDFs, images and anything else download.
- 🌐 PWA + offline — the hosted GUI installs and works offline.
- 🌍 Localised viewer — English, हिन्दी, ગુજરાતી.
- 🧾 Format v2 with a written spec (docs/FORMAT.md); v1 files still open.
- ⚡ Minified, build-verified assets — the viewer template is ~34% smaller.
Install
pip install git+https://github.com/drsamonline/lockhtml
lockhtml lock report.pdf --pin 4821 --one-time --watermark "For A. Sharma"Wheel and sdist are attached below.
Live GUI
👉 https://drsamonline.github.io/lockhtml/
Security note
The PIN (and keyfile) provide real encryption — the file is unreadable without them, even from the raw source. View limits, expiry, watermarking and one-time view are browser-enforced deterrents, not server guarantees. See SECURITY.md and the self-audit in docs/AUDIT.md.
Compatibility
Files are interchangeable between the Python CLI and the JavaScript GUI. Verified in CI and by a real headless-browser end-to-end test on Linux, Windows and macOS (Python 3.9–3.12).
LockHTML v1.0.0
LockHTML v1.0.0
Turn any HTML file into a PIN-protected document — optionally viewable only once.
Highlights
- 🔐 AES-256-GCM encryption with a PBKDF2-SHA256 derived key (250,000 iterations by default).
- 👁️ One-time view — expire a document after its first successful open.
- 🖥️ Cross-platform CLI —
lockhtml lock,unlock,guion Windows, Linux and macOS. - 🌐 Browser GUI — a single self-contained HTML file, no install, runs on any OS.
- 📦 Self-contained output — locked files need no server, no app, no internet.
- 🔁 Reversible — recover the original with the PIN via
lockhtml unlock.
Install
pip install lockhtml
lockhtml lock report.html --pin 4821 --one-timeLive GUI
👉 https://drsamonline.github.io/lockhtml/
Security note
The PIN provides real encryption — the file is unreadable without it, even from the raw source. "One-time view" is a browser-enforced deterrent, not a server guarantee: a determined recipient can still copy the file or screenshot the decrypted page. See SECURITY.md.
Compatibility
Files are interchangeable between the Python CLI and the JavaScript GUI. Verified by CI on Linux, Windows and macOS with Python 3.9–3.12.