Part 7: Stream cipher traits, CFB as a stream cipher, and new CFB8 and CTR modes - #113
Open
dghgit wants to merge 53 commits into
Open
Part 7: Stream cipher traits, CFB as a stream cipher, and new CFB8 and CTR modes#113dghgit wants to merge 53 commits into
dghgit wants to merge 53 commits into
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
4 tasks
…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
hubot
force-pushed
the
feature/stream-cipher
branch
from
September 8, 2026 03:56
93ee992 to
0404ab9
Compare
…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
…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
dghgit
force-pushed
the
feature/stream-cipher
branch
from
September 9, 2026 01:25
0404ab9 to
4adbebb
Compare
hubot
force-pushed
the
release/0.1.3alpha
branch
from
September 9, 2026 01:44
d2a9b35 to
d1dcf75
Compare
This was referenced Sep 9, 2026
Closed
Closed
Contributor
|
I am starting to review this. Note that +23,025 -571 Lines changed is a lot to review, so I can't promise how quickly I'll finish, especially as other interruptions come up. I had left a number of review / discussion comments on #105. I will try to copy the relevant ones over to this PR as I go. |
ounsworth
self-requested a review
September 9, 2026 16:10
…ems like a personal workflow rather than a general thing.
…into feature/stream-cipher
…ems like a personal workflow rather than a general thing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue Link
No linked issue.
Summary
Replaces the never-implemented
StreamCiphertrait with a splitStreamCipherEncryptor/StreamCipherDecryptorpair shaped like the block cipher pair, movesCfbonto it, and adds twonew modes:
Cfb8andCtr.Description
What this PR contains. Six commits, in order:
5936674StreamCipherreplaced by the split encryptor/decryptor pair;TestFrameworkStreamCipherimplemented in place of itstodo!()8285686Cfbbecomes a stream cipher,Cfb8added, withAES_CFB8_*aliases,aes*-cfb8CLI and a shared stream-mode CLIea612a9afa976e2a36645Ctradded, withAES_CTR_*aliases andaes*-ctrCLI93ee992Ctrcross-checked against BC Java'sSICBlockCipherWhat I did, and why.
The trait.
StreamCiphercarried both directions on one trait and put aBLOCK_LENconstparameter on every data method, which a stream cipher has no use for. It had no implementors and its
test-framework suite was a
todo!(). It is replaced byStreamCipherEncryptor/StreamCipherDecryptor, mirroringBlockCipherEncryptor/BlockCipherDecryptor: direction encodedin the type so a policy can permit decryption while forbidding new encryption, in-place data methods
taking a
&mut [u8]of any length, init data generated by the constructor and never supplied, andone-shots provided over a single implementor hook per direction.
CFB. CFB never puts data through the cipher, only the input block, so it is a stream cipher and now
implements the new pair. A message that is not a whole number of blocks gets a short final segment,
taking the
s = 8rstep of the Sec 6.3 equations for that segment alone; the module docs derivethis, and it is what makes ciphertexts interoperate with other streaming CFB128 implementations. The
mode keeps one block that serves as input block, output block and next input block in turn, which is
why it costs one
usizemore thanCbcand no second buffer.CFB8. A separate type, because CFB8 and CFB128 are different, non-interoperable modes: their
ciphertexts agree on the first byte and diverge from the second. Its shift register is Sec 6.3's own
alternative description, a rotate followed by writing the ciphertext byte into the last position.
It costs one forward cipher per byte, 16x CFB on AES, which the docs say plainly.
CTR. The init data is the nonce and the counter takes whatever the nonce leaves, so
INIT_DATA_LENpicks the counter width. This is Appendix B.2'sTj = N | [j]m. The counter iscapped at 4 bytes and must be at least 1, both compile-time assertions, so a nonce outside 12..15
bytes on AES is a compile error. Running out of counter returns
SymmetricCipherError::StateErrorand consumes nothing, because the whole call is checked up front; this is the first use in the crate
of the
Resultthe data methods have always returned. CTR is the only mode here whose encryption isparallel too, so both directions batch.
Alternatives considered.
for j = 1...n, so its example starts at 1. Thisimplementation starts at 0. Appendix B presents B.2 as one of "two examples of approaches" and
allows "other methods and approaches", and the normative rule in Sec 6.5 is only that counter
blocks be distinct, so both are permitted. Zero was chosen because of the vectors: of the 2138
ACVP AES-CTR cases, 1853 have an initial counter block ending in four zero bytes and none ends
in
00000001, so starting at zero is the difference between 1853 official known-answer vectors andnone. It also makes a message identical to one from an implementation handed
nonce || 00000000as a whole-block IV, which is how CTR is usually driven.
in a crate that has deliberately kept init data out of the caller's hands.
scarcer resource and 2^32 blocks is 64 GiB per message.
Tests. Every mode has structural tests against a toy permutation, known-answer tests, chunking
equivalence at byte granularity in both directions, and mutation testing.
The 285 CTR cases skipped begin at a non-zero counter and cannot be expressed through a
nonce-plus-zero-counter API; the count is reported.
tabulated input and output blocks of F.3.7 checked three ways, which pins the shift register
against the spec's own table.
Every ACVP CTR case is a single block, so none of them exercises the counter increment at all:
a deliberately little-endian counter was run against the whole 1853-case set while these tests were
written, and it passed. That gap is closed by OpenSSL-generated five-block vectors and by checking
the counter blocks against the raw permutation at all four counter widths. The width sweep
matters because a wrong counter slice is invisible to a round-trip test, since both directions
build the same wrong block and still recover the plaintext.
Ctris also cross-checked against BC Java'sSICBlockCipher, which shares thenonce-plus-counter construction and so can reach the narrow counters OpenSSL cannot. Agreement is
exact on the 69-byte vectors, on 5000 bytes across the 255-to-256 carry at all three key lengths,
and on where the counter limit falls at both the 1-byte and 2-byte widths.
Scope and Risk
Packages impacted:
bouncycastle-core(trait replaced),bouncycastle-core-test-framework(suite implemented),
bouncycastle-modes(CFB rewritten, CFB8 and CTR added,bouncycastle-utilsadded as a dependency for
Secret),bouncycastle-aes-lowmemory(alias modules),cli(newstream-mode plumbing and six new subcommands).
Runtime behaviour that could change.
Cfbno longer implementsBlockCipherEncryptor/BlockCipherDecryptor. Callers using itthrough the block traits, or wrapping it in
PaddedEncryptor/PaddedDecryptor, will notcompile. That is intended: CFB needs no padding layer.
aes*-cfbno longer rejects unaligned input. It previously errored; it now encrypts anylength. This changes the observable behaviour of an existing command, and the ciphertext for an
unaligned message is new output that had no predecessor.
StreamCipheris gone. It had no implementors, so nothing in tree breaks.Likelihood of regression: low for CBC, ECB and the padding layer, which are untouched. The real
risk is concentrated in CFB, which was rewritten rather than extended: its ciphertext for
block-aligned data must be unchanged, and that is pinned by the F.3 vectors and the 2138 ACVP cases,
which pass unchanged.
Worst case: a keystream mode that repeated keystream would be a confidentiality failure rather
than a corruption. The three ways that could happen are all tested directly: a repeated nonce or IV
(each
do_encrypt_initdraws from the DRBG, and freshness is asserted), a counter that wrapped (CTRrefuses, at two widths, in both directions), and chunking that desynchronised the keystream (checked
as a full cross-product of call sizes in both directions, against a single-call reference, with real
AES as well as the toy).
Validation
Mutation testing reports 0 surviving mutants over the modes crate: 220 mutants, 108 caught, 112
unviable. One needed the tests to reach past runtime behaviour, since stubbing out CTR's
compile-time counter-width guard cannot fail a runtime test; the
compile_faildoctests onCtrarewhat kill it.
The ACVP suites need
bc-test-datacloned alongside this repository. Without it they print a warningand pass, so
cargo teststays green on a bare clone.To check the interoperability claims independently:
AI Usage Statement
Did you use AI in creating this pull request:
If submitted code changes were generated by AI, fill in the following declaration:
Assisted-by: Claude Code:claude-fable-5-1