Skip to content

Feat: Core AEAD Cipher Split (replaces #120) - #149

Closed
dghgit wants to merge 20 commits into
bcgit:feature/xof-cshakefrom
officialfrancismendoza:feature/officialfrancismendoza/119-core-aead-cipher
Closed

dghgit wants to merge 20 commits into
bcgit:feature/xof-cshakefrom
officialfrancismendoza:feature/officialfrancismendoza/119-core-aead-cipher

Conversation

@dghgit

@dghgit dghgit commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Replaces #120, which GitHub records as merged although the change is not in the base branch.

#120 was merged on the GitHub mirror rather than on origin. The mirror is refreshed from
origin, so the reset that followed took the merge commit (a777acdc) with it:
feature/xof-cshake is back at 8a13369 on both remotes and carries none of this work.
GitHub will not reopen a merged pull request, hence this one. Same head branch, same base,
same commits — nothing has been rebased or squashed, and the head is still 62e6a2b.

Original description, from #120:

Implements core AEAD cipher split (#119), based on #118 (feature/xof-cshake). Implements
AEADCipherEncryptor and AEADCipherDecryptor, as well as cherry-picks commits from the ASCON
PR (#21) so that ASCON is compatible — this PR supersedes #21 and implements #22.

Since #120 was approved, the head has gained two commits:

  • ac72292 — a merge of feature/xof-cshake, bringing in the base branch's renames:
    SimpleCipherEncryptor / SimpleCipherDecryptor are SymmetricCipherEncryptor /
    SymmetricCipherDecryptor again, and SymmetricCipherError::IncorrectOutputBufferLength(&'static str, usize)
    is now OutputBufferTooSmall(usize). Two conflicts were resolved: 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; and the two import lists in
    core-test-framework/src/symmetric_ciphers.rs were unioned, minus the deleted trait. The ascon
    and cli code follows the renames.
  • 62e6a2b — three typo fixes in CONTRIBUTING.md (@officialfrancismendoza).

cargo build --workspace, cargo test --workspace (1085 passed, 0 failed, 1 ignored) and
cargo fmt --all --check are clean on 62e6a2b.

The review history, including the approvals and ounsworth's threads, stays on #120.

Assisted-by: Claude:claude-opus-5

🤖 Generated with Claude Code

…in update_out_len and a FINAL_LEN final buffer so a buffering cipher or an inline ciphertext||tag layout can be expressed; TaggedEncryptor/TaggedDecryptor adapt any FINAL_LEN=0 pair to the SimpleCipherEncryptor/SimpleCipherDecryptor ciphertext||tag shape; the block, simple-cipher and AEAD strength sweeps assert they are not vacuous, and the AEAD streaming suite gains a genuinely-buffering toy plus undersized-buffer and std-one-shot coverage
…XOF128/CXOF128) implementing AEADCipherEncryptor/AEADCipherDecryptor via AsconAead128Encryptor/AsconAead128Decryptor, with HashFactory/XOFFactory registration and CLI wiring including a TaggedDecryptor-based decrypt stream
…OF/XOFSqueezer API and updated factory/CLI/tests/benches to compile against the new API (bcgit#119)
…/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 <noreply@anthropic.com>
…#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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…/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 <noreply@anthropic.com>
Assisted-by: Claude:claude-sonnet-5
@dghgit

dghgit commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

@ounsworth PR #120, erm... slipped on the soap in the wash room..., this one replaces it. We think it's now ready for final review as CCM and GCM are also well on their way and they represent the other 2 distinct styles of AEAD ciphers.

CCM #126 is now good to go as well. For reasons not readily explainable, it appears the commit try for this PR is also in CCM #126 and GCM #132...

dghgit and others added 4 commits September 24, 2026 10:43
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…/119-core-aead-cipher

Resolves the two import conflicts from 8823c7b, which moved SecurityStrength
out of core::traits into core::security_strength: cli/src/helpers.rs and
core-test-framework/src/symmetric_ciphers.rs take the base's new import path
and this branch's AEADCipherEncryptor/AEADCipherDecryptor names. The ascon
crate, core's aead_tagged_tests and cli/src/ascon_cmd.rs exist only on this
side, so the base's mechanical import move is applied to them here; the
result matches the resolution already carried by PR bcgit#126 file for file.

Assisted-by: Claude:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dghgit

dghgit commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Now merged into PR #133

@dghgit dghgit closed this Sep 28, 2026
This was referenced Sep 28, 2026
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