Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
4053f21
core, sha3: XOF extends Hash, so SHAKE128 and SHAKE256 are hashes; sq…
dghgit Sep 7, 2026
86819d7
sha3: pin the SHAKE block_bitlen and output_len values, which three m…
dghgit Sep 7, 2026
24ae9b4
core: XofOutput gains do_final and do_final_out, matching BC Java's d…
dghgit Sep 7, 2026
517cd59
sha3: add cSHAKE128 and cSHAKE256 (SP 800-185 Sec 3) with the Sec 2.3…
dghgit Sep 7, 2026
61d3d8e
docs: record the cargo mutants scoping flags, the bc-test-data conven…
dghgit Sep 7, 2026
63ca433
sha3: add KMAC128 and KMAC256 (SP 800-185 Sec 4) with KMACXOF, MACFac…
dghgit Sep 7, 2026
3adbfc1
core: drop the Default supertrait from Hash, so keyed constructions c…
dghgit Sep 7, 2026
b1fff3c
sha3: KMACXOF128 and KMACXOF256 as keyed XOFs, now that Hash no longe…
dghgit Sep 7, 2026
9efbf5f
core-test-framework: the XOF suite takes a constructor closure, so ke…
dghgit Sep 7, 2026
3913b55
sha3: add TupleHash and TupleHashXOF (SP 800-185 Sec 5), where each u…
dghgit Sep 7, 2026
90bd179
sha3: add ParallelHash and ParallelHashXOF (SP 800-185 Sec 6), comple…
dghgit Sep 7, 2026
5c6302a
cli: add tuplehash and parallelhash subcommands, completing SP 800-18…
dghgit Sep 7, 2026
68a3dbc
sha3: pin the Hash and XOF trait views of TupleHash, ParallelHash and…
dghgit Sep 7, 2026
50c125b
factory: replace the todo stub in xof_factory_tests with a differenti…
dghgit Sep 7, 2026
b95dd0c
core: Hash gains Clone as a supertrait, so a hash mid-stream can be f…
dghgit Sep 9, 2026
6707002
core: XOF gains default hash_xof and hash_xof_out bodies so only SHAK…
dghgit Sep 10, 2026
b466d95
core-test-framework: add test_hash_output_buffers, a closure-built Ha…
dghgit Sep 10, 2026
0e8b2f7
sha3: TupleHash and ParallelHash panicked on an output buffer shorter…
dghgit Sep 10, 2026
ad3fa52
core: drop XOFOutput::do_final and do_final_out, which no implementor…
dghgit Sep 10, 2026
8810ae7
core, core-test-framework, sha3, factory, mldsa, mlkem, cli: rename t…
dghgit Sep 14, 2026
0a2bb8f
core, core-test-framework, sha3: a final read of a XOF binds its outp…
dghgit Sep 14, 2026
8a46683
CLAUDE.md: record the cargo mutants mechanics this repo needs, since …
dghgit Sep 14, 2026
b5fcf99
core, core-test-framework: AEADCipherEncryptor/AEADCipherDecryptor ga…
officialfrancismendoza Sep 9, 2026
120b2fe
ascon, cli: add bouncycastle-ascon (SP 800-232 Ascon-AEAD128/Hash256/…
officialfrancismendoza Sep 9, 2026
2c479f4
Rebased #120 onto #118. Ported ASCON XOF/CXOF to new Hash/XOF/XOFSque…
officialfrancismendoza Sep 17, 2026
465e684
Minor doc fix to lib.rs given new XOF api (#119)
officialfrancismendoza Sep 17, 2026
386bbe3
Remediated documentation and test concerns (#119)
officialfrancismendoza Sep 18, 2026
61a0623
Merge remote-tracking branch 'origin/feature/simple-ciphers' into fea…
dghgit Sep 19, 2026
76bb967
Merge remote-tracking branch 'origin/feature/simple-ciphers' into fea…
dghgit Sep 19, 2026
e59229d
Merge remote-tracking branch 'origin/feature/simple-ciphers' into fea…
dghgit Sep 19, 2026
4568928
Merge remote-tracking branch 'origin/feature/simple-ciphers' into fea…
dghgit Sep 20, 2026
3d265ad
Merge remote-tracking branch 'origin/feature/xof-cshake' into feature…
dghgit Sep 20, 2026
6d849a1
cli, ascon: document the generated-nonce stream layout and the 8 MiB …
dghgit Sep 20, 2026
bbc04e3
release notes: the current bouncycastle-ascon mutation figures (#119)
dghgit Sep 20, 2026
2f7c32a
core, core-test-framework, ascon: delete the AEADCipher trait, supers…
dghgit Sep 20, 2026
80098c4
release notes: record the AEADCipher removal and re-measure the mutat…
dghgit Sep 20, 2026
702246a
core, core-test-framework, ascon, cli: carry the inline ciphertext||t…
dghgit Sep 20, 2026
c8190be
release notes: re-measure bouncycastle-ascon after the AEAD trait cha…
dghgit Sep 20, 2026
f376c14
core, core-test-framework: close the mutation gaps a scoped run found…
dghgit Sep 20, 2026
a1468a9
release notes: record the scoped mutation figures for the AEAD trait …
dghgit Sep 20, 2026
8a13369
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 21, 2026
ac72292
Merge branch 'feature/xof-cshake' into feature/officialfrancismendoza…
dghgit Sep 21, 2026
62e6a2b
docs: fix contributing typos
officialfrancismendoza Sep 21, 2026
ed09ae0
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 22, 2026
cf78449
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 22, 2026
336f9cb
core, core-test-framework, ascon, cli: make AEADCipherEncryptor/AEADC…
dghgit Sep 24, 2026
2eb7a99
ascon: add Ascon_AEAD128<Dir>, naming the AEAD pair by direction
dghgit Sep 24, 2026
c89663a
Merge remote-tracking branch 'origin/feature/xof-cshake' into trial/c…
dghgit Sep 24, 2026
570ae03
Initial add of AES lightweight CCM mode (#125)
officialfrancismendoza Sep 10, 2026
5d5cf5c
core, modes: document why AEADCipherEncryptor/Decryptor were not resh…
officialfrancismendoza Sep 14, 2026
3dd3266
cli: --nonce-file for CCM reads raw bytes only, never hex-decodes
officialfrancismendoza Sep 15, 2026
3be43fb
modes, core: zeroize CCM's CBC-MAC state, and make BUFFER_LEN vs the …
officialfrancismendoza Sep 15, 2026
e95a25b
modes: batch CCM's CTR half, and fix docs that claimed it was impossible
officialfrancismendoza Sep 15, 2026
9f437ab
cli: stop BlockModeAction's shared help from describing behaviour CCM…
officialfrancismendoza Sep 15, 2026
4282833
cli: process CCM input in place instead of allocating a second buffer
officialfrancismendoza Sep 15, 2026
ac13ce0
modes: dedupe CcmEncryptor/CcmDecryptor over a shared CcmBuffer, drop…
officialfrancismendoza Sep 15, 2026
e877a32
modes: add a Wycheproof AES-CCM suite, move/drop CCM unit tests that …
officialfrancismendoza Sep 15, 2026
1b593c5
modes: close the mutation-testing gaps the batching and buffer-bounda…
officialfrancismendoza Sep 15, 2026
afa2b3c
modes: adapt CCM buffer errors to the #120 API
officialfrancismendoza Sep 21, 2026
8a24373
Fixed formatting with cargo fmt (#125)
officialfrancismendoza Sep 21, 2026
24a646c
Remediated concerns. F1-F10 fixed except F9 (optional), which was lef…
officialfrancismendoza Sep 23, 2026
67a7b45
modes, aes, core-test-framework: port CcmEncryptor/CcmDecryptor to th…
dghgit Sep 24, 2026
7a6e2a4
Initial add of AES lightengine GCM mode (#124)
officialfrancismendoza Sep 14, 2026
6bd4ed7
modes, aes, cli: follow the base branch's API renames in AES-GCM (#124)
dghgit Sep 24, 2026
788fd06
modes, aes, cli: implement AEADCipherEncryptor/AEADCipherDecryptor fo…
dghgit Sep 24, 2026
15c2fa5
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 25, 2026
52e63bc
Merge branch 'feature/xof-cshake' into feature/officialfrancismendoza…
dghgit Sep 25, 2026
e691aed
Merge the updated CCM branch into feature/officialfrancismendoza/124-…
dghgit Sep 25, 2026
77edc43
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 26, 2026
d05e50e
Merge remote-tracking branch 'bcgit/feature/xof-cshake' into feature/…
officialfrancismendoza Sep 27, 2026
dd4e5f6
Merge remote-tracking branch 'origin/feature/officialfrancismendoza/1…
officialfrancismendoza Sep 27, 2026
2161a04
Fix batched keystream left on stack (#125)
officialfrancismendoza Sep 27, 2026
16a4ba5
Merge remote-tracking branch 'bcgit/feature/xof-cshake' into feature/…
officialfrancismendoza Sep 27, 2026
8e23ced
Merge the CCM branch (2161a04) into feature/officialfrancismendoza/12…
dghgit Sep 27, 2026
985eb94
modes: zeroize CTR's batched and single-block keystream, as 2161a04 d…
dghgit Sep 27, 2026
6a061ed
modes: CCM review fixes -- decryptor nonce floor, one error variant f…
dghgit Sep 27, 2026
f527ac9
aes: AES_CCM_*_Encryptor docs -- the nonce floor applies to the decry…
dghgit Sep 27, 2026
5de0f7d
cli: aes*-ccm -- warn on a nonce file ending in a newline, and explai…
dghgit Sep 27, 2026
30b871b
mem_usage_benches: make the CCM harness measure the streaming path it…
dghgit Sep 27, 2026
68a8934
aes: drop the redundant explicit link targets in the CCM module docs
dghgit Sep 27, 2026
174563c
mem_usage_benches: state the CCM streaming figures against the baseli…
dghgit Sep 27, 2026
62afc19
Intermediate add for CI fix changes
officialfrancismendoza Sep 27, 2026
2c32883
Merge remote-tracking branch 'origin/feature/officialfrancismendoza/1…
officialfrancismendoza Sep 27, 2026
3cee19b
Remove .claude/settings.json (#124)
dghgit Sep 27, 2026
2ed6768
modes: update the crate docs for GCM (#124)
dghgit Sep 27, 2026
01d725e
modes: keep GCM's H, tag mask and computed tag out of unzeroized stac…
dghgit Sep 27, 2026
117f06b
cli: --aad-file for GCM reads raw bytes only, never hex-decodes (#124)
dghgit Sep 27, 2026
8575ea7
Merge branch 'feature/simple-ciphers' into feature/xof-cshake
dghgit Sep 27, 2026
7e83f10
Merge remote-tracking branch 'origin/feature/xof-cshake' into feature…
dghgit Sep 27, 2026
840bf42
Merge remote-tracking branch 'origin/feature/xof-cshake' into feature…
dghgit Sep 27, 2026
7aa826d
Merge the CCM branch (840bf42) into feature/officialfrancismendoza/12…
dghgit Sep 27, 2026
3ecabfd
Merge PR #126 (feature/officialfrancismendoza/125-AES-lightengine-CCM…
dghgit Sep 28, 2026
f8400c3
Merge PR #132 (feature/officialfrancismendoza/124-AES-lightengine-GCM…
dghgit Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,15 @@ Quality / mutation testing:

```
./dev_scripts/quality_stats.sh ./crypto # lines-of-code, docstring & fallibility metrics; CI publishes this
cargo mutants # config in .cargo/mutants.toml (output: custom_mutants_output/)
cargo mutants -p bouncycastle-sha3 # config in .cargo/mutants.toml (output: custom_mutants_output/)
```

`-p` is as non-optional here as `--workspace` is for build and test, and for the same reason: a bare
`cargo mutants` examines only the root `bouncycastle` package, whose single `src/lib.rs` yields no
mutants, so it prints "No mutants found under the active filters" and exits **0**. See
[the mutation-testing mechanics](#notes-on-testing) for scoping a run to one file, for crates whose
tests live elsewhere, and for the test-data symlink.

Stack-memory benches are separate binaries under `mem_usage_benches/src/`, each declared as a
`[[bin]]` in that crate's `Cargo.toml`:

Expand Down Expand Up @@ -130,7 +136,11 @@ Repo mechanics behind those rules, which the documents don't spell out:
- `./dev_scripts/quality_stats.sh` produces the fallibility metrics both documents ask you to check. Run it before
and after a change and compare, rather than eyeballing the diff.
- **CLI commands stream.** The `cli/` binary is stdin→stdout with ~1 KB buffers so commands compose in shell
pipelines; preserve that when adding subcommands.
pipelines; preserve that when adding subcommands. The exception is a construction that is not
itself streamable, such as CCM (SP 800-38C Sec 3: "CCM is not designed to support partial
processing or stream processing", because the payload length is inside the first block the MAC
covers) -- there, read the whole input once and process it in place, rather than adding a second
buffer the size of the input on top of it; see `aes_ccm_cmd.rs`.
- Trait → factory → CLI is the wiring path for a new primitive; see [the workspace architecture](#the-core--core-test-framework--factory-spine) above for the crates involved.

## Scope of changes
Expand Down Expand Up @@ -180,7 +190,11 @@ Rules when working from the downloaded copy:
What a crate must be tested against — including the mutation-testing expectation, the trait test framework, and the
external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING.md. Repo-specific mechanics:

- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`).
- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). Four things about running it here:
- **Always pass `-p <crate>`.** Without it only the root package is examined, which has no mutants, and the run "passes" vacuously — see [Common commands](#common-commands).
- **`-f`/`--file` does nothing while the checked-in config is in play**, because its `examine_globs` wins over the CLI filter: `cargo mutants -p bouncycastle-sha3 -f '**/kmac.rs'` still examines all ~874 mutants in the crate. To scope a run to the files you changed, copy `.cargo/mutants.toml` somewhere outside the repo, delete its `examine_globs` block, and pass `--config <copy>`; `-f` then filters as documented. (`--config /dev/null` also works but throws away `skip_calls`, `error_values`, `cap_lints` and the timeout multipliers with it.)
- **Add `--test-workspace true` when a crate's mutants are killed by another crate's tests.** The `core` traits are the case that matters: their default method bodies are exercised from `sha3` and `factory`, so a `-p bouncycastle-core` run alone reports them all as missed.
- **Symlink the test data into `/tmp`.** `cargo mutants` copies the tree to `/tmp/cargo-mutants-<dir>-XXXX.tmp/`, so the `../../../bc-test-data/...` paths the vector suites use resolve to `/tmp/bc-test-data`. Without `ln -s <path-to>/bc-test-data /tmp/bc-test-data` those tests print their "not found" warning, pass vacuously, and every mutant they would have killed is reported as missed. Use `--jobs 3` and an explicit `--timeout`; note that a mutant which makes a squeeze return no bytes hangs a fill loop for real, so some timeouts are kills rather than false alarms.
- Integration tests in `tests/` are preferred over in-file `#[cfg(test)] mod tests` blocks — see "Unit tests vs integration tests" in QUALITY_AND_STYLE.md for the reasoning and the exceptions. A unit test is justified for high-risk code that has known-answer values and cannot be reached through the public API; when you write one, all of its helpers go inside that `mod tests`.
- A property that can be asserted at compile time (`const _: () = assert!(...)`) stays a compile-time assertion even when a test also covers it: `cargo mutants` cannot see a const assertion fail, so pair the two rather than trading the guarantee for the coverage.
- For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests — don't duplicate them per-implementation.
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ Some specifics:
* Public APIs of a library should be both ergonomic and expressive. When defining a new trait or public function, ask
yourself whether a programmer who is new to cryptography is likely to use this in a way that will get them into
trouble.
* Variables should be well-named, well-structured, and well-commented (a comment-to-code ration of 1:1 is a goal to be
* Variables should be well-named, well-structured, and well-commented (a comment-to-code ratio of 1:1 is a goal to be
strived for!). Think about memory footprint and, where possible, use unnamed scopes to allow the compiler to pop
intermediate value variables off the stack as soon as they are no longer needed.
* Always run your code through `cargo mutants` and get the issue count as low as your can. As a first pass, this forces
* Always run your code through `cargo mutants` and get the issue count as low as you can. As a first pass, this forces
you to write thorough unit tests. As a second pass, this draws your attention to bits of your code that cannot be
tested from the outside. Often this means that the code can be simplified without affecting functionality (as defined
by your set of unit tests) -- "simpler code" usually means faster runtime and easier future maintenance.
Expand All @@ -71,7 +71,7 @@ For minor updates, you can instead choose to create an issue with short snippets

* For contributions touching multiple files try and split up the pull request, smaller changes are easier to review and
test, as well as being less likely to run into merge issues.
* Create a test cases for your change, it may be a simple addition to an existing test. If you do not know how to do
* Create test cases for your change; it may be a simple addition to an existing test. If you do not know how to do
this, ask us and we will help you.
* If you run into any merge issues, check out this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to
help you resolve merge conflicts and other issues.
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ version = "0.1.3"
# *** Internal Dependencies ***
bouncycastle = { path = "./" }
bouncycastle-aes = { path = "./crypto/aes" }
bouncycastle-ascon = { path = "./crypto/ascon" }
bouncycastle-base64 = { path = "./crypto/base64" }
bouncycastle-modes = { path = "./crypto/modes" }
bouncycastle-core = { path = "crypto/core" }
Expand Down Expand Up @@ -46,6 +47,7 @@ edition.workspace = true

[dependencies]
bouncycastle-aes.workspace = true
bouncycastle-ascon.workspace = true
bouncycastle-base64.workspace = true
bouncycastle-core.workspace = true
bouncycastle-factory.workspace = true
Expand Down
40 changes: 40 additions & 0 deletions alpha_0.1.3_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@
* New algorithms added to crypto/ :
* SM3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java.
* AES -- AES-128/192/256, along with its modes AES_ECB, AES_CBC, AES_GCM.
* ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232).
`AsconAead128Encryptor` / `AsconAead128Decryptor` implement the generated-nonce
`AEADCipherEncryptor` / `AEADCipherDecryptor` pair; the inherent `AsconAead128` API keeps the
explicit-nonce, in-place streaming form (`new_encrypting` / `new_decrypting`).
`Ascon_AEAD128<Dir>` names the pair by direction (`Ascon_AEAD128<Encrypting>` /
`Ascon_AEAD128<Decrypting>`).
* `bouncycastle-ascon` is re-exported as `bouncycastle::ascon`; `Ascon-Hash256` and
`Ascon-XOF128` are registered in the factories, and the CLI adds `ascon-hash256`,
`ascon-xof128`, `ascon-cxof128` and `ascon-aead128`. The AEAD command generates and prefixes
the nonce by default, with `--nonce`/`--nonce-file` retained for deterministic vectors.
Streaming decrypt releases plaintext before the final tag check, so callers must discard any
output if finalization or the CLI exit status reports authentication failure.
* `core` gains the streaming AEAD split: `AEADCipherEncryptor<KEY_LEN, NONCE_LEN, TAG_LEN,
FINAL_LEN>` and `AEADCipherDecryptor<...>`, which extend `SymmetricCipherEncryptor<KEY_LEN,
NONCE_LEN, FINAL_LEN>` / `SymmetricCipherDecryptor<...>`. The inherited 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` is the tag plus anything the cipher holds back,
and every decryptor holds back the last `TAG_LEN` bytes it has seen, since it cannot know which
layout its final call will ask for. The AEAD traits add `do_update_aad`; the detached-tag
methods, each named for the base method it mirrors plus `_detached` (`do_final_detached` /
`do_final_out_detached`, `encrypt_out_detached`, `encrypt_out_rng_detached`, `encrypt_detached`,
`decrypt_out_detached`, `decrypt_detached` and the `*_len_detached` sizing helpers); and the
inline-tag one-shots with AAD, named for their base method plus `_with_aad`
(`encrypt_out_with_aad`, `encrypt_out_rng_with_aad`, `encrypt_with_aad`, `decrypt_out_with_aad`,
`decrypt_with_aad`).
`SymmetricCipherDecryptor::decrypt_out` now zeroizes what it wrote when `do_final` fails, as the
AEAD one-shots always have.
The older single-type `core::traits::AEADCipher`, which this splits and which had no
implementors, is removed, along with its `core-test-framework` suites
(`TestFrameworkAEADCipher::test` / `::test_plain_one_shots`).
Mutation testing of the pair's defaults (`traits.rs`, scoped to `AEADCipher{En,De}cryptor` and
`SymmetricCipherDecryptor::decrypt_out`, tested through `bouncycastle-core` +
`bouncycastle-ascon`) reports 134 mutants, 107 caught, 27 unviable and none missed; the
`AsconAead128Encryptor` / `AsconAead128Decryptor` adapters report 76 mutants, 49 caught, 27
unviable and none missed.
* ASCON testing covers the NIST LWC KAT sweeps from `bc-test-data` (1089 AEAD128, 1025 Hash256,
1025 XOF128 and 1089 CXOF128 cases when the data repository is present), plus embedded always-on
vectors. Mutation testing for `bouncycastle-ascon` reports 661 mutants, 558 caught, 97 unviable
and 6 missed; the six survivors are the sponge boundary and `set_state_byte` OR/XOR equivalences
documented at their sites.

## Minor features / bug fixes

Expand Down
184 changes: 184 additions & 0 deletions cli/src/aead_mode_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`.
//!
//! Parallel to [`crate::stream_mode_cmd`], but for [`bouncycastle::modes::Gcm`] rather than a
//! [`StreamCipherEncryptor`](bouncycastle::core::traits::StreamCipherEncryptor) mode: GCM carries
//! additional authenticated data and a tag, neither of which that trait has room for, so this
//! module drives it through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, which add
//! `do_update_aad` to the symmetric-cipher streaming methods.
//!
//! # On-the-wire format: `nonce || ciphertext || tag`
//!
//! `encrypt` writes the generated 12-byte nonce first, then the ciphertext as it streams, then the
//! 16-byte tag once stdin is exhausted. `decrypt` reads the 12-byte nonce first, then streams the
//! rest of stdin through the inline decryptor -- which, per [`SymmetricCipherDecryptor`]'s contract,
//! holds back the last 16 bytes it has seen because they might be the tag -- and checks the tag on
//! `do_final`.
//!
//! # The exit code is the signal, not the output
//!
//! On a tag failure, `decrypt` has **already written plaintext to stdout**: the inline decryptor
//! releases bytes as they clear the tail hold-back, well before the tag at the very end of the
//! stream can be checked. This is the same trade-off `Gcm`'s streaming API documents; a script that
//! needs to know before acting on the output must use the one-shot instead (not exposed by this
//! CLI) or check the exit code before trusting anything already written. On failure this command
//! prints `Error: authentication failed` to stderr and exits non-zero.
//!
//! # AAD
//!
//! `--aad <hex>` or `--aad-file <path>`; if neither is given, AAD is empty. Fed to the engine in
//! one call before any ciphertext, matching SP 800-38D Algorithm 4's requirement that AAD precede
//! data.
//!
//! `--aad-file` is read as raw bytes ([`read_from_file_raw`]), never hex-decoded. The
//! hex-or-raw guess `--key-file` uses would change what is authenticated without any error: a
//! binary header that happens to parse as hex text (`cafe`, or sixteen zero bytes, which the hex
//! decoder skips) would be authenticated as its decoding, and the tag would not verify against any
//! other GCM implementation given the same file.

use crate::helpers::{read_from_file_raw, write_bytes_or_hex};
use bouncycastle::core::key_material::KeyMaterial;
use bouncycastle::core::traits::{
AEADCipherDecryptor, AEADCipherEncryptor, ElectronicCodeBook, SymmetricCipherDecryptor,
SymmetricCipherEncryptor,
};
use bouncycastle::hex;
use bouncycastle::modes::{Decrypting, Encrypting, Gcm};
use std::io;
use std::io::{Read, Write};
use std::process::exit;

/// Bytes read from stdin per call. GCM has no batching advantage from a larger chunk the way CTR's
/// four-block path does, so this matches the other streaming commands' 1 KiB rather than needing
/// its own tuning.
const CHUNK_LEN: usize = 1024;

/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; see the
/// module docs for why not hex). Empty if neither is given: AAD is optional, unlike the key.
pub(crate) fn load_aad(aad: &Option<String>, aad_file: &Option<String>) -> Vec<u8> {
if let Some(path) = aad_file {
read_from_file_raw(path)
} else if let Some(hex_str) = aad {
hex::decode(hex_str).unwrap_or_else(|_| {
eprintln!("Error: `--aad` must be hex. Use `--aad-file` for raw bytes.");
exit(-1);
})
} else {
Vec::new()
}
}

/// Encrypts stdin to stdout under GCM: writes the generated nonce, then the ciphertext as it
/// streams, then the tag.
pub(crate) fn encrypt_gcm<P, const KEY_LEN: usize, const TAG_LEN: usize>(
key: &KeyMaterial<KEY_LEN>,
aad: &[u8],
output_hex: bool,
) where
P: ElectronicCodeBook<KEY_LEN, 16>,
{
let (mut enc, nonce) = Gcm::<P, Encrypting, KEY_LEN, TAG_LEN>::do_encrypt_init(key)
.unwrap_or_else(|e| {
eprintln!("Error: couldn't start encryption: {e:?}");
exit(-1);
});
write_bytes_or_hex(&nonce, output_hex);

enc.do_update_aad(aad).unwrap_or_else(|e| {
eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}");
exit(-1);
});

let mut buf = [0u8; CHUNK_LEN];
let mut out = [0u8; CHUNK_LEN];
loop {
let n = io::stdin().read(&mut buf).unwrap_or_else(|e| {
eprintln!("Error: failed to read from stdin: {e}");
exit(-1);
});
if n == 0 {
break;
}
// GCM's encryptor holds nothing back, so `out` (as long as `buf`) always has room.
let written = enc.do_update_out(&buf[..n], &mut out).unwrap_or_else(|e| {
eprintln!("Error: encryption failed: {e:?}");
exit(-1);
});
write_bytes_or_hex(&out[..written], output_hex);
}

// The detached final flushes nothing for GCM and returns the tag, written last.
let (_, _, tag) = enc.do_final_detached().unwrap_or_else(|e| {
eprintln!("Error: encryption failed: {e:?}");
exit(-1);
});
write_bytes_or_hex(&tag, output_hex);
finish(output_hex);
}

/// Decrypts stdin to stdout under GCM: reads the 12-byte nonce, streams the rest through the
/// inline decryptor, and checks the tag on `do_final`. See the module docs for why plaintext may
/// already be written to stdout by the time a tag failure is reported.
pub(crate) fn decrypt_gcm<P, const KEY_LEN: usize, const TAG_LEN: usize>(
key: &KeyMaterial<KEY_LEN>,
aad: &[u8],
output_hex: bool,
) where
P: ElectronicCodeBook<KEY_LEN, 16>,
{
let mut nonce = [0u8; 12];
if let Err(e) = io::stdin().read_exact(&mut nonce) {
eprintln!(
"Error: input too short to contain the 12-byte nonce that `encrypt` writes first ({e})."
);
exit(-1);
}

let mut dec = Gcm::<P, Decrypting, KEY_LEN, TAG_LEN>::do_decrypt_init(key, &nonce)
.unwrap_or_else(|e| {
eprintln!("Error: couldn't start decryption: {e:?}");
exit(-1);
});
dec.do_update_aad(aad).unwrap_or_else(|e| {
eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}");
exit(-1);
});

let mut buf = [0u8; CHUNK_LEN];
loop {
let n = io::stdin().read(&mut buf).unwrap_or_else(|e| {
eprintln!("Error: failed to read from stdin: {e}");
exit(-1);
});
if n == 0 {
break;
}
let out_len = dec.update_out_len(n);
let mut out = vec![0u8; out_len];
dec.do_update_out(&buf[..n], &mut out).unwrap_or_else(|e| {
eprintln!("Error: decryption failed: {e:?}");
exit(-1);
});
write_bytes_or_hex(&out, output_hex);
}

if let Err(e) = dec.do_final() {
// Whatever plaintext was already written above stands; the exit code is the signal a
// script must check (see the module docs).
io::stdout().flush().ok();
eprintln!("Error: authentication failed: {e:?}");
exit(-1);
}

finish(output_hex);
}

/// Flushes stdout, and adds the trailing newline the hex-output commands all emit.
fn finish(output_hex: bool) {
if output_hex {
println!();
}
io::stdout().flush().unwrap_or_else(|e| {
eprintln!("Error: failed to flush stdout: {e}");
exit(-1);
});
}
Loading
Loading