Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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
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
29 changes: 29 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,35 @@
* 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`).
* `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<...>`, with AAD updates, exact `update_out_len`, detached
tags, one-shot helpers and a `FINAL_LEN` flush buffer for implementations that hold data back.
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
tested through `bouncycastle-core` + `bouncycastle-ascon`) reports 116 mutants, 95 caught, 19
unviable and 2 missed, the two being `written + final_len` -> `written - final_len` in
`encrypt_out_rng`, equivalent while every implementor has `FINAL_LEN = 0`.
* The same pair carries the inline `ciphertext || tag` layout that most wire formats and files
use, as four default methods rather than a separate adapter type: `tagged_encrypt` /
`tagged_do_aead_encrypt_final` append the tag to the ciphertext stream, and `tagged_decrypt` /
`tagged_do_aead_decrypt_final` take it back off the end of one.
* 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
323 changes: 323 additions & 0 deletions cli/src/ascon_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
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::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SecurityStrength};
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 (`tagged_do_aead_encrypt_final`), 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 has FINAL_LEN = 0, so `tail` only has to hold the 16-byte tag.
let mut tail = [0u8; 16];
let tail_len = cipher.tagged_do_aead_encrypt_final(&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.
/// Everything but the last 16 bytes seen is released to [`AsconAead128Decryptor`] as soon as it is
/// known not to be part of the tag; what is left at EOF goes to
/// [`AEADCipherDecryptor::tagged_do_aead_decrypt_final`], which decrypts any ciphertext still in it
/// and then checks the tag.
fn aead128_decrypt_stream(
key: &KeyMaterial<16>,
nonce: Option<&[u8; 16]>,
ad_opt: Option<&[u8]>,
output_hex: bool,
) {
const CHUNK: usize = 1024;
const TAG_LEN: usize = 16;
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();
}

// The tag is the last TAG_LEN bytes of the stream, and nothing says where the stream ends
// until it does, so the last TAG_LEN bytes seen are always held back in `tail` and only
// released once something newer has arrived behind them. At EOF whatever is still in `tail`
// is the tag, which `tagged_do_aead_decrypt_final` checks.
let mut tail = [0u8; TAG_LEN];
let mut tail_len = 0usize;
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;
}
let total = tail_len + n;
if total <= TAG_LEN {
// Everything seen so far might still be the tag.
tail[tail_len..total].copy_from_slice(&buf[..n]);
tail_len = total;
continue;
}

// Release the part of the old tail that is now known not to be the tag, then as much of
// the new input as is also known not to be; two calls over what is one contiguous run of
// ciphertext, which is the same to the cipher as one call over both.
let releasable = total - TAG_LEN;
let from_tail = tail_len.min(releasable);
let from_new = releasable - from_tail;
// infallible on both: `out` is CHUNK bytes and neither slice is longer than `buf`, and
// Ascon-AEAD128 writes exactly what it is given.
if from_tail > 0 {
let written = cipher.do_update_out(&tail[..from_tail], &mut out).unwrap();
helpers::write_bytes_or_hex(&out[..written], output_hex);
}
if from_new > 0 {
let written = cipher.do_update_out(&buf[..from_new], &mut out).unwrap();
helpers::write_bytes_or_hex(&out[..written], output_hex);
}

// Whatever was not released is the new tail: the end of the old one, then the end of this
// read. Those are exactly TAG_LEN bytes, since `total - releasable == TAG_LEN`.
let mut new_tail = [0u8; TAG_LEN];
let kept = tail_len - from_tail;
new_tail[..kept].copy_from_slice(&tail[from_tail..tail_len]);
new_tail[kept..].copy_from_slice(&buf[from_new..n]);
tail = new_tail;
tail_len = TAG_LEN;
}

match cipher.tagged_do_aead_decrypt_final(&tail[..tail_len], &mut out) {
Ok(last_len) => {
helpers::write_bytes_or_hex(&out[..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