Skip to content

Key object rework: the traits lose their constructors, every mode gets a key, and workers borrow the key - #172

Open
dghgit wants to merge 55 commits into
feature/key-reworkfrom
feature/key-rework-memory
Open

dghgit wants to merge 55 commits into
feature/key-reworkfrom
feature/key-rework-memory

Conversation

@dghgit

@dghgit dghgit commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Issue Link

No issue filed. This continues the key object rework prototype in #170, which it is based on, and takes it to the end of the document's plan: the trait constructors go, and the workers borrow their keys.

Summary

The symmetric cipher and MAC traits lose their constructors and key-taking one-shots; every mode, Ascon-AEAD128 and KMAC get key objects; and a worker borrows the key it is made from, so creating an encryptor, decryptor, generator or verifier copies the per-message state and nothing else.

Description

  • The traits describe a worker that exists. SymmetricCipher*, AEADCipher*, BlockCipher*, StreamCipher* and MAC carry only streaming methods and length helpers. Construction is a key's create_*, and the one-shots are provided on the role traits in core::symmetric_key over them. KEY_LEN is on the role and mode traits, never on a worker.
  • Every mode has a key path. CBC<X>, CFB, CFB8, CTR<N, B>, hazmat::ECB<X>, CCM<N, T>, CCM_Packet<N, T, A, D>, GCM<T>, GMAC<T> and CMAC<B> are mode types a key is bound to; AsconAEAD128Key and KMAC128Key / KMAC256Key join AESKey<P, M> and HmacKey<H>. The MAC factory is a key enum that hands out a worker enum. The CLI, the test framework (test_key runners and the construction suites) and the memory benches run on keys.
  • Workers borrow the key. The role and mode traits hand out workers through type Encryptor<'k>, and create_encryptor takes &'k self. Every cipher-crate worker holds &'k to the mode's key state; HMAC<'k, H> holds &'k HmacKey<H>. Ascon and KMAC keep owned workers, since their working state is the key-dependent sponge. A key must outlive its workers, which the compiler enforces.
  • The permutation trait splits. BlockPermutation<BLOCK_LEN> carries the block methods and ElectronicCodeBook<KEY_LEN, BLOCK_LEN> adds new, so a mode's Mode<P> impl never names a key length and the GCM, GMAC and CMAC impls are written once in cipher. KEY_LEN stays on the mode traits' worker halves as the tie between a key's length and its permutation's, which is what keeps a key object's role impls to the one length.
  • Resume takes the key object. SuspendableKeyed<'k, N> and SuspendableComponent<'k> take the key by &'k: a cipher-mode worker's Key is the mode's KeyState, reached through core::hazmat::ModeKey::state; HMAC's is HmacKey; Ascon's is AsconAEAD128Key; HKDF's stays the salt. No suspended layout changed.
  • One rustc limitation shaped the code. The mode traits' GAT bound is where P: 'k, not where Self::KeyState: 'k: a generic impl whose key state is a struct over P is rejected under the latter (E0276). CLAUDE.md records this with the rest of the mechanics.
  • Keys compare in constant time. Lifted from SymmetricCipherKey: dedicated key types per algorithm #167's SymmetricCipherKey: PartialEq + Eq: AESKey<P, M>, its mode states (AESInternal, GcmKeyState, CmacKeyState) and AsconAEAD128Key implement PartialEq / Eq over the expanded state, every byte inspected and the fields joined with &, so two keys can be compared without the material being reachable. AESKey's impl is bounded on the state, not on Mode::KeyState, so a permutation without the impl still has keys.
  • Dropped with no key-path twin, each named in its commit: new_allow_weak_key on cipher-based MACs, Gmac::new / new_nonce and KeyStream::new (the last material routes), GCM's verify-before-decrypt one-shot override, the ECB RNG-panic tests.

Measured

Struct sizes, and massif for the stack row; HMAC from size_of:

Worker before after
AES-256-GCM encryptor 424 B 176 B
CCM worker, any AES key length 264 / 296 / 328 B 96 B
CCM_Packet encryptor, AAD 64 344 B 176 B
CMAC worker, any AES key length 248 / 280 / 312 B 48 B
HMAC-SHA-256 / SHA-512 / SHA3 worker 224 / 416 / 880 B 120 / 216 / 448 B
CCM in-place peak stack, 16 KiB frame 19 328 B 18 608 B

The Ascon stack table is re-measured on the key path with the frame-remark method the bench documents.

Scope and Risk

  • Packages: core, utils, cipher, aes, ascon, sha2, sha3, sm3, hmac, hkdf, factory, core-test-framework, cli, mem_usage_benches, plus CLAUDE.md and QUALITY_AND_STYLE.md. 148 files, +8855 / -8976 against feature/key-rework, in 19 commits.
  • Runtime behaviour: the public API changes shape throughout (constructors leave the traits, workers gain a lifetime, aliases change), as the alpha status allows. Every known-answer suite runs through the key path and passes; no vector, suspended layout or output changed.
  • Regression likelihood: low on outputs, which the vectors pin; the risk is in borrow-shaped API ergonomics, which the tests exercise in every form a caller would write.
  • Worst case: a resume accepting a key state it should not. Resume is typed by the key state now, so a key of another mode cannot reach it except through the hazmat accessor, which is documented as such.

Review

All issues in report REVIEW-2026-10-30-PR172-key-rework-memory-mythos.md now fixed.

All issues in report REVIEW-2026-10-30-PR172-correctness-mythos.md now fixed, except C4 (GCM resume accepting a counter below 2), which goes with the cipher workers' resume path under #173.

The one leftover in report REVIEW-2026-10-31-PR172-fix-verification-mythos.md (the KMAC commands' key construction panicking on a refused tag length) now fixed.

The items of report REVIEW-2026-10-31-PR170-correctness-mythos.md that were open on this head now fixed here: P3, P4 and P7 (GCM byte-count bounds and refusing before the copy; CCM packet resume owing no AAD) and P13 (the bare key-state and permutation constructors are crate-private, with NonceMacMode in core::hazmat for GMAC). P5 stays with #173 as C4 above; P11 was a #170 description fix.

Validation

cargo test --workspace                       # 1196 tests
cargo test --workspace --all-targets --no-run   # benches and mem_usage_benches build
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
cargo fmt --all --check
bash cli/tests/test_all.sh                   # 10/10
cargo run --release -p mem_usage_benches --bin bench_ccm_mem_usage
./dev_scripts/quality_stats.sh ./crypto      # unwraps in core code 827 -> 782, Err() 474 -> 440

AI Usage Statement

Did you use AI in creating this pull request:

  • No
  • Yes, indirectly - no submitted code was generated by AI (e.g., answering questions, performing a review, suggestions, etc.)
  • Yes, trivial code changes were generated by AI (e.g., autocompletion of a single line, reformatting, or spell-checking)
  • Yes, non-trivial code changes were generated by AI

Assisted-by: Claude:claude-mythos-5-1

officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Oct 21, 2026
officialfrancismendoza added a commit that referenced this pull request Oct 22, 2026
Keeping a key to one algorithm is moving to compile-time key types
(#167), so the opt-in runtime binding that 0de70ae added goes. Nothing
outside the tests ever bound a key, so no cipher call outside them
changes outcome; KeyMaterial's Display/Debug lose the `algorithm` field.

Removed:
- KeyMaterial's `algorithm` field and KeyMaterialTrait::algorithm,
  set_algorithm and check_algorithm.
- The 26 check_algorithm calls: 24 in crypto/cipher, one in Ascon's
  checked_key, one in the ToyCbc test double. The cipher imports go
  back to KeyMaterial alone.
- The framework's key_binding module and its four suite blocks, two
  tests in key_material_tests.rs, and aes/tests/key_binding_tests.rs,
  including the cross-size test 6994217 added there.

This is not `git revert 0de70ae`: that commit also folded the two
ToyBlockCipher copies into the cipher crate's, which stays, and a plain
revert conflicts with 6994217. Files no later commit touched are back
to 0de70ae~1 byte for byte.

#172's from_key_material and #157's ChaCha20 keys still call
check_algorithm, and need the same removal when they next merge this.

cargo test --workspace --no-fail-fast: 1106 passed, 1 failed. The
failure is ascon_wycheproof, whose vector file is missing from the
local wycheproof checkout; it fails the same way without this change.

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dghgit
dghgit force-pushed the feature/key-rework branch from 69e24ba to a615121 Compare October 24, 2026 09:53
@hubot
hubot force-pushed the feature/key-rework branch from a615121 to 69e24ba Compare October 24, 2026 10:06
dghgit added 19 commits October 24, 2026 21:16
…airs (prototype)

Three more key paths beside the AEAD, packet and MAC ones, so that every mode in
the tree can be reached from a mode-bound key: SymmetricEncryptionKey /
SymmetricDecryptionKey over a SymmetricCipherEncryptor / Decryptor worker (padded
CBC, CFB, CFB8, CTR), BlockEncryptionKey / BlockDecryptionKey over the whole-block
pair (CBC with no padding layer), and EcbEncryptionKey / EcbDecryptionKey in hazmat,
whose constructors take no RNG since ECB draws nothing. Each has its hazmat mode
pair (SymmetricEncryptMode and friends) for a mode type to implement. The one-shots
are provided over create_* and the worker's streaming methods; the symmetric pair's
in-place forms are gated on the worker being a stream cipher.

The workers are bounded on the cipher traits themselves, so KEY_LEN rides along
until the constructors leave those traits.

Assisted-by: Claude:claude-mythos-5-1
…(prototype)

test_key variants of the symmetric, stream, block, AEAD and tagged-layout runners
and the MAC runner, each driving a role-trait key with a caller-supplied RNG, plus
an ECB variant that takes none. The key-type, algorithm-binding and strength checks
those runners made at every do_*_init move to TestFrameworkFromKeyMaterial and
TestFrameworkFromAnyKeyMaterial, which check them once, where the key is made. The
E/D-generic runners stay until the constructors leave the cipher traits.

Assisted-by: Claude:claude-mythos-5-1
…me (prototype)

CBC<X>, CFB, CFB8, CTR<NONCE_LEN, BLOCK_LEN> and hazmat::ECB<X> implement Mode<P>
over any ModeNames permutation, and the role-side mode traits: CBC<Pad> and ECB<Pad>
the symmetric pair through the padding adapters, CBC<Blocks> the block pair, ECB<Blocks>
the ECB pair, the stream modes the symmetric pair with no final output. CCMFrame<N, T,
A, D> puts the fixed-frame CCM pair on the AEAD mode traits. The worker types gain
crate-private from_perm / from_inner constructors so the modes can build them from a
key state; the trait constructors stay until the deletion step.

Assisted-by: Claude:claude-mythos-5-1
A 16-byte key drawn from a 256-bit DRBG was tagged 256-bit, a figure
set_security_strength refuses for that length, so retagging such a key (as a MAC
mode's key does to hand it to the permutation) failed. The strength is now the
lower of the RNG's and the one the length supports.

Assisted-by: Claude:claude-mythos-5-1
tests/common/toy_key.rs is a ToyKey<P, M> with the AES key's shape: FromKeyMaterial
over any 16-byte toy permutation, the role impls forwarded to M's mode traits, and
ToyGCM / ToyGMAC / ToyCMAC standing in for the modes whose key state needs the key
length. The mode, padding and suspend tests build their workers from it and the
mode types, the conformance runs move to the test_key runners, and each key gets
the construction suite in place of the per-test wrong-key-type checks. The
padded_tests toy, an 8-byte XOR cipher with its own trait constructors, is replaced
by CBC<PKCS7> / CBC<NoPadding> over the shared toy; the ECB RNG-panic tests go,
since the ECB key path has no RNG-taking entry point.

Assisted-by: Claude:claude-mythos-5-1
The module examples built their workers through the trait constructors. They now
build the mode's key state and call the mode traits, which is what a mode-bound key
does, with GCM and CMAC on their from_key_state routes; the one-shots are the key's
and are shown in the key crate. The CcmEncryptor examples and compile_fail checks
move to CCMFrame.

Assisted-by: Claude:claude-mythos-5-1
The 128-bit key validated once, on FromKeyMaterial, with the AEAD role pair handing
out Ascon_AEAD128<Encrypting> / <Decrypting> per message; the cipher's constructor
splits into the key check and the per-message init so both routes share it. The
workers get the temporary worker-trait forwarding the role traits are bounded on.
Tests, the bench and the memory harness move onto the key, the key gets the
construction suite, and the crate docs show the key. The stack-usage rows for the
init and one-shot entry points are withdrawn until they are re-measured on the key
path.

Assisted-by: Claude:claude-mythos-5-1
KMACKeyInternal<PARAMS>, as KMAC128Key / KMAC256Key: the cSHAKE state after the key
binding of SP 800-185 Sec 4.3 step 1, with the customization and output length that
make it one KMAC function, on FromAnyKeyMaterial (nominal length, no customization)
and with_params / with_params_allow_weak for the rest, handing out KMACInternal
workers through the MAC role pair. KMACInternal::new_with_params becomes the crate's
own, written over the key; MAC::new stays until the deletion. The tests move onto the
key and gain the construction suite; the CLI's mac commands build KMACKey and HmacKey
objects in place of the trait constructors.

Assisted-by: Claude:claude-mythos-5-1
As KeyMaterial::from_rng now does: a 28-byte HMAC-SHA224 key drawn from a 256-bit
DRBG was tagged 256-bit, which set_security_strength refuses for that length.

Assisted-by: Claude:claude-mythos-5-1
MACFactory::new makes an HmacKey or KMACKey from the material and hands out its
generator, in place of MAC::new on the worker; the trait's own new and
new_allow_weak_key arms go with the deletion.

Assisted-by: Claude:claude-mythos-5-1
…o the key path (prototype)

AESKey<P, M> forwards the symmetric, block and hazmat ECB role pairs to M's mode
traits, so AES128Key<CBC<PKCS7>>, <CBC<Blocks>>, <CFB>, <CFB8>, <CTR>,
<ECB<Blocks>>, <ECB<PKCS7>> and <CCMFrame<..>> are keys. The tests, benches, the CCM
memory harness and the module docs move off the trait constructors and static
one-shots onto the keys: the known-answer suites pin their IVs through
create_encryptor and a FixedSeedRNG as before, the generic helpers are generic over
the key type, the construction suite replaces the per-test wrong-key checks, and
mode_key_tests.rs runs the framework over every new mode key. Dropped with no key
path equivalent: the CMAC new_allow_weak_key acceptances and the wrong-length
KeyMaterial512 refusals (a cipher key's length is in its type); the fixed-frame
CCM key's one-shots take the frame length, so the "any length" cases move to the
CCM packet key. The modes benches draw IVs from a FixedSeedRNG outside the timed
closure, where the trait constructors drew from an OS DRBG inside it.

Assisted-by: Claude:claude-mythos-5-1
The block, stream and GCM helpers take the key, or the worker it made, in place of
the key material: load_key_object builds the AES key type a command's mode wants,
the encrypt side draws its IV or nonce from the OS DRBG through create_encryptor,
and the decrypt side's create_decryptor is infallible. The Ascon command builds an
AsconAEAD128Key the same way. Output is unchanged: every cli/tests script passes.

Assisted-by: Claude:claude-mythos-5-1
…g one-shots (prototype)

SymmetricCipherEncryptor / Decryptor, AEADCipherEncryptor / Decryptor, BlockCipher*
and StreamCipher* now describe a worker that already exists: do_encrypt_init,
do_encrypt_init_rng, do_encrypt_init_nonce, do_decrypt_init and every static one-shot
over them are gone, and with them the KEY_LEN parameter, which was only ever the
material's. MAC loses new and new_allow_weak_key. Construction is a key object's
(create_encryptor / create_decryptor / create_generator) and the one-shots are the role
traits' in core::symmetric_key; the temporary AeadEncryptWorker / AeadDecryptWorker
traits fold back into the AEAD cipher traits, which the role and mode traits bound on
directly.

Downstream, the impls go from the cipher modes, the padding adapters, the stream
wrapper, Ascon-AEAD128, HMAC, KMAC and CMAC; the CCM packet workers and GMAC's material
route are rewritten over the key state; the test framework drops its E/D-generic
runners and test_mac; the HMAC callers in the hmac, sha2, sha3 and sm3 crates (tests,
benches, docs) move onto HmacKey; the last GCM and CCM test helpers in aes move onto
the keys. Dropped with no key-path twin: GCM's verify-before-decrypt one-shot override
(the provided one-shots zeroize on failure), and the comparisons against the worker
constructors, which now compare against the key-state route or an RFC reference.
QUALITY_AND_STYLE.md's API and naming rules say where construction lives, and
CLAUDE.md records the lessons.

quality_stats.sh, ./crypto, before and after the branch: unwraps in core code 827 -> 798,
Err() in core code 474 -> 446, test ratio .95 -> .98.

Assisted-by: Claude:claude-mythos-5-1
…ared-length side loses "packet" (prototype)

The fixed-frame mode type is CCM_Packet<N, T, AAD_LEN, DATA_LEN>, was CCMFrame,
spelled as the spec spells it with #[allow(non_camel_case_types)] like
Ascon_AEAD128. Its workers are CcmPacketEncryptor / CcmPacketDecryptor, taking
the names from the any-length pair, which becomes CcmEncryptor / CcmDecryptor
over the crate-private engine, now Ccm<P, Dir, ..>. The core traits that pair
implements go from AeadPacket{Encrypt,Decrypt}Mode and
AeadPacket{Encryption,Decryption}Key to AeadDeclared*, after the lengths they
take per message. The test and bench aliases (Frame, ToyCcmFrame,
Aes128CcmFrameKey) follow as Packet, ToyCcmPacket and Aes128CcmPacketKey, and
"packet" in the docs, test names and the CLI bound follows the same split. The
GCM packet bench keeps its word, which is the ordinary one.

Assisted-by: Claude:claude-mythos-5-1
…edule (prototype)

The role traits in core::symmetric_key and the mode traits in core::hazmat hand
out their workers through a generic associated type with a lifetime,
`type Encryptor<'k>`, and `create_encryptor` takes `&'k self`; every worker in
the cipher crate (Cbc, Cfb, Cfb8, CtrKeyStream, Ecb, Gcm, Gmac, Cmac, the CCM
engine and both CCM pairs) holds `&'k` to the mode's key state, so making a
worker copies the per-message state and nothing else, and a worker cannot
outlive its key. The mode traits spell the bound `where P: 'k`: rustc rejects a
generic impl whose key state is a struct over `P` under `Self::KeyState: 'k`.

What that removes: `KeyStream::new` and its `KEY_LEN`, `Gmac::new` /
`new_nonce` (the last material route), `Cmac::init`, and every `P::new` on
resume. SuspendableKeyed and SuspendableComponent gain the lifetime and take
the key by `&'k`; a cipher-mode worker's `Key` is now the mode's `KeyState`,
which a key object exposes through the new `core::hazmat::ModeKey::state`
(AESKey and the test ToyKey implement it), and the suspended layouts are
unchanged. The test framework's keystream suite takes a constructor closure
and leaves the key-policy checks to the construction suite. Ascon, HMAC, KMAC
and HKDF keep owned workers (HKDF embeds an HMAC worker) and implement the
traits for every `'k`.

Sizes: the CCM workers go from 264/296/328 B to 96 B at every key length, the
CCM_Packet pair from 344 B to 176 B, the CMAC workers from 248/280/312 B to
48 B; the CCM peak-stack table is re-measured with massif (in place 19 328 ->
18 608 B). Tests bind the key before the worker, since the worker borrows it.

quality_stats.sh, ./crypto: unwraps in core code 798 -> 784, Err() 446 -> 440,
test ratio .98 unchanged.

Assisted-by: Claude:claude-mythos-5-1
…y length leaves the workers (prototype)

`BlockPermutation<BLOCK_LEN>` now carries the block methods and
`ElectronicCodeBook<KEY_LEN, BLOCK_LEN>` adds only `new`. Every worker in the
cipher crate and every `Mode<P>` impl bound on the former and no longer name a
key length, so the GCM, GMAC and CMAC impls that `aes` wrote three times over
are written once in `cipher`, generic over the permutation, and the cipher
crate's tests lose their ToyGCM / ToyGMAC / ToyCMAC stand-ins. CMAC takes the
block length as a parameter, `CMAC<BLOCK_LEN = 16>`, as CTR does. The AES GMAC
pair becomes one inherent impl over `AESParams`.

With that, `KEY_LEN` goes from every worker type (Cbc, Cfb, Cfb8, Ecb,
CtrKeyStream, Ctr, StreamCipher, the padding adapters, Gcm, Gmac, Cmac, Ccm and
both CCM pairs) and from `GcmKeyState::new` / `CmacKeyState::new`; the AES
aliases follow. It stays on the mode traits' worker-producing halves, where
`P: ElectronicCodeBook<KEY_LEN, ..>` is the tie between a key's length and its
permutation's and is what keeps a key object's role impls to the one length.
No layout, vector or size changes.

quality_stats.sh, ./crypto: code lines 29792 -> 29432; unwraps and Err()
unchanged at 784 / 440.

Assisted-by: Claude:claude-mythos-5-1
…es the key object (prototype)

`HMAC<'k, H>` holds the inner hash and a reference to the `HmacKey` it came
from, whose outer pad state it is finished through, so a worker is one hash
state plus a pointer rather than two states: 224 -> 120 bytes for the SHA-256
family, 416 -> 216 for SHA-512, 880 -> 448 for SHA-3. The two halves of the
worker are public on the key as `inner_state` and `finish_out`, which is how
HKDF's extract phase now runs: a struct cannot own a key and a worker that
borrows it, so HKDF keeps the salt key and the inner hash and finishes through
the key. Its suspended layout is unchanged. The MAC factory becomes a key enum,
`MACFactory`, that hands out a worker enum, `MACFactoryWorker<'k>`, through the
MAC role traits; its one-shots are the role traits' and its streaming goes
through `create_generator`.

Resume takes the key object where there is one: `HMAC`'s `SuspendableKeyed::Key`
is `HmacKey`, Ascon-AEAD128's is `AsconAEAD128Key`, with no re-validation on
resume since the key object was validated when it was made. HKDF's stays the
salt material. No suspended layout changes.

quality_stats.sh, ./crypto: unwraps in core code 784 -> 782, Err() 440 unchanged.

Assisted-by: Claude:claude-mythos-5-1
They exist for HKDF's extract phase and are not part of the key's documented
API; the worker is the way onto HMAC.

Assisted-by: Claude:claude-mythos-5-1
Every row of the Ascon-AEAD128 and sponge tables is re-read from the frame
remarks and the assembly of the release build, with the same method as
before, under one compiler (rustc 1.97.0-nightly): the key check, the
constructors and the one-shots, which had been withdrawn when the key objects
arrived, get their figures, and the rows that had stood since the direction
moved into the type are brought up to date. The sponge rows come out 8 bytes
lower than the old figures under this compiler.

Assisted-by: Claude:claude-mythos-5-1
dghgit added 21 commits October 30, 2026 23:32
…-test-framework: the AEAD suite reaches the finals and the streaming contract the symmetric suite used to

From the review of PR #172 (R2, S7). On the base, aead.rs ran the whole
symmetric-cipher suite over the AEAD pair and called the array-returning
detached finals; after the key-object rework an AEAD key has no symmetric
role, so neither ran, and the provided do_encrypt_final_detached /
do_decrypt_final_detached and the inline do_*_final_out finals were exercised
by nothing in the framework. test_key now checks the array-returning detached
finals against the _out ones (round trip and the wrong-tag refusal) and runs
the no-AAD inline layout as the symmetric contract over the key's workers:
streaming through do_*_final_out in every chunking, a refused short streaming
buffer, the per-call output counts at the cuts that catch a running total, and
a corrupted inline ciphertext failing at every position. A fixed-length pair
runs it at its one length, zero included.

The three rationale comments the rewrite dropped are back: why the
ciphertext-differs check waits for eight bytes (a 1-in-256 flake otherwise),
why the inline one-shot is probed into a roomy buffer and an exact one, and
why the streaming cuts are placed where they are.

Assisted-by: Claude:claude-mythos-5-1
…: the block one-shots say what they check at run time, from_rng's strength cap is tested, KeyStream's docs follow the key objects

From the review of PR #172 (S1, S2, S4). BlockEncryptionKey::encrypt_inplace,
BlockDecryptionKey::decrypt_inplace and the two ECB one-shots said the block
alignment is checked at compile time; the parameter is a slice and the bodies
return DataNotBlockAligned, which their Errors sections now name. The cap
cb8188f8 put on KeyMaterial::from_rng -- the RNG's strength, at most what the
key length carries -- had no test asserting the strength; one does now at 16,
32 and 64 bytes from the 256-bit default DRBG. KeyStream's security note
described a constructor that took the init data and implementors that own the
key; a keystream is started by the key object and borrows it.

Assisted-by: Claude:claude-mythos-5-1
…, hkdf: the two halves of an HMAC computation move under hmac::hazmat

From the review of PR #172 (R4, S2). HmacKey::inner_state and finish_out were
pub behind doc(hidden), which is not a visibility; a primitive whose safe use
depends on the caller composing it correctly belongs under hazmat. They are
crate-private on the key now and reached through bouncycastle_hmac::hazmat's
inner_state and finish_out, with the security note saying what the pair does
not check. HKDF's extract phase, their one caller, uses those. HmacKey::generate's
doc says the drawn material's strength is capped at what the output length
carries, and the key test pins that rule on the material it is shared with
(a key object has no strength accessor, so the cap is not observable through
the key itself).

Assisted-by: Claude:claude-mythos-5-1
…: the HKDF Memory Usage table is measured after the rework

From the review of PR #172 (R3). HKDF holds the extract phase as an HmacKey,
both pad states, plus the inner hash, so the objects grew: size_of gives 368,
656 and 656 bytes for HKDF_SHA256, HKDF_SHA384 and HKDF_SHA512 where the table
still said 256, 448 and 448.

Assisted-by: Claude:claude-mythos-5-1
…: KMAC's generate doc says the drawn strength is capped at the key length

From the review of PR #172 (S2): the doc still said "at the RNG's strength".

Assisted-by: Claude:claude-mythos-5-1
…ory: the MAC worker enum's forwarding is written out, not a macro

From the review of PR #172 (R5). c13546e introduced a forward! macro for the
nine MAC methods of MACFactoryWorker with no justification against the Macros
rule; the base was unrolled, as HashFactory still is. The thirteen-arm matches
are written out per method.

Assisted-by: Claude:claude-mythos-5-1
… the CCM refusal pin, ECB<NoPadding> through the framework, and the modes bench says what it measures

From the review of PR #172 (R1, S5, S6). ccm_key_tests pins that a refused
do_encrypt_out / do_decrypt_out on the any-length CCM workers leaves only
zeros in the caller's buffer, over AES as well as the cipher crate's toy.
mode_key_tests runs the symmetric suite over ECB<NoPadding> at both key
lengths, which reached no framework run. The modes bench named
create_encryptor "(schedule clone + IV draw)" and create_decryptor
"(schedule clone)" when nothing is cloned any more: the names and the
bench_init doc say borrow; the decrypt comment names decrypt_4blocks, which
is what the N=8 bench uses; two stray "controlled comparison" lines go; and
the four single-element for loops become the bench_function calls they
wrapped.

Assisted-by: Claude:claude-mythos-5-1
…: the GAT docs say the borrow once, and the decrypt one-shots say when the plaintext is zeroized again

The eighteen `Encryptor<'k>` / `Decryptor<'k>` / `Generator<'k>` doc comments
in `symmetric_key` and `hazmat::mode` each restated the module-level
borrow-not-copy statement; they now say "borrowing the key for `'k`" and
leave the rest to the module docs. `ElectronicCodeBook` loses the paragraph
of rationale its own module docs already give. The AEAD and symmetric
decrypt one-shots said the output buffer is zeroized "again before an error
is returned"; the provided bodies zeroize the released prefix when the final
fails, not on every error, and the docs now say that. The `KeyStream`
security note is rewrapped after the earlier reword.

Assisted-by: Claude:claude-mythos-5-1
…-test-framework: the comments name the contract, not a test that no longer exists

`block_cipher.rs` and `symmetric_ciphers.rs` pointed at `test` and
`test_encryptor_decryptor`, which went with the constructors; the comments
now state the contract they introduce. `symmetric_key.rs`'s note on the MAC
split said the constructors were still to go. `from_key_material.rs` says
what `alg_name` is used for and that material of the right type is accepted.

Assisted-by: Claude:claude-mythos-5-1
…er: the CCM unit tests move to the end of the file

The `#[cfg(test)] mod tests` block sat between the mode's helpers and the
public types; it is now the last item in `ccm.rs`, where the other modes
keep theirs. One over-wide doc line on `CCM` is rewrapped.

Assisted-by: Claude:claude-mythos-5-1
… the doc examples say what they expect, and the key module docs are rewrapped

The CBC, CFB8 and ECB usage examples unwrapped key construction and the
cipher calls with a bare `.unwrap()`; each now carries an `.expect` message
naming the thing that cannot fail. The `key` module's opening paragraph is
rewrapped at the column limit after the earlier borrow-not-copy edit.

Assisted-by: Claude:claude-mythos-5-1
…n: AsconAEAD128Key does not need the non_camel_case_types allow

The attribute was carried over from `Ascon_AEAD128<Dir>`, whose name has
the underscore; the key type's does not.

Assisted-by: Claude:claude-mythos-5-1
…ory: the KMAC128 variant doc names the key type

`MACFactory::KMAC128` told a caller wanting a customization string to
construct `KMAC128` directly, which is the worker; the key object
`bouncycastle_sha3::kmac::KMAC128Key` is what takes the parameters.

Assisted-by: Claude:claude-mythos-5-1
…: the crate docs are rewrapped after the key-object edit

Assisted-by: Claude:claude-mythos-5-1
…usage_benches: the Ascon FixedNonce doc is rewrapped

Assisted-by: Claude:claude-mythos-5-1
… aes-ccm's module doc says "declared-length" once

Assisted-by: Claude:claude-mythos-5-1
…DE.md: MACFactory is a key enum, not a MAC

The factory section said every factory enum implements the trait it fronts;
`MACFactory` implements the MAC role traits and hands out a
`MACFactoryWorker`, which is the `MAC`.

Assisted-by: Claude:claude-mythos-5-1
…'s output length has SP 800-185's floor and a cap, at construction and on resume

C1 and C3. with_params and with_params_allow_weak accepted any output
length: at 0 a zero-length tag verified every message, since do_verify_final
compares only against the stored length, and above 2^61 the right_encode
argument 8 * L overflowed its u64 at the final. Both constructors now refuse
a length below KMAC_MIN_OUTPUT_LEN, 4 bytes, SP 800-185 Sec 8.4.2's "shall
not select an output length L that is less than 32 bits", and above
KMAC_MAX_OUTPUT_LEN, u64::MAX / 8, with MACError::InvalidLength. The check
sits on the two public MAC constructors, not the shared key binding: KMACXOF
is the L = 0 form of Sec 4.3.1 and builds its key through the same binding.
A suspended KMAC state claiming a length outside the bounds is refused on
resume, where it was previously read up to usize::MAX.

kmac_tests pins the refusals on both constructors, that the floor is the
shortest tag and never verifies an empty one, and both ends of the cap;
sp800_185_suspend_tests pins the resume bounds and both edges. A scoped
mutants run on kmac.rs: 142 tested, 78 caught, 57 unviable, 6 missed, all
of them pre-existing (the generate strength comparison, the provided mac and
verify bodies).

Assisted-by: Claude:claude-mythos-5-1
…entropy counter saturates, and resume refuses a value usize cannot hold

C5. A resumed state carried its entropy counter straight into
HkdfEntropyTracker, and the next credit_entropy did a plain add, which
overflows on a counter set to u64::MAX. The counter is bookkeeping, so the
add is now saturating, and read_state reads it through bounded_usize, which
refuses a value a narrower usize would have truncated. hkdf_tests pins the
saturating resume. Scoped mutants on lib.rs: 136 tested, 99 caught, 37
unviable, none missed.

Assisted-by: Claude:claude-mythos-5-1
…B's open segment is a Secret, and the GCM nonce and CCM_Packet one-shot docs say what the code does

C6. Cfb kept the unused tail of O_j, keystream that gives away the next
plaintext bytes against their ciphertext, in a plain array, with a comment
saying it never held key material; it is now a Secret, read straight into on
resume, and the comment says why.

C7. The GCM module doc said the nonce is never accepted from the caller;
create_encryptor_nonce, the _nonce one-shots and from_key_state_nonce take
one, and the doc now names them. The CcmPacketEncryptor and
CcmPacketDecryptor docs said the one-shots were Ccm's own and took any
length; a key bound to CCM_Packet provides them over the packet workers, so
they carry the frame, which CCM_Packet's own doc already said.

Assisted-by: Claude:claude-mythos-5-1
…MAC tag length help gives the floor, and the construction failure names the length

Assisted-by: Claude:claude-mythos-5-1
@dghgit
dghgit requested a review from mwcw October 30, 2026 13:17
…a refused KMAC tag length is reported, not panicked on

The kmac128 and kmac256 commands built their key with an expect, so a tag
length the key refuses, below the 32-bit floor or too large for a u64 of
bits, exited through a panic trace rather than the error route the hex and
key-file errors take. The construction now reports the refusal with its
reason and exits as those do. test_kmac.sh covers the round trip for both
widths, the customization string and tag length being bound into the tag,
and the two refused lengths on compute and on verify.

Assisted-by: Claude:claude-mythos-5-1
…resumed CCM packet state owes no AAD

P7. CcmAdapter::read_state bounded the payload still owed by the frame but
left aad_owed at whatever the state carried, once the header was formatted.
The packet pair declares and absorbs its AAD in one step in begin_data, so a
real state never owes any, before the header or after it; a resumed state
with a non-zero count is now refused in both directions. The Ccm component's
own read is unchanged, since the any-length workers it also serves do not
suspend. ccm_suspend_tests pins the formatted case alongside the unformatted
one it already had. Scoped mutants on the adapter's read_state: 11 caught,
none missed.

Assisted-by: Claude:claude-mythos-5-1
…M holds its byte counts to the spec limits and refuses before it copies

P3. A resumed GCM state carried its AAD and data byte counts unbounded,
and the length block multiplied each by 8 unchecked, on the argument that
the live path's checked_add could never get there; a crafted state could,
and the final overflowed. The two counts are now held to SP 800-38D Sec
5.2.1.1's limits, 2^64 - 1 bits of AAD and 2^39 - 256 bits of plaintext,
on every absorption and on resume, which is what makes the multiplication
safe. The plaintext limit is where the keystream runs out anyway, so the
live path reports it as DataLimitExceeded, as the counter did; the AAD
limit stays a StateError.

P4. The encryptor copied the plaintext into the output before the keystream
check refused the call, so a refused do_encrypt_out handed the caller's
plaintext back in the ciphertext buffer; the decryptor had absorbed the
bytes into GHASH before its keystream refused. Both directions now ask
whether the bytes fit, in the limit and in the keystream, before they copy
or absorb anything, through a check the stream cipher exposes beside its
own. gcm_tests pins the four edges and the zeroed output in both directions.

Scoped mutants on the changed lines of gcm.rs and stream.rs: 29 caught,
none missed. The crate-local run also reports GCM's length block (`* 8`,
unchanged here) as missed, since only the AES known answers see the tag,
and three equivalent zero-length guards in do_decrypt_out.

Assisted-by: Claude:claude-mythos-5-1
…nce-based MAC mode trait, which GMAC implements and the keys forward to

P13, first step. GMAC's generator/verifier pair on AESKey<P, GMAC> named
Gmac's constructors directly, from the aes crate, which is what kept those
constructors public. The hazmat mode traits gain NonceMacMode, the shape of
an AEAD mode's constructors with no ciphertext: a generator under a drawn or
supplied nonce, a verifier under the tag's nonce. GMAC implements it in
cipher; the AES key's inherent pair and the test toy key forward to it, as
the role impls forward to the other mode traits. No role trait is added,
GMAC being the only nonce-based MAC. Public API otherwise unchanged.

Assisted-by: Claude:claude-mythos-5-1
…e bare key-state and permutation constructors leave the public API

P13. GcmKeyState::new, CmacKeyState::new, Gcm's and Gmac's from_key_state
constructors, Cmac::from_key_state and the CCM workers' from_perm forms were
pub and re-exported from modes, so a caller holding a keyed permutation
could build any worker without the key object's type, strength and nonce
handling, and nothing on that path said hazmat, which QUALITY_AND_STYLE's
rule requires. Every mode-trait impl that calls them lives in this crate, so
they are now pub(crate): the key object is the route, and a key reaches the
state only to resume a suspended worker, through hazmat::ModeKey.

The doc examples build their key as the test suite does, through the toy
key over the toy permutation, which is now include!d beside it, and so no
longer carry an inner attribute. The cipher and aes tests that compared the
key route against the bare one compare it against the hazmat mode traits
the key forwards to, which is the same route under its notice. Public
surface otherwise unchanged; the key-state types stay nameable.

Assisted-by: Claude:claude-mythos-5-1
…s, HMAC, HKDF and KMAC do

The architecture section asked every multi-call algorithm to implement a
suspend trait, under a name the tree never had. The #173 decision (2026-10-30)
is that cipher and MAC workers neither suspend nor clone, since a second copy
of a nonce, counter or keystream position is a keystream reuse waiting to
happen, and that the worker traits stay dyn-compatible so a message is paused
by holding its worker. The sentence now says which types suspend and why the
workers do not, ahead of the sweep that removes the impls.

Assisted-by: Claude:claude-mythos-5-1
…ls come out in the #173 sweep

From mwcw's verification note of 4e8576b: the sentence stated a rule the tree
does not yet meet, since Gcm, Ccm and Cmac still suspend and clone until the
sweep removes the impls, and the document did not say so. One clause does.

Assisted-by: Claude:claude-mythos-5-1

This branch has not been deployed

No deployments
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.

2 participants