Repository navigation
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.