Skip to content

Releases: drsamonline/lockhtml

Latest app build

Choose a tag to compare

@github-actions github-actions released this 22 Oct 11:03

Automated build from df6f004. Assets: lockhtml-portable.exe, lockhtml.apk.

LockHTML v2.1.0

Choose a tag to compare

@drsamonline drsamonline released this 21 Oct 02:05

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

The 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
  • X25519 recipients and identities (Bech32 age1… / 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

  1. The official vector. testdata/example.age and its identity from FiloSottile/age are embedded in the test suite, so CI decrypts a real reference-produced age file with no network access.
  2. Bidirectional interop against the age v1.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

👉 https://drsamonline.github.io/lockhtml/

LockHTML v2.0.1

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 17:16

LockHTML v2.0.1

A post-release audit pass. Every fix here was found by running the code, not reading it.

Fixed

  • gui.sh did not work at all. It exec'd lockhtml.sh, which has no executable bit, so it died with Permission denied. It now runs the launcher through sh.
  • 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.bat broke on any Python path containing a space (C:\Program Files\…) — %PY% was used unquoted.
  • unlock()/decrypt() on a streaming payload raised a bare KeyError; they now say the sidecar is needed.

Changed

  • --auto-kdf now says so instead of silently doing nothing when --kdf pbkdf2 is also given; mismatched --hidden-pin/--hidden-file counts print a note.
  • make minify is now an alias for make build. Minifying an already-minified index.html is 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) and gui.sh, instead of only syntax-checking them — a syntax check is exactly what missed the gui.sh bug.
  • 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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 16:06

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 calibrate times one Argon2id pass on this machine and picks the strongest time cost that still unlocks in about half a second. --auto-kdf stores 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 DIR on the CLI, and a folder picker plus directory drag-and-drop in the GUI.
  • 🚀 Launcher scripts — lockhtml.sh / lockhtml.bat and gui.sh / gui.bat run 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.yml workflow 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 ./locked

Live 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

  • age interop 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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 14:42

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:3 writes a .shares.txt to 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 --stream

Live 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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 14:04

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 --stream

Live 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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 13:25

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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 11:46

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

Choose a tag to compare

@drsamonline drsamonline released this 20 Oct 08:46

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, gui on 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-time

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