KEYM v4: a padded payload, from spec to parity - #215
Merged
404SecNotFound merged 1 commit intoSep 26, 2026
Merged
404SecNotFound merged 1 commit into
404SecNotFound merged 1 commit into
Conversation
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
merged commit Sep 26, 2026
01c4166
into
claude/serene-carson-0739mv-release
15 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
keym2.py encrypt --padwrites 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.Order of work, as CLAUDE.md requires
reference/keym2.pyfrom the spec alone:--pad,inspectreports "padded bytes" rather than claiming a plaintext length,_seal_streamsplit out so the self-test can seal streams no conforming writer produces. Self-test 674 to 680 checks; 71 lines are v4.keym-v2.ts(constants,keym2PaddedLen, pad/unpad, the writer over the stream, the reader's unpad after the final chunk),keymaker-crypto.tsdispatch, the inspector, the self-extract profile, the bridge.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.recovery_test.pyexecutes 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
test:keym2test:conformance2test:recoverytest:keymakertest:keym2-dispatchtest:fuzz3,test:secret-erase-core,test:release-notesnpm run typechecknpm run build, then Chromium--workers=2main)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.