Skip to content

Land #213, #215, #216, #217 and #218 on main - #219

Merged
404SecNotFound merged 14 commits into
mainfrom
claude/serene-carson-0739mv
Sep 26, 2026
Merged

404SecNotFound merged 14 commits into
mainfrom
claude/serene-carson-0739mv

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

Why this PR exists

#213, #215, #216, #217 and #218 were merged this morning, but each merged into the branch below it in the stack rather than into main, because their bases were never retargeted after #212 merged. main has #212 and #214 and nothing else from the stack. Checked against main's tree, not the merge status: none of the five heads is an ancestor of main.

This branch is the union. It merges claude/serene-carson-0739mv-padding (which carries #213, #215, #216, #217) and claude/serene-carson-0739mv-split (which carries #218) into the designated branch. All five heads are ancestors of this branch's tip, and it merges into main with no conflicts.

What it carries

  1. Bring the roadmap up to date and prepare v2.3.0 #213: roadmap brought up to date, package.json to 2.3.0, ## Unreleased closed as ## Keymaker v2.3.0.
  2. KEYM v4: a padded payload, from spec to parity #215: KEYM v4, a padded payload, from spec to parity.
  3. Attest the release tarball and manifest with GitHub build provenance #216: GitHub build attestation on the release tarball and manifest.
  4. Split the encryptor tool into per-tab modules #217: the encryptor tool split into per-tab modules.
  5. Hide the size of what is inside, and re-seal an old backup #218: the "Hide the size of what is inside" switch and the re-seal offer.

The one hand-made change

CHANGELOG.md conflicted where #216 and #218 both added entries under Unreleased. Resolved by keeping both sets of entries, and dropping the sentence "a switch to write one is separate" that the split side had already removed, since #218 adds that switch. No other file needed a hand merge.

Gates on this tree

npm run typecheck, test:release-notes, test:release-gate, test:secret-erase-core and test:keym2-dispatch pass. Each constituent PR passed all 15 CI checks on its own head; this PR's CI run covers the union.

Before tagging

The changelog on this branch has ## Unreleased (v4, attestation, the switch, the re-seal) above ## Keymaker v2.3.0. A v2.3.0 tag on main after this merges would ship v4 without its notes. Either fold the Unreleased section into the v2.3.0 heading before tagging, or tag as v2.4.0. Decide that before the tag, not after.

Merging

Merge this into main directly. Nothing else is stacked on it.


Generated by Claude Code

The roadmap's sequencing table stopped at 4.5. Everything #210 landed is now
a row: the end-to-end review fixes, the FORMAT-V2-DESIGN §4.8
password-and-shares slot, the paper vault's second pass (set code, presets,
version-25 symbols, self-contained strips, the printout check), photo and
live camera scanning, and RECOVERY.md being executed by recovery_test.py.
Phase 8 now says what it waits on: the owner, after the next tag.

The Cut table's steganography row records why the audio carrier stays: it is
documented as a carrier, not steganography, and claims nothing that row cuts.

TEN-X-PLAN.md said Bet 1 (camera scanning) was on hold in three places. It
shipped in 22c7cdf and 161e919, and the decoder decision that held it was
taken as BarcodeDetector where present and pinned jsqr where not. The plan
now says so, and says what from the sketch did not ship.

Release prep: package.json and the lockfile go to 2.3.0, a minor version
because the changelog carries an additive format change (slot type 0x03),
and the changelog's Unreleased section becomes the v2.3.0 heading.
SECURITY.md's supported-versions table names only "latest release" and
needs no change.

Gates run on this tree: test:release-notes, test:release-gate and
test:release-recipe all pass. The tag itself is the owner's to push:
git tag -a v2.3.0 and git push origin v2.3.0.
v2 §8 and v3 §7 both deferred the length leak: a container's length
determined its plaintext's byte for byte, so a backup's size said whether
it held a password, a 12-word seed or a 24-word one. docs/FORMAT-V4-DESIGN.md
is the padding scheme on its own, as both deferrals asked for.

v4 is a delta on v3 that changes one thing. The payload seals a stream of
an 8-byte length prefix, the plaintext and zeros, padded to 256 bytes for
anything up to 248 and by Padmé above that, so the overhead stays under 7%
and the length reveals only the bucket. Header, container_id, slot table,
MAC, chunking, nonces and AAD are v3's, and the version byte sits inside
every AAD, so a relabelled container opens in neither direction. The reader
verifies the prefix, the bucket and every padding byte, and every failure
is the generic one.

Written in the house order. The section first; reference/keym2.py from the
section alone (encrypt --pad, inspect reporting padded bytes rather than a
plaintext length, and a self-test section of 71 checks including the
published vector); then the TypeScript core, the app's dispatch, the
inspector and the self-extract profile; then seven frozen fixtures, six at
the floor and one past it; then crosstest2.py comparing the emitted bytes
across both KDFs, three ciphers and every stream boundary, holding both to
the vector, and requiring the TypeScript to refuse every stream the
reference refuses. RECOVERY.md's commands claim v4 and recovery_test.py
runs them against v4 containers the app wrote.

Writers MAY emit v4; the default stays v3, because the cost lands on the
writer's medium and on paper bytes are symbols. The self-extracting page
keeps its v3 container and its writer refuses v4. The app opens a v4 backup
today; the switch to write one is separate.

Negative controls on the reference: the zero check removed fails the three
step-5 checks, the bucket check removed fails the two step-4 checks, and a
512-byte floor fails the pinned table and the vector.
…ance

Beside the Sigstore signature, not in place of it. The signature over
SHA256SUMS is what docs/VERIFYING.md teaches and what the notes print; the
attestation is a second, independent statement about the same bytes, SLSA
provenance signed through GitHub's Sigstore instance by the same run, that
`gh attestation verify` checks with nothing but GitHub's CLI.

The sign job gains `attestations: write` and one pinned step, placed after
the unpacked archive has passed the sha256sum check so a broken package is
never attested. VERIFYING.md documents the command and why --signer-workflow
is not optional, and the changelog records it. The three release gates pass.
encryptor-tool.tsx had grown to 6,459 lines. Move its code, unchanged, into
src/components/encryptor/: shared.ts (types, constants, pure helpers),
shared-ui.tsx (LockWarning, FileSelector, RevealableQr, InfoTip),
use-encryptor-state.ts (the state and handlers more than one tab reads or
writes, exposed through EncryptorContext), secret-form.tsx (the Encrypt and
Decrypt tabs, which turn out to be one shared render function rather than
two), workspace-tab.tsx, recovery-tab.tsx, shares-dialog.tsx and
recovery-kit-dialog.tsx.

encryptor-tool.tsx stays the entry point, now composing these modules; it
drops to a few hundred lines of Tabs/header/footer chrome. No behaviour
change: every string, id, data-testid, and handler moved rather than being
rewritten.
KeymakerOptions gains `padded`, off by default. encryptContainer and
encryptContainerWithSharesRequired pass it to the keym-v2 writer as the
version, so the worker and its no-worker fallback, which both call them,
write the same thing. The dispatch test pins it: padded writes v4 and
opens through decryptData, and without it the app still writes v3.
Two things the format work left for the app, on the split's modules.

The switch. v4 §6 makes padding the owner's choice and does not move the
default, because the cost lands on paper: more bytes are more symbols. So
"Hide the size of what is inside" sits under Advanced on the Encrypt tab,
off until turned on, with the cost beside it, and passes `padded` to the
writer. The browser spec shows a one-byte and a 200-byte secret sealing to
the same length as v4, a padded backup opening here with the padding gone,
and the default unchanged.

The re-seal offer. v3 §6: moving a v2 backup to v3 means decrypting and
re-encrypting it, and it is the owner's decision. The Decrypt tab now makes
the offer with the backup open, for v2, v1 and IttyBitz files, and says what
is not carried over. It is not a one-click re-encrypt: the password is
cleared on success (U13) and the plaintext erased, so the button carries the
recovered text to the Encrypt tab as its input (a file, downloaded and not
kept, is asked for) and the ordinary seal writes today's format with a
password the owner types. A v3 or v4 backup gets no offer.

Both specs were written first and failed against the tree without the UI.
…t' into claude/serene-carson-0739mv

# Conflicts:
#	CHANGELOG.md
@404SecNotFound
404SecNotFound merged commit 4a2d29d into main Sep 26, 2026
15 checks passed
@404SecNotFound
404SecNotFound deleted the claude/serene-carson-0739mv branch September 26, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant