Conversation
… their HMAC variants
… and CLI wiring (PR #89)
… bits in the MSBs, unused low bits ignored
…ke the other hashes
…ptor with multi-block and one-shot methods (PR #107)
…-free bit-sliced AES permutation (PR #105)
…nd aes*-cbc CLI subcommands (PR #106)
…ryptor/PaddedDecryptor) (PR #97)
…me lengths, AES_CBC_* aliases, simpler CLI (PR #109)
…mmands and shared block-mode CLI (PR #111)
…locks8, SymmetricCipherEncryptor/Decryptor (from feature/sm4); CFB follows suit
…cb CLI subcommands; block-mode CLI generic over INIT_DATA_LEN
…S_PADS; SymmetricCipherEncryptor::do_final reports its output length
…with API changes and per-commit summaries
…rams/HashMLDSAParams/MLKEMParams traits, one impl per parameter set (#117)
…eamCipherDecryptor pair, shaped like the block cipher pair (in place, any length, generated init data); TestFrameworkStreamCipher implemented in place of its todo!()
… and Cfb8 (SP 800-38A Sec 6.3, s = 8) is added, with AES_CFB8_* aliases, aes*-cfb8 CLI subcommands and a shared stream-mode CLI
…, CFB8 is added, and the StreamCipher trait is replaced by the split encryptor/decryptor pair; re-measured throughput and mutation figures
…inst real AES at all three key lengths, not only the toy permutation
…th picks the counter width (max 4 bytes) and which errors rather than repeat a counter, with AES_CTR_* aliases and aes*-ctr CLI subcommands
… the nonce-plus-counter construction, pinning the 1, 2 and 3-byte counter widths that the ACVP and OpenSSL vectors cannot reach
… their HMAC variants
… and CLI wiring (PR #89)
…24-AES-lightengine-GCM-mode' into feature/officialfrancismendoza/124-AES-lightengine-GCM-mode
A local Claude Code permissions file, committed with the initial GCM add (7a6e2a4); checked in, it pre-approves git rebase/add for every contributor's session in the repo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The crate docs predated Gcm, or described its pre-788fd06 shape: - an "inherent detached-tag API", made private by 788fd06; the AEAD traits are Gcm's whole API - "For a new design, use Ccm", and a Security Considerations section that exempted only CCM from being unauthenticated - GCM listed under Not yet implemented; replaced with the two GCM options Gcm deliberately omits (non-96-bit IVs, 32/64-bit tags) - the CLI section counted six modes and had no -gcm framing Also adds a GCM alias and a doctested detached-tag round trip alongside the CCM ones. Spec references checked against SP 800-38D (Sec 5.2.1.1, 5.2.1.2, 8.3, Algorithm 4 step 2, Appendices A and C). Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k copies (#124) setup built H = CIPH_K(0^128) and CIPH_K(J0) in plain stack arrays before moving them into their Secrets, tag_block copied the mask back out with `*self.ek_j0`, and Ghash::absorb copied Y out with `*self.y` -- none of which were zeroized. SP 800-38D Sec 5.3 requires GCM intermediates to stay secret, and Appendix A: H recovered is authentication lost. The module docs already claimed all of these lived in Secret. H and CIPH_K(J0) are now encrypted in place inside their Secrets; Ghash updates Y in place, and absorb is an associated function over the fields so the pending block is passed by reference rather than copied. Ghash::finish and Gcm::tag_block write into a caller's Secret instead of returning an array: on the decrypting side that value is the expected tag T', which is a forgery for the rejected ciphertext if it survives a failed comparison. cargo mutants over gcm.rs and ghash.rs: 277 mutants, 222 caught, 46 unviable, 1 timeout, 8 missed -- all equivalent (check_shape's const assert, the documented | vs ^ in impl_mul64, and > vs >= guards on zero-length copies). Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_aad used read_from_file, whose hex-or-raw guess changes what the tag covers without any error: an AAD file of "cafe" was authenticated as 2 bytes, sixteen zero bytes (which the hex decoder skips) as empty AAD, and a file ending in a backslash panicked out of bounds in hex::decode_out. The tag then fails against any other GCM implementation given the same file. Same fix as 3dd3266 made for CCM's --nonce-file: read_from_file_raw. New test aad_file_is_raw_bytes_not_hex_decoded covers all three cases; it fails before this change and passes after. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… encrypting in place in the Secret and apply_batch/apply_one holding their transient blocks in Secrets, the same fix 2161a04 made for CCM's batch path; benches within noise (at most a few percent on batched decrypt), cargo mutants on ctr.rs 70 tested, 48 caught, 22 unviable, 0 missed Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in f8f7a2e, which keeps Ctr's keystream out of unzeroized stack arrays (refill, apply_batch, apply_one). Merged cleanly; no conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/officialfrancismendoza/124-AES-lightengine-GCM-mode Brings in f8f7a2e (via 8575ea7), which keeps Ctr's keystream out of unzeroized stack arrays -- GCM's GCTR runs through Ctr, so this closes the last of the stack copies behind gcm.rs's claim that the CTR keystream lives in Secret. Merged cleanly; this branch's only ctr.rs change is start_at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/officialfrancismendoza/125-AES-lightengine-CCM-mode Brings in f8f7a2e (via 8575ea7), feature/simple-ciphers' fix for Ctr's keystream being left in unzeroized stack arrays -- the same leak 985eb94 already fixed on this branch. The one conflict, ctr.rs's refill, apply_batch and apply_one, takes this branch's 985eb94 versions (one Secret per batch width held for the whole apply() call, rather than one per batch), keeping f8f7a2e's module-doc sentence; the crate docs' Memory Usage note is reworded to match. Resolving it here means the same conflict does not recur when this branch lands on feature/xof-cshake. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4-AES-lightengine-GCM-mode Brings in the CCM branch's review fixes since 174563c, including 985eb94's Ctr keystream zeroization, and its merge of feature/xof-cshake, so this branch now contains the CCM branch's tip and PR #132 can be based on it: the diff against it is the GCM work alone. Merged cleanly; ctr.rs is the CCM branch's version plus this branch's start_at. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| //! ``` | ||
| //! | ||
| //! `NoPadding` never writes a byte: asking it to is the error that tells the caller their data was | ||
| //! not block-aligned, and a "padded" block is all data. |
There was a problem hiding this comment.
Maybe it's me, but I can't figure out what this sentence is trying to tell me.
Could you try wording it a different way?
There was a problem hiding this comment.
It say's it throws an error if it's asked to pad (so it's used with the arbitrary length construct to ensure the data is block aligned). Would you tell me what the point of confusion is?
There was a problem hiding this comment.
The point of confusion is that I read the sentence 5 times and had no idea what it was trying to tell me. lol.
The sentence is:
NoPaddingnever writes a byte: asking it to is the error that tells the caller their data was not block-aligned, and a "padded" block is all data.
The implementation of NoPadding always returns an error. Calling it is an error. Period. That is its purpose. So what does "tells the caller their data was not block-aligned" mean? What does "and a "padded" block is all data" mean? I literally don't understand what that's trying to tell me.
Wouldn't it be clearer to replace this sentence with:
NoPaddingis a padding mode that always fails, it exists to ...
but I'm struggling to understand the point of a function that always fails, so I don't even know how to finish the sentence.
| return Err(SymmetricCipherError::OutputBufferTooSmall(out_len)); | ||
| } | ||
| // out_len is a multiple of BLOCK_LEN, so the remainder of this split is empty. | ||
| let (mut out_blocks, _) = ciphertext[..out_len].as_chunks_mut::<BLOCK_LEN>(); |
There was a problem hiding this comment.
I'm always a little bit leery of syntactic sugar when I'm not 100% sure that it does.
In this case, I'm leery about this creating a deep-copy under the hood (which would be bad on a GB-sized data.
This one seems ok, but just for the notes: the docs for as_chunks_mut weren't clear about whether this can ever result in a copy, so I had Fable weigh in and check the assembly. This seems ok, so I think that going forward, as_chunks is safe to use.
| fn wrap(inner: E) -> Self { | ||
| Self { inner, buf: Secret::new(), buf_len: 0, _padding: PhantomData } | ||
| } | ||
| } |
There was a problem hiding this comment.
There's an asymmetry between how this constructor for the Encryptor works, and the anagous constructor of the Decryptor.
I think I prefer the pattern used in the Decryptor, so I'm going to make this match.
| } | ||
| let original = block; | ||
|
|
||
| <PKCS7 as BlockCipherPadding<K>>::pad(&mut block, data_len).unwrap(); |
There was a problem hiding this comment.
The fact that you have to do a "turbofish" here -- ie the <>:: pattern, is a bit of a code smell. It means that the PKCS7 struct is impl'ing more than one trait that defines a pad() function. I think we should avoid having multiple traits define functions with the same name, both because it's hard to read, and because if we do this in the public API, then we're begging for users to grab for the wrong one and end up confused.
Perhaps this should go into QUALITY_AND_STYLE.md ?
There was a problem hiding this comment.
Oh, I see, the turbofish here is actually serving a different purpose: it's to bind K. But it turns out that the compiler can deduce that from context, so this line can be simplified to:
PKCS7::pad(&mut block, data_len).unwrap();
I think the point about potentially amending QUALITY_AND_STYLE.md to prefer not overloading functions might still be a good idea.
…-mode: AES LightEngine CCM Mode) into feature/xof-cshake Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-mode: AES LightEngine GCM Mode) into feature/xof-cshake Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| bouncycastle-aes.workspace = true | ||
| bouncycastle-core-test-framework.workspace = true | ||
| bouncycastle-hex.workspace = true | ||
| # Only to prove the modes compose with the padding layer for arbitrary-length data; no runtime dep. |
There was a problem hiding this comment.
Note-to-self: remove the comments.
Brings in the SP 800-185 work, the core AEAD traits and ascon wiring, and PRs #126 (AES CCM) and #132 (AES GCM). Merged cleanly: git's rename detection carried xof-cshake's padding changes into 013d806's padded_block_cipher.rs, and no xof-cshake file uses the old PaddedEncryptor/PaddedDecryptor names in code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| //! | CBC | [`Cbc`] | SP 800-38A Sec 6.2 | Cipher Block Chaining | | ||
| //! | CFB | [`Cfb`] | SP 800-38A Sec 6.3 | Cipher Feedback, full-block segment (`s = b`), i.e. CFB128 for AES | | ||
| //! | CFB8 | [`Cfb8`] | SP 800-38A Sec 6.3 | Cipher Feedback, 8-bit segment (`s = 8`) | | ||
| //! | CTR | [`Ctr`] | SP 800-38A Sec 6.5 | Counter. Nonce plus counter, both directions parallel | |
There was a problem hiding this comment.
Struct names like Ctr are idiomatic rust, but in keeping with the library's QUALITY_AND_STYLE.md, these should be capitalized to match SP 800-38A, which would be ECB, CBC, CFB, CFB8, and CTR.
That just a refactor of the struct names, which my IDE can handle easily, but will affect dependent PRs.
| /// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the | ||
| /// former and [`BlockCipherDecryptor`] only for the latter, so a `Cbc<_, Encrypting, _, _>` has no | ||
| /// decryption methods at all -- using one in the wrong direction is a compile error rather than a | ||
| /// runtime check. |
| //! type Aes128Ccm<Dir> = Ccm<AES128Internal, Dir, 16, 16, 12, 16>; | ||
| //! type Aes256Ccm<Dir> = Ccm<AES256Internal, Dir, 32, 16, 12, 16>; | ||
| //! // A 13-byte nonce leaves q = 2, so a payload of at most 64 KiB - 1; 802.11 CCMP's pair. | ||
| //! type Aes128CcmShortTag<Dir> = Ccm<AES128Internal, Dir, 16, 16, 13, 8>; |
There was a problem hiding this comment.
Aes128CcmShortTag seems like an odd example to put here since it doesn't exist in the AES crate, and I don't think it really adds to this tutorial about how to use the CCM<> struct to create type aliases.
Question: if Aes128CcmShortTag is a real thing that's used in the wild, then should it be defined in the AES crate?
| } | ||
|
|
||
| impl<P, const KEY_LEN: usize, const BLOCK_LEN: usize> BlockCipherEncryptor<KEY_LEN, 0, BLOCK_LEN> | ||
| for Ecb<P, Encrypting, KEY_LEN, BLOCK_LEN> |
There was a problem hiding this comment.
Is this a good idea?
If ECB is really meant as an internal building block, and maybe a toy for students / researchers, but otherwise hazmat, then maybe we shouldn't expose it through the BlockCipherEncryptor trait so that people don't get the wrong idea?
I think I'm suggesting just deleting these impl's.
There was a problem hiding this comment.
It's a knotty one... FIPS still test for it, so I'd leave it in. It's not that the mode is inherently insecure (after all you could argue that every other block cipher mode is built on it!) it's more that you need to know what you're doing if you use it. It's probably also better to leave it in so there's a way of doing it - the alternative is we'll find people using ElectronicCodeBook directly (as they say, life will always find a way...), which would definitely suck!
There was a problem hiding this comment.
But, like, it's not even a good fit for the BlockCipher API because that API forces it to return an IV, which for ECB will always be a zero-length array.
let (bytes_written, no_iv): (usize, [u8; 0]) = Aes128Ecb::<Encrypting>::encrypt(&key, &mut data).expect("encryption");
assert_eq!(no_iv.len(), 0, "EBC mode returns the IV as an empty array");
assert_eq!(data[..16], data[16..], "equal plaintext blocks give equal ciphertext blocks");and on the decrypt side, you have an init() that has to be handed something that the compiler knows is the empty array.
fn do_decrypt_init(
key: &KeyMaterial<KEY_LEN>,
_init_data: &[u8; 0],
) which you have to call like this:
Aes128Ecb::<Decrypting>::decrypt(&key, &[], &mut data)That just feels clunky.
To me, we have two reasons not to impl BlockCipher on ECB:
- It's misleading; we'll end up having to include AES_ECB, Aria_ECB, etc in the BlockCipherFactory, which is just wrong cause we really don't want people finding them and using them by accident.
- ECB is a bad fit for the BlockCipher API anyway.
If someone really wants to use AES_ECB and knows what they're doing, they can use it through the ElectronicCodeBook trait, which is pub.
What's the advantage of also exposing an API via BlockCipher?
Is there some structural thing that needs ECB to be exposed as a clunky BlockCipher? Does anything break if we just delete it?
…-- ecb.rs trailing whitespace (the rustfmt failure) and the batching sentence's lost object; lib.rs restores the required "Usage Examples" section name, moves the init-data and CFB/CFB8 paragraphs out from under "Notes on GCM Mode", repairs the AEAD and GCM opening sentences, says it is Gcm and not SP 800-38D that fixes the nonce at 12 bytes (Sec 5.2.1.1 only recommends 96 bits), fixes CBF8 and a leftover CCM comment fragment, drops the alias doctest's unused imports and wraps at 100 columns; and the last prose mentions of PaddedEncryptor/PaddedDecryptor take 013d806's new names Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| Ccm<P, Encrypting, KEY_LEN, BLOCK_LEN, NONCE_LEN, TAG_LEN> | ||
| where | ||
| P: ElectronicCodeBook<KEY_LEN, BLOCK_LEN>, | ||
| { |
There was a problem hiding this comment.
This impl block seems like it's basically duplicating all the functions of the SymmetricCipherEncryptor / Decryptor traits, but as inherents. I suggest that this should instead actually impl SymmetricCipher.
Consolidates the stacked pair #113 (
feature/stream-cipher→release/0.1.3alpha) and #115(
feature/symmetric-cipher→feature/stream-cipher) into a single branch off the releasebranch, and adds the SHA-512/t work on top.
feature/simple-cipherswas built by branching fromrelease/0.1.3alphaand mergingfeature/stream-cipherthenfeature/symmetric-cipher; both merges fast-forwarded, so thecontent below the two new commits is exactly what #113 and #115 already carried — this is a
re-packaging, not new review surface. #113 (+21,938) and #115 (+2,310) sum to roughly the
+23,499 here.
New in this PR, on top of those two
core:SecurityStrength::from_bits/from_bytesbecomeconst fn(3912434) — twokeywords, no behaviour change. Needed so an associated const can derive a strength from a const
generic; split out ahead of the feature per CLAUDE.md's scope-of-changes rule.
sha2:SHA512t<T>is usable for everytFIPS 180-4 s. 5.3.6 defines a hash for (785dbef).Previously only
T = 224andT = 256had parameter impls, so nothing else could be named.sha512t_h0to100 <= t < 512and emitted a fixed three digits, since only 224 and 256 were reachable. Witharbitrary
treachable that would produce the"0256"spelling s. 5.3.6 explicitly forbids —and hence the wrong IV — for every
tbelow 100. The one- and two-digit branches are restoredand
check_tasserts the section's own rule: positive,< 512, not 384.tmust be a multiple of 8, becauseHashis byte-oriented anda 100-bit digest has no representation here. BC Java's
SHA512tDigestimposes the identicalrestriction, so the two libraries accept the same set of truncations.
ElectronicCodeBook::ENCRYPTION_APPROVEDgatestwo-key TDEA:
SHA512tParams::<T>::FIPS_APPROVEDis public andSHA512Internal::newasserts itin an inline
const, so an unapprovedtis a compile error at the call site andnew_allow_unapproved_t()is the deliberate way in. That also blocksDefault, which keeps anunapproved truncation out of generic code by accident.
ALG_NAME,OUTPUT_LENandMAX_SECURITY_STRENGTHare derived fromt, withconstassertions pinning them to the values 224 and 256 previously had by hand.
Verification
cargo test --workspace: 943 passed, 0 failed (was 923).cargo fmt --checkclean; no newclippy warnings. Both new commits build independently.
SHA512tDigest, an independent implementation:eight truncations (8, 16, 24, 88, 96, 104, 264, 504) spanning all three decimal branches, over
the FIPS 180-4 Appendix C messages plus the one-million-'a' case. The 224/256 rows in the same
table match the NIST-published values.
cargo mutantson the changed files: 259 mutants — 180 caught, 5 timeout-kills, 69 unviable,5 missed. All five missed are the pre-existing XOR/OR equivalences already annotated at their
sites in
ch,majanddo_final_internal; no new missed mutants.Note for reviewers
Adding
constto a publiccorefunction is a forward compatibility commitment, andSHA512tParamsis currently its only caller. The alternative was a private copy of the roundingladder inside
sha2, free to drift from the real one — happy to switch if the API-surface cost isthe greater worry.
🤖 Generated with Claude Code