Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43bc846
Added ci.yml to run unit tests, but also check format, run clippy, an…
officialfrancismendoza Jul 15, 2026
27e2574
Added badges for new workflows and existing style workflow (#45)
officialfrancismendoza Jul 16, 2026
2a17ce2
Add initial rust-build.yml (#45)
officialfrancismendoza Jul 16, 2026
6116291
Add initial rust-docs.yml (#45)
officialfrancismendoza Jul 16, 2026
0bd460f
Add initial rust-test.yml (#45)
officialfrancismendoza Jul 16, 2026
1c888b9
Added workflow dispatch (#45)
officialfrancismendoza Jul 16, 2026
98d73f4
Initial add of ASCON boilerplate
officialfrancismendoza Jun 15, 2026
61645e7
Initial push, passes first test cases
officialfrancismendoza Jun 15, 2026
e4d513e
Updated to rebase off of main, post release/0.1.2alpha merge
officialfrancismendoza Jun 15, 2026
3cfbfe9
Greatly expanded testing suite, including mutant tests, with successf…
officialfrancismendoza Jun 16, 2026
b1d4972
Greatly expanded testing suite, including mutant tests, with successf…
officialfrancismendoza Jun 16, 2026
486569e
Added summary.md to comprehensively document changes within crate for…
officialfrancismendoza Jun 16, 2026
3118475
Fixed formatting for rustfmt
officialfrancismendoza Jun 16, 2026
8fec3c4
Changed testing for ASCON PR to utilize bc-test-data and wycheproof. …
officialfrancismendoza Jun 19, 2026
ce63f1c
Corrected formatting with rustfmt
officialfrancismendoza Jun 19, 2026
0f15f1f
ERROR. Resolved some further merge conflicts, but ran into errors res…
officialfrancismendoza Jul 20, 2026
d8f10c5
SUCCESS. Enormous refactor to use main Secret<T>, XOF::absorb made fa…
officialfrancismendoza Jul 20, 2026
4526aed
SUCCESS. Enormous refactor to use main Secret<T>, XOF::absorb made fa…
officialfrancismendoza Jul 20, 2026
6d60f6d
Update all crates inherit version from version.workspace
tad-fr Jul 24, 2026
296a874
Deduplicate internal dependencies version in core cargo.toml
tad-fr Jul 24, 2026
096796d
Stop tracking .claude/*
officialfrancismendoza Jul 27, 2026
366363c
tweaks to centralized versioning
ounsworth Jul 28, 2026
43e2536
rustfmt
ounsworth Jul 28, 2026
b9aa184
Merge PR #62
ounsworth Jul 28, 2026
e4bf0b8
Merge branch 'feature/officialfrancismendoza/45-github-action-unit-te…
ounsworth Jul 29, 2026
eadea3a
Merge PR #59: run unit tests on github PR
ounsworth Jul 29, 2026
7c967d2
Merge branch 'release/0.1.3alpha` into 'feature/officialfrancismendoz…
ounsworth Jul 29, 2026
daa12ec
tweaks
ounsworth Jul 31, 2026
7810b56
renamed State to StateMachine
ounsworth Aug 2, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ mutants.out*/

.idea/
.vscode/

.claude/*
18 changes: 10 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ edition = "2024"
[workspace.dependencies]

# *** Internal Dependencies ***
bouncycastle = { path = "./", version = "0.1.2" }
bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.2" }
bouncycastle-core = { path = "crypto/core", version = "0.1.2" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.2" }
bouncycastle-factory = { path = "./crypto/factory", version = "0.1.2" }
bouncycastle-hex = { path = "./crypto/hex", version = "0.1.2" }
bouncycastle-hkdf = { path = "./crypto/hkdf", version = "0.1.2" }
bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.2" }
bouncycastle = { path = "./", version = "0.1.1" }
bouncycastle-ascon = { path = "./crypto/ascon", version = "0.1.2" }
bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.1" }
bouncycastle-core = { path = "crypto/core", version = "0.1.1" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.1" }
bouncycastle-factory = { path = "./crypto/factory", version = "0.1.1" }
bouncycastle-hex = { path = "./crypto/hex", version = "0.1.1" }
bouncycastle-hkdf = { path = "./crypto/hkdf", version = "0.1.1" }
bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.1" }
bouncycastle-mlkem = { path = "./crypto/mlkem", version = "0.1.2" }
bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory", version = "0.1.2" }
bouncycastle-mldsa = { path = "./crypto/mldsa", version = "0.1.2" }
Expand All @@ -40,6 +41,7 @@ version = "0.1.2"
edition.workspace = true

[dependencies]
bouncycastle-ascon.workspace = true
bouncycastle-base64.workspace = true
bouncycastle-core.workspace = true
bouncycastle-factory.workspace = true
Expand Down
152 changes: 152 additions & 0 deletions cli/src/ascon_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::io::{Read, Write};
use std::process::exit;
use std::{fs, io};

use bouncycastle::ascon::ascon_aead128::AsconAead128;
use bouncycastle::ascon::ascon_cxof128::AsconCXof128;
use bouncycastle::ascon::ascon_hash256::AsconHash256;
use bouncycastle::ascon::ascon_xof128::AsconXof128;
use bouncycastle::core::traits::{Hash, XOF};
use bouncycastle::hex;

/// Write `data` to stdout, either as hex or raw binary, followed by a newline.
fn emit(data: &[u8], output_hex: bool) {
if output_hex {
for b in data.iter() {
print!("{b:02x}");
}
} else {
io::stdout().write_all(data).unwrap();
}
println!();
}

/// Read all of stdin into a Vec.
fn read_stdin() -> Vec<u8> {
let mut data = Vec::new();
io::stdin().read_to_end(&mut data).expect("Failed to read from stdin");
data
}

/// Load a hex string or a binary 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 {
fs::read(file).unwrap_or_else(|e| {
eprintln!("Error: failed to read {label} file: {e}");
exit(-1)
})
} 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 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)
})
}

/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest.
pub(crate) fn hash256_cmd(output_hex: bool) {
let mut h = AsconHash256::new();
let mut buf = [0u8; 1024];
let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
while bytes_read != 0 {
h.do_update(&buf[..bytes_read]);
bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
}
let out = h.do_final();
emit(&out, output_hex);
}

/// Ascon-XOF128 of stdin, producing `output_len` bytes. Streaming absorb.
pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) {
let mut x = AsconXof128::new();
let mut buf = [0u8; 1024];
let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
while bytes_read != 0 {
// Absorb cannot fail here: we only absorb before any squeeze.
x.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible");
bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
}
let out = x.squeeze(output_len);
emit(&out, 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 mut x = AsconCXof128::with_customization(&z);
let mut buf = [0u8; 1024];
let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
while bytes_read != 0 {
// Absorb cannot fail here: we only absorb before any squeeze.
x.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible");
bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin");
}
let out = x.squeeze(output_len);
emit(&out, output_hex);
}

/// Ascon-AEAD128 of stdin. Encrypts (stdin = plaintext, output = ciphertext||tag) or, with
/// `decrypt`, decrypts (stdin = ciphertext||tag, output = plaintext). Decryption exits with a
/// non-zero status if the authentication tag does not verify.
#[allow(clippy::too_many_arguments)]
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 = require_16(load_bytes(key, key_file, "key"), "key");
let nonce = require_16(load_bytes(nonce, nonce_file, "nonce"), "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()) };

let input = read_stdin();

if decrypt {
if input.len() < 16 {
eprintln!("Error: ciphertext is shorter than the 16-byte tag.");
exit(-1);
}
let mut out = vec![0u8; input.len() - 16];
match AsconAead128::decrypt(&key, &nonce, ad_opt, &input, &mut out) {
Ok(n) => {
out.truncate(n);
emit(&out, output_hex);
}
Err(_) => {
eprintln!("Error: Ascon-AEAD128 authentication failed.");
exit(-1);
}
}
} else {
let mut out = vec![0u8; input.len() + 16];
let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &input, &mut out);
out.truncate(n);
emit(&out, output_hex);
}
}
83 changes: 83 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod ascon_cmd;
mod encoders_cmd;
mod helpers;
mod hkdf_cmd;
Expand Down Expand Up @@ -124,6 +125,76 @@ enum Subcommands {
x: bool,
},

/// Perform Ascon-Hash256 of the content provided on stdin.
/// Supports streaming update for low memory footprint.
AsconHash256 {
#[arg(short)]
/// Output the digest in hex format.
x: bool,
},

/// Perform Ascon-XOF128 of the content provided on stdin. Requires the output length in bytes.
/// Supports streaming update for low memory footprint.
AsconXOF128 {
/// Length of the output in bytes.
length: usize,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// Perform Ascon-CXOF128 of the content provided on stdin. Requires the output length in bytes.
/// Supports streaming update for low memory footprint.
AsconCXOF128 {
/// Length of the output in bytes.
length: usize,

/// Customization string in hex (optional).
#[arg(long)]
customization: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin.
/// Encrypts by default (stdin = plaintext, output = ciphertext||tag); with --decrypt the
/// reverse. Decryption fails with a non-zero exit status if the tag does not verify.
/// Note: in production uses, secrets should not be passed on the command-line because they get
/// logged in shell history. Use the file-based input instead.
AsconAEAD128 {
/// The 128-bit key in hex.
/// The `key_file` option is preferred to avoid leaving key material in command history.
#[arg(long)]
key: Option<String>,

/// A file containing the 128-bit key in binary.
#[arg(long)]
key_file: Option<String>,

/// The 128-bit nonce in hex. Must be unique per encryption under a given key.
#[arg(long)]
nonce: Option<String>,

/// A file containing the 128-bit nonce in binary.
#[arg(long)]
nonce_file: Option<String>,

/// Associated data in hex (authenticated but not encrypted).
#[arg(long)]
ad: Option<String>,

/// Decrypt instead of encrypt.
#[arg(short, long)]
decrypt: bool,

#[arg(short)]
/// Output in hex format.
x: bool,
},

/// Perform HMAC-SHA256 of the content provided on stdin.
/// Supports streaming update for low memory footprint.
/// Note: in production uses, secrets should not be passed on the command-line because they get
Expand Down Expand Up @@ -531,6 +602,18 @@ fn main() {
Some(Subcommands::SHAKE256 { length, x }) => {
sha3_cmd::shake_cmd(256, *length, *x);
}
Some(Subcommands::AsconHash256 { x }) => {
ascon_cmd::hash256_cmd(*x);
}
Some(Subcommands::AsconXOF128 { length, x }) => {
ascon_cmd::xof128_cmd(*length, *x);
}
Some(Subcommands::AsconCXOF128 { length, customization, x }) => {
ascon_cmd::cxof128_cmd(customization, *length, *x);
}
Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, decrypt, x }) => {
ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, *decrypt, *x);
}
Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => {
mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x)
}
Expand Down
27 changes: 27 additions & 0 deletions crypto/ascon/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "bouncycastle-ascon"
version = "0.1.2"
edition.workspace = true

[features]
# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot cipher APIs, mirroring the
# `std` feature of `bouncycastle-core`. On by default; a future `--no-default-features` build is
# what will let the crate move toward `#![no_std]`.
default = ["std"]
std = ["bouncycastle-core/std"]

[dependencies]
bouncycastle-core.workspace = true
bouncycastle-rng.workspace = true
bouncycastle-utils.workspace = true

[dev-dependencies]
bouncycastle-core-test-framework.workspace = true
bouncycastle-hex.workspace = true
bouncycastle-rng.workspace = true
criterion.workspace = true
serde_json = "1.0"

[[bench]]
name = "ascon_benches"
harness = false
Loading
Loading