Repository navigation
Feat: Add ChaCha20, Poly1305, and Chacha20-Poly1305-AEAD - #157
Conversation
…/officialfrancismendoza/119-core-aead-cipher
…CLI thread, and fix a broken intra-doc link (bcgit#119) Review follow-ups on the head of bcgit#120; no behaviour changes. - cli/src/main.rs, cli/src/ascon_cmd.rs: the ascon-aead128 command's help and module docs still described the pre-nonce-prefix format ("output = ciphertext||tag") after the command started generating a nonce and writing it as the first 16 bytes of the stream. They now spell the convention out in both directions, the way aes128-ctr's help does for its own nonce, and say what --nonce/--nonce-file turn off -- the part a user gets wrong, since feeding a prefixed ciphertext to "--decrypt --nonce ..." decrypts garbage and only then fails the tag check. The two encrypt paths each gain a line saying which API they drive and why the explicit-nonce one cannot use the AEADCipherEncryptor pair (do_encrypt_init generates the nonce by construction). - cli/src/main.rs: fn main's 8 MiB thread gains a comment for the constraint it exists for. It is load-bearing: with it removed and `ulimit -s 1024`, every subcommand -- sha3-256 as much as ascon-aead128 -- overflows during argument parsing in a debug build, before any algorithm runs. - crypto/ascon/src/lib.rs: [`ascon_aead128::AsconAead128Decryptor::do_decrypt_final`] does not resolve, because do_decrypt_final is an AEADCipherDecryptor method rather than an inherent one, so `cargo doc` warned and published a dead link. Points at the trait method instead. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…#119) The entry carried the pre-remediation run, flagged as such ("20 missed before the XOF/CXOF boundary-test additions"). Re-measured on this head with `cargo mutants -p bouncycastle-ascon --test-package bouncycastle-ascon --jobs 3 --timeout 120`, with bc-test-data reachable from the copied tree and a config whose examine_globs block is removed: 735 mutants, 618 caught, 111 unviable, 6 missed. The six are the known equivalences already commented at their sites -- the sponge absorb/squeeze boundaries and the two disjoint-bit `|` -> `^` in set_state_byte -- so the 14 real survivors that run found in the XOF/CXOF Hash view are dead. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…eded by the AEADCipherEncryptor/AEADCipherDecryptor split (bcgit#119) AEADCipher was the single-type AEAD trait this issue exists to split. It had no implementor on the base branch and its conformance suite had nothing to run against; this PR was about to give it its first and only implementor, on AsconAead128, in the same change that introduces the pair meant to replace it. That would have left the library with two parallel AEAD abstractions and Ascon-AEAD128 with four public one-shot encrypt surfaces. Deleted instead: - crypto/core/src/traits.rs: the trait itself (encrypt/encrypt_out/decrypt/decrypt_out, the aead_* pair, do_aead_encrypt_final/do_aead_decrypt_final). The AEADCipherEncryptor doc that contrasted its tag placement with this trait's now just points at tagged_aead. - crypto/core-test-framework/src/symmetric_ciphers.rs: TestFrameworkAEADCipher::test and ::test_plain_one_shots, the suites for it. The struct keeps test_encryptor_decryptor and test_buffering_toy, which exercise the pair. - crypto/ascon/src/ascon_aead128.rs: the impl, and the module-doc sentence that justified the newtype pair by pointing at it. Test coverage is kept where it was about Ascon rather than about the trait: the chunk-boundary sweep and the wrong-tag rejection now drive the inherent do_encrypt_final/do_decrypt_final (they only used the trait for its finalizers), and the undersized-buffer suite is rewritten against the inherent one-shots, whose own length checks -- including the 16-byte-ciphertext and oversized-buffer boundaries that must NOT be rejected -- were previously reached only through the trait. The three tests that were about the deleted code (the std Vec wrappers, the plain view's DecryptionFailed remapping, the AEADCipher framework conformance call) go with it. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…ion figures (bcgit#119) Deleting the trait and its suites takes bouncycastle-ascon from 735 mutants to 655: 558 caught, 91 unviable, 6 missed, the same six known equivalences as before, so the tests ported onto the inherent one-shots hold the coverage the deleted trait's tests had. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…ag layout on the AEAD traits, and address the remaining API-shape review points (bcgit#119) The `tagged_aead` adapter pair is gone; what it did belongs to the traits themselves. - crypto/core/src/traits.rs: AEADCipherEncryptor gains `tagged_encrypt` (one-shot into `ciphertext || tag`), `tagged_do_aead_encrypt_final` (streaming: flush, then append the tag) and `tagged_encrypt_out_len`; AEADCipherDecryptor gains `tagged_decrypt`, `tagged_do_aead_decrypt_final` (streaming: the tail is leftover ciphertext followed by the tag) and `tagged_decrypt_out_max_len`. All are defaults over the existing methods, so every implementor gets both layouts and neither has to be bolted on by a wrapper type that cannot express a buffering cipher's lengths (the `FINAL_LEN = 0` restriction TaggedEncryptor and TaggedDecryptor carried). - crypto/core/src/tagged_aead.rs is deleted, with its module declaration and every use of it. crypto/core/tests/aead_tagged_tests.rs keeps the toy AEAD the deleted module's in-`src` tests used and points it at the new methods: round trip at every length crossing `TAG_LEN`, every chunking, tampering, a stream that ends before a whole tag, and every undersized buffer. - crypto/core-test-framework: the AEAD suite now checks the inline layout for every implementor (one-shot against streaming, and a too-short tail as DecryptionFailed), and the buffering toy checks it where FINAL_LEN > 0, which is where `tagged_do_aead_encrypt_final` has to flush and append in one call. Its short-buffer probe on the decryptor now feeds the decryptor its own ciphertext rather than the plaintext, and uses the ciphertext's length. - crypto/ascon: `AsconAead128::new`'s `for_encryption: bool` is no longer public API -- `new_encrypting` / `new_decrypting` name the direction, and the bool constructor they share is private. The crate docs gain a `tagged_*` example. - cli/src/ascon_cmd.rs: both directions drive the trait pair, holding the tag back by hand on the way in, which is what the adapter did for it. A failed `do_encrypt_init`/`do_decrypt_init` -- the RNG or the key material -- now prints an error and exits rather than panicking, as block_mode_cmd.rs does for the same call, and the remaining unwraps carry their `infallible:` notes. - crypto/core/src/traits.rs also: the allocating one-shot's three-part return is now the named `AEADEncrypted<NONCE_LEN, TAG_LEN>` (clippy `type_complexity`), and `decrypt_out` / `encrypt_out_rng` get the same "an implementor with FINAL_LEN > 0 must override this" note `encrypt_out` already had. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…nges (bcgit#119) 661 mutants, 558 caught, 97 unviable, 6 missed -- the same six known equivalences (the sponge absorb/squeeze boundaries and the two disjoint-bit `|` -> `^` in set_state_byte). The count moves from 655 with the new_encrypting/new_decrypting constructors. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
… in the tagged AEAD defaults (bcgit#119) `cargo mutants -p bouncycastle-core -f crypto/core/src/traits.rs --re 'AEADCipherEncryptor|AEADCipherDecryptor' --test-package bouncycastle-core --test-package bouncycastle-ascon` reported 116 mutants, 91 caught, 19 unviable, 6 missed. Four of the six were real: the buffer guards could be weakened without a test noticing, because a too-short buffer is rejected either by the guard or by the `do_update_out` behind it, and both report IncorrectOutputBufferLength with the same length -- so the probes could not tell which had fired. - crypto/core/tests/aead_tagged_tests.rs: `tagged_do_aead_decrypt_final` with a buffer of exactly `needed` must succeed. Kills `plaintext.len() < needed` -> `<=` and -> `==`. - crypto/core-test-framework: the buffering toy now finishes from a tail that still holds ciphertext (TAG_LEN + 4 bytes) into an exactly-sized buffer, which is what makes `update_out_len(..) + FINAL_LEN` observable -- with a generous buffer any arithmetic there would do. Kills `+ FINAL_LEN` -> `* FINAL_LEN`. The AEAD suite also feeds `encrypt_out_rng` a buffer with room to spare, so its own guard cannot be flipped to `>` unnoticed. The re-run is 116 mutants, 95 caught, 19 unviable, 2 missed; the two are `written + final_len` -> `written - final_len` in `encrypt_out_rng`, equivalent while every implementor has FINAL_LEN = 0. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…defaults (bcgit#119) The ascon crate's numbers were already there; the pair's own defaults in core were only in f376c14's commit message. 116 mutants, 95 caught, 19 unviable, 2 missed. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…rDecryptor back to SymmetricCipherEncryptor / SymmetricCipherDecryptor; the framework suite returns to TestFrameworkSymmetricCipher and the modes API test file is renamed to match Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…ferLength to OutputBufferTooSmall(usize) and expand the SymmetricCipherEncryptor/SymmetricCipherDecryptor update_out_len docs, per ounsworth's review on bcgit#133 Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…/119-core-aead-cipher Two conflicts, both from the base branch's renames. In crypto/core/src/traits.rs the AEADCipher trait this PR deletes collided textually with the AEADCipherDecryptor docs that replaced it, resolved in favour of the deletion. In core-test-framework's symmetric_ciphers.rs the two import lists were unioned, minus the deleted AEADCipher. The PR's own code follows the base branch's API renames: SimpleCipherEncryptor / SimpleCipherDecryptor are SymmetricCipherEncryptor / SymmetricCipherDecryptor, TestFrameworkSimpleCipher is TestFrameworkSymmetricCipher, and SymmetricCipherError::IncorrectOutputBufferLength(&'static str, usize) is OutputBufferTooSmall(usize) -- the dropped name field took a sentence in ascon's two one-shots, which is no loss since the error identifies the buffer by the call it came from. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
Assisted-by: Claude:claude-sonnet-5
…but David seems to have overwritten
… AES-128 crate doctest, plus two doc facts dropped in the 7535274 merge and dcec6b8 -- the doctest's key had become an ascending byte sequence carrying 0x08 where 0x06 belongs (0x08 twice, no 0x06) while the "FIPS 197 Appendix B" comment above the plaintext stayed put, and the ciphertext assertion had been replaced by a "`block` now contains the ciphertext" comment, leaving an example that only round-trips: encrypt-then-decrypt agreeing shows the two are inverses, not that either is AES, so the doctest passed with nothing checking the cipher's output at all. Key and assertion are restored from FIPS 197 (upd1) Appendix B, whose Input and Key are 3243f6a8885a308d313198a2e0370734 and 2b7e151628aed2a6abf7158809cf4f3c, and whose output state array read column-wise is 3925841d02dc09fbdc118597196a0b32. Also restores the bitslice.rs note that test_layout_matches_the_documented_table pins the bit-plane table exhaustively and that every mask in the crate is only correct relative to it, and the BlockCipherEncryptor note that the init data's length is specified by the implementing struct via the INIT_DATA_LEN constant -- the only prose documenting that const parameter, on the trait that carries it. Fixes "not fixable while with a lookp table based implementation" to "not fixable with a lookup-table-based implementation". Doc comments only; no code changes, and the other editorial rewording from those two commits is left alone. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
… for a cipher with no init data, and document that contract on the three encryptor traits and the one-shots provided over them, per ounsworth's review on bcgit#133 -- the thread asked how the RNG-taking constructor should behave for a cipher that needs no randomness (Rot13 was the example) and for the answer to be written into the function contract either way. Ecb was silently doing the opposite: `INIT_DATA_LEN == 0`, `_rng` ignored, delegating to do_encrypt_init, so the codebase had already answered "alias" by accident. It now panics. Reaching for the RNG-taking constructor means the caller expects a randomized mode, and ECB is not one (SP 800-38A Table D.2 lists its IV column as "Not applicable"), so returning a deterministic encryptor would leave that mistaken expectation undisturbed; QUALITY_AND_STYLE.md's fallibility rule puts "programmer didn't read the docs" on the panic side of the line rather than the Result side, so it is unimplemented!() and not a SymmetricCipherError. The contract goes on BlockCipherEncryptor, StreamCipherEncryptor and SymmetricCipherEncryptor: an implementation with INIT_DATA_LEN == 0 must panic rather than ignore the RNG, one with INIT_DATA_LEN > 0 must draw its init data from it and must not panic. The RNG-taking one-shots (encrypt_rng on the two block/stream traits, encrypt_out_rng on the symmetric one) are provided over the constructor, so each documents that it panics in exactly the cases the constructor does. The shared conformance suites drove all three of those paths unconditionally, which would have failed ECB for obeying the new rule, so each RNG-driven block now sits behind `if INIT_DATA_LEN > 0`, the same guard already used a few lines below for the "init data differs on two runs" check -- most of that file's diff is re-indentation. ecb_tests replaces the_rng_constructor_draws_nothing, which pinned the old aliasing behaviour, with should_panic tests for the constructor and for the encrypt_rng one-shot, and the determinism test's repeatability check now calls encrypt twice instead of encrypt then encrypt_rng. The modes and aes ECB module docs note the panic where they already explain INIT_DATA_LEN == 0; the aes one matters because the padded aliases inherit it through PaddedEncryptor's delegation. One commit across the four crates rather than one per crate: the ECB change and the framework guard are the two halves of a single contract, and landing either alone leaves cargo test --workspace failing. Assisted-by: Claude:claude-opus-5 Co-Authored-By: Claude Opus 5 <[email protected]>
…ipherDecryptor extend SymmetricCipherEncryptor/SymmetricCipherDecryptor, with the detached-tag methods named *_detached and the inline one-shots with AAD named *_with_aad
The inherited SymmetricCipher{En,De}cryptor methods are the AEAD with no
associated data and the tag inline (ciphertext || tag), so an AEAD can be held
and used as a plain symmetric cipher. FINAL_LEN keeps one meaning across both
traits: the tag plus anything the cipher holds back. Every AEAD decryptor now
holds back the last TAG_LEN bytes it has seen, since do_update_out is shared
and cannot know which final will be called: SymmetricCipherDecryptor::do_final
checks those bytes as the tag, AEADCipherDecryptor::do_final_out_detached
decrypts them as ciphertext.
The AEAD traits keep do_update_aad and add, named for the base method they
mirror: do_final_detached / do_final_out_detached (the _out form is the
required one), encrypt_out_detached, encrypt_out_rng_detached,
encrypt_detached, decrypt_out_detached, decrypt_detached and the
*_len_detached sizing helpers; and encrypt_out_with_aad,
encrypt_out_rng_with_aad, encrypt_with_aad, decrypt_out_with_aad and
decrypt_with_aad for the inline layout with AAD. do_encrypt_init,
do_decrypt_init, update_out_len and do_update_out are now inherited, and
tagged_do_aead_{en,de}crypt_final and the tagged_*_len helpers are gone,
their jobs taken by the inherited do_final and length helpers.
SymmetricCipherDecryptor::decrypt_out now zeroizes what it wrote when
do_final fails, since an AEAD reaches it through this trait and the AEAD
one-shots always have.
AsconAead128Encryptor / AsconAead128Decryptor implement both layers with
FINAL_LEN = TAG_LEN; the decryptor carries the 16-byte hold-back. The CLI's
Ascon-AEAD128 decrypt stream drops its hand-rolled tag tail, which the
decryptor now does itself. TestFrameworkAEADCipher::test_encryptor_decryptor
runs the whole TestFrameworkSymmetricCipher suite first, then the AEAD
checks; the buffering toy implements both layers and drives every one-shot.
cargo mutants -p bouncycastle-core -f crypto/core/src/traits.rs --re
'AEADCipher|SymmetricCipherDecryptor::decrypt_out' --test-package
bouncycastle-core --test-package bouncycastle-ascon: 134 mutants, 107 caught,
27 unviable, 0 missed. cargo mutants -p bouncycastle-ascon -f
crypto/ascon/src/ascon_aead128.rs --re 'AsconAead128(En|De)cryptor'
--test-package bouncycastle-ascon --test-package cli: 76 mutants, 49 caught,
27 unviable, 0 missed.
Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ascon_AEAD128<Encrypting> is AsconAead128Encryptor and Ascon_AEAD128<Decrypting> is AsconAead128Decryptor, spelled as SP 800-232 spells the algorithm. A plain type alias cannot choose between two distinct types, so it is written as a projection through AsconAead128Mode, which is implemented for the two bouncycastle-modes direction markers and nothing else; any other Dir is a compile error. bouncycastle-ascon now depends on bouncycastle-modes for those markers only. The alias is re-exported at the crate root, carries a doctest of the no-AAD inline-tag round trip, and aead128_tests.rs runs the AEAD conformance suite through it. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…cm-on-aead-new # Conflicts: # crypto/core/src/traits.rs
(cherry picked from commit fb594fa)
…aped for CCM CCM was implemented in part to test whether the AEAD streaming traits could support a packet cipher; it confirmed they cannot without buffering, since SP 800-38C needs the total AAD and payload length before it can authenticate anything, and the trait's do_encrypt_init/do_update_aad/do_update_out are open-ended by design for the common case (Ascon-AEAD128, and GCM once it exists) that never needs a total up front. Record the finding and the chosen resolution -- buffer internally or ship a dedicated non-buffering API, not a length parameter on the shared trait -- at the trait definition itself, cross referenced from CcmEncryptor, so a future implementor doesn't have to re-derive it. (cherry picked from commit a1b2245)
read_from_file's hex-or-raw heuristic is fine for a key, where a wrong guess only produces a mismatch, but for a CCM nonce it can turn two distinct binary nonce files into the same nonce value if both happen to be valid hex text for it -- and a repeated nonce under one key breaks CCM's authentication (SP 800-38C Appendix B). Add read_from_file_raw and use it for --nonce-file specifically; --nonce (hex on the command line) is unaffected. PR bcgit#126 review, finding F1. (cherry picked from commit 6a194ae)
…payload limit a compile error Ccm::y held Yr (the raw tag before the S0 mask) and every intermediate CBC-MAC chaining value in a plain array, unlike the keystream beside it, which is a Secret for the same reason; wrap it and finish_mac's local S0 the same way. Separately, CcmEncryptor/CcmDecryptor's BUFFER_LEN could exceed the payload limit NONCE_LEN implies (A.1's 2^8q - 1) and only fail at do_*_final, after buffering the whole message for nothing; assert the relationship at construction instead, which also makes MAX_PAYLOAD_LEN pub and lets do_*_final's # Errors sections state the guarantee precisely. Document the same capacity error as a general possibility on the trait's do_update_aad/do_update_out. PR bcgit#126 review, findings F3 and F4. (cherry picked from commit cd84a2c)
apply_keystream generated one counter block per encrypt_block call, even though A.3's Ctrj depends only on j and the counter blocks are exactly as independent as CTR's -- only the CBC-MAC half is genuinely serial (Sec 6.1 step 3). Restructure it like Ctr::apply: finish any open keystream block byte-wise, batch aligned whole blocks through encrypt_4blocks/encrypt_2blocks, then finish the tail byte-wise. Measured ~35-38% throughput gain (26->36 MiB/s for AES-128, no AAD; matches the buffering pair too), all 480 ACVP cases and 4 Appendix C vectors still pass. That made three doc passages actively wrong, since they said this was inherent: modes/src/lib.rs's mode comparison, modes_benches.rs's CCM doc comment (both rewritten with the new ratios against CTR), and lib.rs's "CCM takes no direction"/"there is no direction parameter" claims, which were already false against the code (Dir is very much a parameter) and predate this session. Also: fixed lib.rs's "264 B" vs the documented and now-tested 256 B, added size_of assertions pinning Ccm/CcmEncryptor's sizes against the memory table (previously undocumented by a test), and added the CCM aliases to the AES crate's "Modes of operation" section, which listed every other mode but this one. PR bcgit#126 review, findings F5 and F7. (cherry picked from commit 99effdc)
… doesn't have The Encrypt/Decrypt value help (rendered by clap under --help for every mode subcommand, including the three CCM ones) said a fresh IV or nonce is generated and written to the output. CCM's nonce is supplied via --nonce and never written, so bc-rust aes128-ccm --help printed instructions that produce "authentication failed" if followed. Trim the shared enum's help to direction only and point at each subcommand's own --help, which already documents its mode's exact framing (CBC/CFB/CFB8/CTR already do; CCM's own help already explains the nonce is supplied, not generated). PR bcgit#126 review, finding F6. (cherry picked from commit 8b74a5c)
go() called the *_detached one-shots, each of which needs a fresh ciphertext/plaintext buffer the size of the input on top of the input buffer already read from stdin. Use Ccm::new plus do_*_update/do_*_final directly on the buffer already in hand: input.len() is exactly the declared payload length and is supplied in one call, so the two do_*_update/do_*_final calls this replaces cannot fail, which the .expect()s explain. Also: decrypt's tag split now goes through split_last_chunk_mut, matching Ccm::decrypt's own reasoning for admitting Clen == Tlen instead of restating the spec's stricter Clen <= Tlen and then testing < anyway; and the payload-limit error message reads Ccm::MAX_PAYLOAD_LEN (now pub) instead of re-deriving it. Documented the packet-AEAD exception to CLAUDE.md's CLI-streams rule this relies on. PR bcgit#126 review, finding F8 (buffer only; the pre-existing duplicated nonce-range check is deliberate and stays, per its own comment). (cherry picked from commit 81a0cea)
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
…work The final h vs h - p select built its mask by sign-spreading the borrow bit and applied it with and/or, outside the ct module. It now uses Condition::<u64>::from_msb and Condition::select, whose mask goes through the value barrier so the optimiser cannot lower the select to a branch or cmov on the secret. Polarity is unchanged: h is kept iff the borrow out of h - p is set. The arithmetic is untouched; the RFC 8439 A.3 reduction-edge vectors still pass. process_block multiplied the five wrapped product terms by 5 on every block and allocated, and zeroized on drop, a Secret accumulator per block. 5 * r[1..5] is now computed once at key setup (r5; not "s", which RFC 8439 uses for the key's second half, here "pad"), and the accumulator is a Secret field reset each block. Each term has the same value as before, so the "below 2^58" bound comment still holds. Poly1305 now goes through core-test-framework's MAC suite, via the fixed-output entry point, for each RFC 8439 vector and once with a self-derived tag that needs no external data. The test file header no longer says the framework is skipped. cargo mutants -p bouncycastle-poly1305 (BC_TEST_DATA set): 202 mutants, 176 caught, 7 missed, 3 timeouts, 16 unviable. Five misses are equivalent: | to ^ where the operands share no bits (the 2^128 bit against a 24-bit load, and the four limb-packing ORs). The other two were real and now have tests: a key tagged exactly 128-bit must be accepted by new(), and finalize's last h1 += h0 >> 26, which no RFC or Wycheproof vector reaches, is pinned by two searched-for one-block inputs checked against an independent big-integer evaluation of RFC 8439 Sec 2.5. The timeouts are kills: each makes do_update loop forever. 12 unviable are error_values / Default::default() replacements; the other 4 were Windows LNK1104 linker failures, and each, applied by hand, is caught. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…bc-test-data locator Each of the three crates carried its own test-data locator, and they introduced BC_TEST_DATA / WYCHEPROOF_ROOT overrides no other suite used. With an override set and the file absent, they hard-panicked in fs::read_to_string(..).expect(..) rather than skipping with the warning every other suite gives. core_test_framework::external_vectors::external_vector_file is now the one locator. It tries the same two relative roots as the AES suites' acvp_helpers::test_data_dir (the repository cloned beside this one, seen from a crate directory or the workspace root), and a missing file prints a warning and returns None. The overrides stay, as the absolute root to use instead of the relative paths: neither relative path resolves from a scratch worktree or from the temporary copy cargo mutants builds in, and setting them is how the mutation runs on these crates saw the RFC and Wycheproof vectors at all. A set but wrong root now warns and skips like a missing checkout. CLAUDE.md's mutants notes mention them beside the /tmp symlink recipe. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
… permutation encrypt_gcm / decrypt_gcm built a Gcm<P, ..> from a permutation type, so the next AEAD command would have had to copy the whole nonce/AAD/ 1 KB streaming loop. encrypt_aead / decrypt_aead now take any AEADCipherEncryptor / AEADCipherDecryptor, with the nonce length a const parameter, and encrypt_gcm / decrypt_gcm are one-line wrappers over them, so the aes*-gcm commands are unchanged. Two things the GCM-only code could ignore are now handled for a generic AEAD: the ciphertext the detached encryptor final returns, and the plaintext the inline decryptor final returns, are both written out. Both are empty for GCM, so its output is byte-for-byte the same. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
… poly1305 commands
QUALITY_AND_STYLE.md requires a CLI for every primitive, and CLAUDE.md
names the factory as the wiring path for a new MAC.
- chacha20: -d encrypt|decrypt, --key / --key-file, -x, through the
shared run_stream_mode loop exactly as aes*-ctr: the generated 12-byte
nonce is written ahead of the ciphertext.
- chacha20-poly1305: the aes*-gcm flags, through the generic AEAD
helpers from the previous commit: nonce || ciphertext || tag, 1 KB
chunks, no input-sized buffer.
- poly1305: --key / --key-file / -v / -x like the HMAC commands, a
separate entry point rather than an HMACVariant since it is not an
HMAC. The help text says the key is one-time, and a key that is not
32 bytes is an error message rather than a panic.
- MACFactory::Poly1305, by Poly1305::ALG_NAME ("Poly1305"), tested
with the RFC 8439 Sec 2.5.2 vector and the full-tag rule.
Checked by hand: 5- and 5000-byte round trips through chacha20 and
chacha20-poly1305, a wrong AAD failing authentication, and the poly1305
command producing and verifying the Sec 2.5.2 tag.
Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…_std, release note The obligations QUALITY_AND_STYLE.md places on every crate: - benches/: one criterion bench per algorithm over 16 KiB of random data, with Throughput::Bytes. One each rather than one-shot and streaming variants, since those share an implementation and the style guide says not to bench them separately. - Crate docs under Usage Examples (one-shot and streaming for the two ciphers), Memory Usage and Security Considerations, reusing the warnings already there. The size_of figures in each Memory Usage table are pinned by a test, as ctr_tests does for CTR. - #![no_std] on chacha20 and chacha20-poly1305-aead, which use nothing from std. poly1305 is left without it: it returns Vec through the MAC trait, as HMAC does, and HMAC's crate carries no no_std either. - A release-notes line. On F10's per-block savings: this bench on this machine measured about 1.03 GiB/s after against 1.00 to 1.02 GiB/s before, over two rounds of each, which is within run-to-run noise. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…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 retargeted to the key-rework-memory feature branch (PR #172) and made changes based on upstream design. However, this is a separate issue from the SuspendableKeyed and keystream reuse issue (#173), which should require its own separate PR. |
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 <[email protected]>
af15bb9 to
6c5e223
Compare
Based on RFC 8439, implement three separate chacha20, poly1305, and chacha20-poly1305-AEAD crates compatible with the new API shape (implements #145).