Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ jobs:
permissions:
contents: read
id-token: write
# For the build attestation below. Write access to attestations only;
# nothing in this job can touch the repository or the release.
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -268,6 +271,21 @@ jobs:
cd "keymaker-$TAG"
sha256sum -c SHA256SUMS

# GitHub's own build attestation, beside the Sigstore signature and not
# in place of it. The signature over SHA256SUMS is what docs/VERIFYING.md
# teaches and what the notes print. This is a second, independent
# statement about the same bytes — SLSA provenance, signed through
# GitHub's Sigstore instance by this run — that `gh attestation verify`
# checks with nothing but GitHub's CLI installed. The subjects are what a
# user downloads: the tarball and the manifest inside it. Attested after
# the archive has been unpacked and checked above, so a broken package is
# never attested.
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
keymaker-${{ github.ref_name }}.tar.gz
out/SHA256SUMS

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-assets
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@

## Unreleased

One format revision, opt-in: **KEYM v4** (`docs/FORMAT-V4-DESIGN.md`), a delta
on v3 whose only change is a padded payload. Nothing the app writes today
changes, and every v2 and v3 container reads exactly as before.

### Added
- **KEYM v4: the container's length no longer states the plaintext's.** v2 §8
and v3 §7 both deferred this. The payload now seals a stream of an 8-byte
length prefix, the plaintext and zeros, padded to a bucket: 256 bytes for
anything up to 248, and Padmé above that, so a 12-word seed, a 24-word seed
and a password give the same file. Header, slots, MAC and chunking are
v3's; the version byte sits inside every AAD, so a relabelled container
opens in neither direction. Specified first, implemented in `keym2.py` from
the spec (`encrypt --pad`, and `inspect` says "padded bytes" rather than
claiming a plaintext length), then TypeScript; byte-identical across both,
three ciphers, both KDFs and every stream boundary; a published vector held
in both; seven frozen fixtures; RECOVERY.md's commands claim v4 and
`recovery_test.py` executes them against v4 containers the app wrote. v4
is written on request only, so the paper vault does not grow by default;
the self-extracting page keeps its v3 container and its writer refuses v4.
- **"Hide the size of what is inside"**, under Advanced on the Encrypt tab, is
how the app writes v4. Off by default, with the cost stated beside it: up
to 248 bytes plus under 7%, more symbols on paper, and older readers
cannot open it. A browser test shows a one-byte and a 200-byte secret
sealing to the same length.
- **Re-seal an old backup.** With a v2, v1 or IttyBitz backup open, the
Decrypt tab says its list of ways in is not authenticated and offers to
re-seal it. The button carries the recovered text to the Encrypt tab (a
recovered file, already downloaded, is asked for), and the ordinary seal
writes today's format with a password the owner types. Shares and
passkeys are not carried over, and the notice says so. Not a one-click
re-encrypt on purpose: the password is cleared on success and the
plaintext erased, and a silent re-seal would have to keep both.
- **Releases carry a GitHub build attestation** on the tarball and on
`SHA256SUMS`, beside the Sigstore signature and not in place of it, so
`gh attestation verify` checks the same bytes with nothing but GitHub's CLI.
VERIFYING.md says what it asserts and why `--signer-workflow` is not
optional.

## Keymaker v2.3.0

One additive format change: a new slot type, `0x03` (FORMAT-V2-DESIGN §4.8),
which a container carries only when its owner chooses it. Every container
v2.2.0 wrote reads exactly as before, the existing fixture corpus is unchanged
Expand Down
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,14 +466,19 @@ a way of not saying anything:
- **Someone looking at your screen.** Secret fields blur by default and reveal
toggles exist for that reason, but a shoulder, a webcam and a screen-recorder
all defeat it.
- **How long your plaintext is.** The container is not padded, and the final
chunk is not padded either, so its length reveals the plaintext's length
*exactly* — overhead is a constant, and one more byte in gives one more byte
out. Not "to within a chunk": byte for byte. If the mere *size* of
what you are protecting is sensitive — which document, which of two possible
answers — that leaks regardless of the cipher. This is stated in
[§8 of the format design](docs/FORMAT-V2-DESIGN.md) and a padding scheme is
deliberately not in v2: it is its own design with its own trade-offs.
- **How long your plaintext is.** A v3 container (what the app writes today)
is not padded, and the final chunk is not padded either, so its length
reveals the plaintext's length *exactly* — overhead is a constant, and one
more byte in gives one more byte out. Not "to within a chunk": byte for
byte. If the mere *size* of what you are protecting is sensitive — which
document, which of two possible answers — that leaks regardless of the
cipher. This is stated in [§8 of the format design](docs/FORMAT-V2-DESIGN.md).
[KEYM v4](docs/FORMAT-V4-DESIGN.md) pads the payload so the length says only
which bucket the plaintext is in, and everything up to 248 bytes is the same
size; `keym2.py encrypt --pad` and the app's *Hide the size of what is inside*
switch write it, and it is opt-in because it costs bytes, and on paper bytes
are symbols. Padding is not deniability: the file
is still plainly a backup.
- **A weak password.** Argon2id makes guessing expensive; it cannot make a
guessable password unguessable.
- **Forgetting the password.** There is no reset, no recovery email and nobody
Expand Down Expand Up @@ -657,8 +662,8 @@ process list to every other account on the machine while the KDF ran. Prefer
the interactive prompt, or `--shares-from` with a file only you can read.

**Two scripts, because there are two container generations.** The app writes
**KEYM v3**, and `keym2.py` reads v3 and v2 alike — one command, whichever year
your backup is from. Backups older than that are **v1** and need `keym.py`;
**KEYM v3**, and `keym2.py` reads v4, v3 and v2 alike — one command, whichever
year your backup is from. Backups older than that are **v1** and need `keym.py`;
neither script reads the other's format, and the one that refuses is telling you
which you have.
[docs/RECOVERY.md](docs/RECOVERY.md) is the printable procedure, and
Expand Down Expand Up @@ -732,6 +737,7 @@ every KDF and cipher combination, and takes a few minutes.
| [`docs/FORMAT.md`](docs/FORMAT.md) | Normative KEYM v1 byte-level specification |
| [`docs/FORMAT-V2-DESIGN.md`](docs/FORMAT-V2-DESIGN.md) | Normative KEYM v2 specification — still read, no longer written |
| [`docs/FORMAT-V3-DESIGN.md`](docs/FORMAT-V3-DESIGN.md) | Normative KEYM v3 specification, a delta on v2 — the format the app writes today |
| [`docs/FORMAT-V4-DESIGN.md`](docs/FORMAT-V4-DESIGN.md) | Normative KEYM v4 specification, a delta on v3 — a padded payload, written on request |
| [`docs/FORMAT-AUDIO-STEGO.md`](docs/FORMAT-AUDIO-STEGO.md) | The KAUD1 encrypted audio carrier layout: packing a container into audio, with a diagram |
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Phased plan: what ships next, and what was cut |
| [`docs/VERIFYING.md`](docs/VERIFYING.md) | Checking that the site you loaded is the code you read |
Expand Down
18 changes: 11 additions & 7 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ Out of scope / known limitations:
- **JavaScript memory hygiene is best-effort.** `secureErase` zero-fills
buffers, but the JS engine/GC may retain copies of secrets. WebCrypto keys
are non-extractable where the API allows.
- **Deniability / traffic analysis.** Containers are not padded, so their
length reveals the plaintext's length exactly: the overhead is fixed for a
given format and settings, so container length determines plaintext length
byte for byte (FORMAT-V2-DESIGN §8). If the *size* of what is being protected is
itself sensitive, the cipher does not help. `docs/FORMAT-V2-DESIGN.md` §8
records why a padding scheme was deliberately left out of v2 rather than
bundled into it.
- **Deniability / traffic analysis.** v1, v2 and v3 containers are not padded,
so their length reveals the plaintext's length exactly: the overhead is fixed
for a given format and settings, so container length determines plaintext
length byte for byte (FORMAT-V2-DESIGN §8). A **v4** container pads its
payload (`docs/FORMAT-V4-DESIGN.md`): its length reveals only which bucket
the plaintext falls in, and every plaintext up to 248 bytes gives the same
length. v4 is written on request, not by default, and padding is not
deniability: the container is still plainly a Keymaker container, its cipher
and slots are still readable, and a plaintext just over a bucket boundary is
still on the far side of it. If the *size* of what is being protected is
itself sensitive and the backup is not v4, the cipher does not help.
- **Password strength.** Weak passwords undermine any KDF. Argon2id
(memory-hard) is the default recommendation, but cannot fix a weak
password.
Expand Down
2 changes: 2 additions & 0 deletions docs/FORMAT-V2-DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1990,6 +1990,8 @@ older reader, and `--capacity` still overrides the size.
standing between a mistake and a permanent one. That decision is now settled
rather than open — the format is frozen (§9), so padding is a v3 question.
README.md states the leak plainly rather than leaving it to be discovered.
*Answered by [FORMAT-V4-DESIGN.md](FORMAT-V4-DESIGN.md): v4 pads the payload,
on request. v2 and v3 containers are exactly as this bullet says.*

- **The slot table is authenticated slot-by-slot, not as a whole.** §5.3 keeps
`slot_count` out of every AAD so a table stays editable by someone holding one
Expand Down
3 changes: 3 additions & 0 deletions docs/FORMAT-V3-DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ fix a gap v2 never claimed to close.
it with an authentication fix would make both harder to review; that is the
same reasoning v2 used to defer it in the first place. It stays open, and
bumping the version here does not close it.
*Closed by [FORMAT-V4-DESIGN.md](FORMAT-V4-DESIGN.md), a delta on this
document that changes the payload and nothing else. A v3 container's length
still says exactly what this bullet says.*

---

Expand Down
Loading
Loading