Skip to content

KEYM v4: a padded payload, from spec to parity - #215

Merged
404SecNotFound merged 1 commit into
claude/serene-carson-0739mv-releasefrom
claude/serene-carson-0739mv-padding
Sep 26, 2026
Merged

404SecNotFound merged 1 commit into
claude/serene-carson-0739mv-releasefrom
claude/serene-carson-0739mv-padding

Conversation

@404SecNotFound

@404SecNotFound 404SecNotFound commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #213 (shares CHANGELOG.md). Closes the one documented open format weakness: v2 §8 and v3 §7 both deferred it.

What v4 is

docs/FORMAT-V4-DESIGN.md, 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 a bucket: 256 bytes for anything up to 248, Padmé above that (under 7% overhead, under 4% from 64 KiB). Header, container_id, slot table, MAC, chunking, nonces and AAD are v3's. 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.

Decisions, stated for review.

  • A version byte, not a flag bit. v2 §3.3: "new meanings get a version byte, not a reclaimed reserved bit."
  • A 256-byte floor under Padmé, because Padmé alone leaves a 12-word seed, a 24-word seed and a password distinguishable, and 256 is the largest floor that keeps a one-slot backup on one printed symbol.
  • Writers MAY emit v4; the default stays v3. The cost lands on paper (more bytes are more symbols), so it is a choice to put in front of the owner. keym2.py encrypt --pad writes it. The app opens v4 today; the switch to write one is a separate PR after the encryptor-tool split, so it lands on the split's modules.
  • The self-extracting page keeps its v3 container and its writer refuses v4 (v4 §6).

Order of work, as CLAUDE.md requires

  1. The specification, with a published vector.
  2. reference/keym2.py from the spec alone: --pad, inspect reports "padded bytes" rather than claiming a plaintext length, _seal_stream split out so the self-test can seal streams no conforming writer produces. Self-test 674 to 680 checks; 71 lines are v4.
  3. TypeScript: keym-v2.ts (constants, keym2PaddedLen, pad/unpad, the writer over the stream, the reader's unpad after the final chunk), keymaker-crypto.ts dispatch, the inspector, the self-extract profile, the bridge.
  4. Seven frozen fixtures: six at the floor, one 300-byte plaintext past it. Both corpus counts updated (35 to 42; the parity gate's 29 to 36).
  5. crosstest2.py: byte equality over both KDFs and three ciphers, eight stream boundaries with the expected length computed independently, the published vector held in both, py to js and js to py round trips at four lengths per cipher, and the TypeScript required to refuse every stream the reference refuses.
  6. RECOVERY.md's commands claim v4 and recovery_test.py executes them against v4 containers the app wrote (185 checks). README, SECURITY.md and the v2 and v3 specs point at v4.

Gates run on this tree

gate result
test:keym2 680 passed
test:conformance2 passed, 54 v4 lines
test:recovery 185 passed
test:keymaker passed (42-fixture corpus, new v4 section)
test:keym2-dispatch 73 passed
test:fuzz3, test:secret-erase-core, test:release-notes passed
npm run typecheck clean
npm run build, then Chromium --workers=2 304 passed, 5 skipped, exit 0 (same counts as main)

Negative controls, each confirmed to compile first

Reference: the zero check removed fails the three step-5 checks; the bucket check removed fails the two step-4 checks; a 512-byte floor fails the pinned table and the vector.

TypeScript, through the parity gate: the zero check removed fails exactly "step 5: a non-zero padding byte: both refuse (python refused, js opened)"; a 512-byte floor fails every stream-boundary comparison (js 681 bytes against 425) and the vector.

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.
@404SecNotFound
404SecNotFound merged commit 01c4166 into claude/serene-carson-0739mv-release Sep 26, 2026
15 checks passed
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