Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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
ac72292
Merge branch 'feature/xof-cshake' into feature/officialfrancismendoza…
dghgit Sep 21, 2026
62e6a2b
docs: fix contributing typos
officialfrancismendoza Sep 21, 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
861b070
Merge branch 'feature/xof-cshake' into feature/officialfrancismendoza…
officialfrancismendoza Sep 25, 2026
b7d27a0
Merge branch 'feature/xof-cshake' into feature/officialfrancismendoza…
dghgit Sep 27, 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
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
290 changes: 290 additions & 0 deletions cli/src/ascon_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
use std::io::{self, Read};
use std::process::exit;

use bouncycastle::ascon::ascon_aead128::{
AsconAead128, AsconAead128Decryptor, AsconAead128Encryptor,
};
use bouncycastle::ascon::ascon_cxof128::AsconCXof128;
use bouncycastle::ascon::ascon_hash256::AsconHash256;
use bouncycastle::ascon::ascon_xof128::AsconXof128;
use bouncycastle::core::errors::SymmetricCipherError;
use bouncycastle::core::key_material::{
KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations,
};
use bouncycastle::core::security_strength::SecurityStrength;
use bouncycastle::core::traits::{
AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor,
};
use bouncycastle::hex;

use crate::helpers;

/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied.
fn load_bytes(value: &Option<String>, value_file: &Option<String>, label: &str) -> Vec<u8> {
if let Some(file) = value_file {
helpers::read_from_file(file)
} else if let Some(v) = value {
hex::decode(v).unwrap_or_else(|_| {
eprintln!("Error: {label} is not valid hex.");
exit(-1)
})
} else {
eprintln!("Error: {label} must be supplied.");
exit(-1)
}
}

fn load_optional_bytes(
value: &Option<String>,
value_file: &Option<String>,
label: &str,
) -> Option<Vec<u8>> {
if let Some(file) = value_file {
Some(helpers::read_from_file(file))
} else {
value.as_ref().map(|v| {
hex::decode(v).unwrap_or_else(|_| {
eprintln!("Error: {label} is not valid hex.");
exit(-1)
})
})
}
}

fn require_16(bytes: Vec<u8>, label: &str) -> [u8; 16] {
bytes.try_into().unwrap_or_else(|_: Vec<u8>| {
eprintln!("Error: {label} must be exactly 16 bytes.");
exit(-1)
})
}

/// Build a `KeyMaterial<16>` for the AEAD key, warning (and forcing usable metadata) only if the
/// key turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does.
fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> {
let mut key =
KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap();
if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit {
eprintln!(
"Warning: low entropy key provided. We'll still process it, but it may be insecure."
);
do_hazardous_operations(&mut key, |k| {
k.set_key_type(KeyType::SymmetricCipherKey)?;
k.set_security_strength(SecurityStrength::_128bit)
})
.unwrap();
}
key
}

/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest.
pub(crate) fn hash256_cmd(output_hex: bool) {
helpers::stream_hash(AsconHash256::new(), output_hex);
}

/// Ascon-XOF128 of stdin, producing `output_len` bytes. Streaming absorb.
pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) {
helpers::stream_xof(AsconXof128::new(), output_len, output_hex);
}

/// Ascon-CXOF128 of stdin with a hex customization string, producing `output_len` bytes.
pub(crate) fn cxof128_cmd(customization: &Option<String>, output_len: usize, output_hex: bool) {
let z = match customization {
Some(v) => hex::decode(v).unwrap_or_else(|_| {
eprintln!("Error: customization is not valid hex.");
exit(-1)
}),
None => Vec::new(),
};
let x = AsconCXof128::with_customization(&z).unwrap_or_else(|_| {
eprintln!("Error: customization string exceeds 256 bytes.");
exit(-1)
});
helpers::stream_xof(x, output_len, output_hex);
}

/// Ascon-AEAD128 of stdin. Encrypts (stdin = plaintext, output = nonce||ciphertext||tag) or, with
/// `decrypt`, decrypts (stdin = nonce||ciphertext||tag, output = plaintext). Decryption exits with
/// a non-zero status if the authentication tag does not verify.
///
/// The 16-byte nonce is generated by the library and travels at the head of the stream, the same
/// convention `block_mode_cmd`/`stream_mode_cmd` use for their IV, so an encrypt and a decrypt
/// compose in a pipeline with nothing but the key passed between them. A caller-supplied `nonce`
/// overrides that and is kept out of the stream in both directions; it is there for known-answer
/// vectors, and repeating one under a given key breaks Ascon-AEAD128 outright.
///
/// Both directions stream stdin in fixed-size chunks (no full-buffer slurp). Encryption emits
/// ciphertext eagerly, before the tag is known; note that in the decryption direction, plaintext
/// is likewise emitted before the tag has been checked, so it should not be treated as
/// authentic until this command exits with status 0 (see the crate's "Security Considerations").
pub(crate) fn aead128_cmd(
key: &Option<String>,
key_file: &Option<String>,
nonce: &Option<String>,
nonce_file: &Option<String>,
ad: &Option<String>,
decrypt: bool,
output_hex: bool,
) {
let key = load_key_material(&require_16(load_bytes(key, key_file, "key"), "key"));
let nonce =
load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce"));
let ad_bytes = match ad {
Some(v) => hex::decode(v).unwrap_or_else(|_| {
eprintln!("Error: associated data is not valid hex.");
exit(-1)
}),
None => Vec::new(),
};
let ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) };

if decrypt {
aead128_decrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex);
} else {
aead128_encrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex);
}
}

/// Generated-nonce encryption: drives [`AsconAead128Encryptor`] in the inline `ciphertext || tag`
/// layout (the inherited [`SymmetricCipherEncryptor::do_final_out`]), writing the nonce it
/// generated ahead of the stream.
/// With an explicit nonce there is no nonce to write, so that case goes to
/// [`aead128_encrypt_stream_with_explicit_nonce`] instead.
fn aead128_encrypt_stream(
key: &KeyMaterial<16>,
nonce: Option<&[u8; 16]>,
ad_opt: Option<&[u8]>,
output_hex: bool,
) {
if let Some(nonce) = nonce {
aead128_encrypt_stream_with_explicit_nonce(key, nonce, ad_opt, output_hex);
return;
}

let (mut cipher, nonce) = AsconAead128Encryptor::do_encrypt_init(key).unwrap_or_else(|e| {
eprintln!("Error: couldn't start encryption: {e:?}");
exit(-1);
});
if let Some(ad) = ad_opt {
// infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none
// has been yet.
cipher.do_update_aad(ad).unwrap();
}

helpers::write_bytes_or_hex(&nonce, output_hex);

let mut buf = [0u8; 1024];
loop {
let n = io::stdin().read(&mut buf).expect("Failed to read from stdin");
if n == 0 {
break;
}
let mut out = [0u8; 1024];
// infallible: `out` is as long as `buf`, so it cannot be shorter than the `n` bytes read
// into it, which is the only length `OutputBufferTooSmall` could complain about.
let written = cipher.do_update_out(&buf[..n], &mut out).unwrap();
helpers::write_bytes_or_hex(&out[..written], output_hex);
}
// infallible: Ascon-AEAD128 holds nothing back, so the inline final is only the 16-byte tag.
let mut tail = [0u8; 16];
let tail_len = cipher.do_final_out(&mut tail).unwrap();
helpers::write_bytes_or_hex(&tail[..tail_len], output_hex);
if output_hex {
println!();
}
}

/// Encryption under a caller-supplied nonce, which nothing is written to the stream for. This
/// drives the inherent [`AsconAead128`] API rather than the `AEADCipherEncryptor` pair because the
/// pair generates its own nonce by construction -- `do_encrypt_init` owns that choice, which is
/// the point of the trait -- and has no caller-supplied-nonce constructor to call here.
fn aead128_encrypt_stream_with_explicit_nonce(
key: &KeyMaterial<16>,
nonce: &[u8; 16],
ad_opt: Option<&[u8]>,
output_hex: bool,
) {
let mut cipher = AsconAead128::new_encrypting(key, nonce, ad_opt).unwrap_or_else(|e| {
eprintln!("Error: couldn't start encryption: {e:?}");
exit(-1);
});
let mut buf = [0u8; 1024];
loop {
let n = io::stdin().read(&mut buf).expect("Failed to read from stdin");
if n == 0 {
break;
}
cipher.do_encrypt_update(&mut buf[..n]);
helpers::write_bytes_or_hex(&buf[..n], output_hex);
}
let tag = cipher.do_encrypt_final();
helpers::write_bytes_or_hex(&tag, output_hex);
if output_hex {
println!();
}
}

/// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached.
/// [`AsconAead128Decryptor`] holds the last 16 bytes it has seen back itself, releasing everything
/// before them as soon as it is known not to be part of the tag; at EOF
/// [`SymmetricCipherDecryptor::do_final`] checks what it held back as the tag.
fn aead128_decrypt_stream(
key: &KeyMaterial<16>,
nonce: Option<&[u8; 16]>,
ad_opt: Option<&[u8]>,
output_hex: bool,
) {
const CHUNK: usize = 1024;
let nonce = match nonce {
Some(nonce) => *nonce,
None => {
let mut nonce = [0u8; 16];
if let Err(e) = io::stdin().read_exact(&mut nonce) {
if e.kind() == io::ErrorKind::UnexpectedEof {
eprintln!("Error: ciphertext is shorter than the 16-byte nonce.");
exit(-1);
}
panic!("Failed to read from stdin: {e}");
}
nonce
}
};

let mut cipher = AsconAead128Decryptor::do_decrypt_init(key, &nonce).unwrap_or_else(|e| {
eprintln!("Error: couldn't start decryption: {e:?}");
exit(-1);
});
if let Some(ad) = ad_opt {
// infallible: as on the encrypt side, no ciphertext has been fed in yet.
cipher.do_update_aad(ad).unwrap();
}

let mut buf = [0u8; CHUNK];
let mut out = [0u8; CHUNK];
loop {
let n = io::stdin().read(&mut buf).expect("Failed to read from stdin");
if n == 0 {
break;
}
// infallible: the decryptor releases at most what it has held back (16 bytes) plus what
// it is given, less the 16 it keeps, so never more than the `n <= CHUNK` bytes read.
let written = cipher.do_update_out(&buf[..n], &mut out).unwrap();
helpers::write_bytes_or_hex(&out[..written], output_hex);
}

match cipher.do_final() {
Ok((last, last_len)) => {
helpers::write_bytes_or_hex(&last[..last_len], output_hex);
if output_hex {
println!();
}
}
Err(SymmetricCipherError::DecryptionFailed) => {
eprintln!("Error: ciphertext is shorter than the 16-byte tag.");
exit(-1);
}
Err(_) => {
eprintln!("Error: Ascon-AEAD128 authentication failed.");
exit(-1);
}
}
}
Loading
Loading