Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1a7c0b5
Add BLAKE2b-512, BLAKE2s-256 (RFC 7693) and BLAKE3 (#123)
Oct 25, 2026
36835f4
blake2, blake3, cli, factory: address the review of #180
Nov 2, 2026
b8e3921
blake2, blake3, factory: follow feature/key-rework-memory's trait cha…
Nov 2, 2026
950765c
blake2, blake3, cli: the adaptations to release/0.1.4alpha, carried o…
dghgit Nov 5, 2026
0f9fbce
blake2, blake3: the MAC workers no longer suspend
dghgit Nov 9, 2026
412569a
blake2, blake3, factory, cli: drop BLAKE2's kk byte, measured stack t…
Nov 9, 2026
26960d8
blake2, blake3, factory, cli, mem_usage_benches: port to the MAC rework
dghgit Nov 10, 2026
0b28a56
blake2, blake3, factory: the MAC workers borrow their key
dghgit Nov 10, 2026
393ae75
blake2: resume refuses a t in the upper half of its range; state buff…
dghgit Nov 10, 2026
90a8c51
blake3: tighter resume checks, the squeezer state recomputes its buff…
dghgit Nov 10, 2026
5014bcc
blake2, blake3: public docs say what a caller observes
dghgit Nov 10, 2026
4f0c3d8
blake2, blake3: re-measured stack tables
dghgit Nov 10, 2026
9c6b4fe
blake2: note the equivalent mutant on the t bound
dghgit Nov 10, 2026
d279232
blake3: note the surviving mutant on the squeezer's buffer recompute
dghgit Nov 10, 2026
5626d71
blake2, blake3, cli: the resume compares the serialized h, one error …
dghgit Nov 10, 2026
d3adf8e
core, hmac, sha3, blake2, blake3: one key draw for every MAC key's ge…
dghgit Nov 10, 2026
c5b23f6
cli: one MAC key loader, with the length bound as a parameter
dghgit Nov 10, 2026
667ced1
blake2: the state layout is a code comment, and read_state's doc defe…
dghgit Nov 10, 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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ bouncycastle-camellia = { path = "./crypto/camellia" }
bouncycastle-aria = { path = "./crypto/aria" }
bouncycastle-ascon = { path = "./crypto/ascon" }
bouncycastle-base64 = { path = "./crypto/base64" }
bouncycastle-blake2 = { path = "./crypto/blake2" }
bouncycastle-blake3 = { path = "./crypto/blake3" }
bouncycastle-chacha20 = { path = "./crypto/chacha20" }
bouncycastle-chacha20-poly1305-aead = { path = "./crypto/chacha20-poly1305-aead" }
bouncycastle-cipher = { path = "./crypto/cipher" }
Expand Down Expand Up @@ -73,6 +75,8 @@ bouncycastle-camellia.workspace = true
bouncycastle-aria.workspace = true
bouncycastle-ascon.workspace = true
bouncycastle-base64.workspace = true
bouncycastle-blake2.workspace = true
bouncycastle-blake3.workspace = true
bouncycastle-chacha20.workspace = true
bouncycastle-chacha20-poly1305-aead.workspace = true
bouncycastle-cipher.workspace = true
Expand Down
52 changes: 52 additions & 0 deletions cli/src/blake2_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! BLAKE2b-512 and BLAKE2s-256 (RFC 7693): the unkeyed hashes and their built-in keyed (MAC) mode.

use bouncycastle::blake2::{BLAKE2b_512, BLAKE2b_512_MACKey, BLAKE2s_256, BLAKE2s_256_MACKey};
use bouncycastle::core::symmetric_key::FromAnyKeyMaterial;

use crate::helpers::{load_mac_key, stream_hash};
use crate::mac_cmd::do_mac_with_key;

#[allow(non_camel_case_types)]
pub(crate) enum BLAKE2Variant {
BLAKE2b_512,
BLAKE2s_256,
}

/// BLAKE2b-512 / BLAKE2s-256 of stdin, streamed in 1 KiB pieces.
pub(crate) fn blake2_cmd(variant: BLAKE2Variant, output_hex: bool) {
match variant {
BLAKE2Variant::BLAKE2b_512 => stream_hash(BLAKE2b_512::new(), output_hex),
BLAKE2Variant::BLAKE2s_256 => stream_hash(BLAKE2s_256::new(), output_hex),
}
}

/// Keyed BLAKE2b-512 / BLAKE2s-256 (RFC 7693 s. 3.3) of stdin: compute a tag, or with `--verify`
/// check one and report the result in the exit status.
pub(crate) fn blake2_mac_cmd(
variant: BLAKE2Variant,
key: &Option<String>,
key_file: &Option<String>,
verify_val: &Option<String>,
output_hex: bool,
) {
// RFC 7693 s. 2.1 table, "Key bytes": "0 <= kk <= 64" (BLAKE2b) / "0 <= kk <= 32" (BLAKE2s).
// kk = 0 is the unkeyed hash, which is the other command, so a MAC key is at least 1 byte.
//
// The allow-weak form, as the HMAC and KMAC commands use: a CLI is used for test vectors and
// scripting, where a short or all-zero key is a legitimate thing to want.
match variant {
BLAKE2Variant::BLAKE2b_512 => {
let key = load_mac_key(key, key_file, Some(("BLAKE2b-512-MAC", 1..=64)));
// infallible: a 1..=64-byte MACKey (or Zeroized) key is exactly what the key object
// accepts once the strength check is skipped.
let mac_key = BLAKE2b_512_MACKey::from_key_material_allow_weak(&key).unwrap();
do_mac_with_key(&mac_key, verify_val, output_hex);
}
BLAKE2Variant::BLAKE2s_256 => {
let key = load_mac_key(key, key_file, Some(("BLAKE2s-256-MAC", 1..=32)));
// infallible: as above, with the 1..=32-byte range of BLAKE2s.
let mac_key = BLAKE2s_256_MACKey::from_key_material_allow_weak(&key).unwrap();
do_mac_with_key(&mac_key, verify_val, output_hex);
}
}
}
74 changes: 74 additions & 0 deletions cli/src/blake3_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! BLAKE3 (BLAKE3 specification, version 20211102173700): the `hash`, `keyed_hash` and
//! `derive_key` modes, each with output of any length.

use std::process::exit;

use bouncycastle::blake3::{BLAKE3, BLAKE3DeriveKey, BLAKE3KeyedHashKey};
use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey};
use bouncycastle::core::traits::MACGenerator;

use crate::helpers::{load_mac_key, squeeze_to_stdout, stream_stdin, stream_xof};
use crate::mac_cmd::do_mac_with_key;

/// BLAKE3 spec s. 2.3: `keyed_hash` "takes an input of any length together with a 256-bit key".
/// The crate has no public constant for it.
const BLAKE3_KEY_LEN: usize = 32;

/// The default-length BLAKE3 output: BLAKE3 spec s. 2.1, "The chaining value of the root node,
/// encoded as 32 bytes in little-endian order, is the default-length BLAKE3 hash of the input."
pub(crate) const BLAKE3_DEFAULT_OUT_LEN: usize = 32;

/// BLAKE3 `hash` mode of stdin, streamed in 1 KiB pieces, squeezing `output_len` bytes.
pub(crate) fn blake3_cmd(output_len: usize, output_hex: bool) {
stream_xof(BLAKE3::new(), output_len, output_hex);
}

/// BLAKE3 `derive_key` mode: the key material is streamed from stdin, the context string is the
/// argument, and `output_len` bytes of derived key are written out.
///
/// BLAKE3 spec s. 6.2: the context string "should be hardcoded, globally unique, and
/// application-specific"; this command takes it as an argument only so that scripts can name the
/// application's fixed string.
pub(crate) fn blake3_derive_key_cmd(context: &str, output_len: usize, output_hex: bool) {
stream_xof(BLAKE3DeriveKey::new(context.as_bytes()), output_len, output_hex);
}

/// BLAKE3 `keyed_hash` mode of stdin: write `output_len` bytes of keyed output, or with `--verify`
/// check a 32-byte tag and report the result in the exit status.
pub(crate) fn blake3_keyed_hash_cmd(
key: &Option<String>,
key_file: &Option<String>,
verify_val: &Option<String>,
output_len: usize,
output_hex: bool,
) {
// Verification goes through MACVerifier::do_verify_final, which compares the default 32-byte
// tag in constant time; an extended output length has no
// meaning there, so refuse the combination rather than silently ignore --length.
if verify_val.is_some() && output_len != BLAKE3_DEFAULT_OUT_LEN {
eprintln!(
"Error: --verify checks the {BLAKE3_DEFAULT_OUT_LEN}-byte tag and cannot be combined \
with --length."
);
exit(-1);
}

let key =
load_mac_key(key, key_file, Some(("BLAKE3-KeyedHash", BLAKE3_KEY_LEN..=BLAKE3_KEY_LEN)));
// The allow-weak form, as the HMAC and KMAC commands use: a CLI is used for test vectors and
// scripting, where a low-entropy or all-zero key is a legitimate thing to want.
// infallible: a 32-byte MACKey (or Zeroized) key is exactly what the key object accepts once
// the strength check is skipped.
let mac_key = BLAKE3KeyedHashKey::from_key_material_allow_weak(&key).unwrap();

if verify_val.is_some() {
// do_mac_with_key streams stdin and, given a value to verify, exits with the result.
do_mac_with_key(&mac_key, verify_val, output_hex);
return;
}

// BLAKE3 spec s. 2.6: the keyed output stream, of which the MAC tag is the first 32 bytes.
let mut mac = mac_key.create_generator();
stream_stdin(|chunk| mac.do_update(chunk));
squeeze_to_stdout(mac.into_squeezer(), output_len, output_hex);
}
54 changes: 54 additions & 0 deletions cli/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use bouncycastle::hex;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use std::ops::RangeInclusive;
use std::process::exit;
use std::sync::atomic::{AtomicBool, Ordering};

Expand Down Expand Up @@ -239,6 +240,59 @@ pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] {
bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes)
}

/// Warns on stderr, then lets the caller carry on, if `key` is empty or all zero bytes. A CLI is
/// used for test vectors and scripting, where such a key is a legitimate thing to want, so this
/// is a warning and not a refusal. Shared by every MAC key loader so they all say the same thing.
///
/// This looks at the bytes, not at [`KeyType::Zeroized`]: the secret readers above tag what they
/// read `Unknown` inside a hazardous-operations closure, which never produces `Zeroized`.
pub(crate) fn warn_if_zeroized_key(key: &impl KeyMaterialTrait) {
if bouncycastle_utils::ct::ct_eq_zero_bytes(key.ref_to_bytes()) {
eprintln!(
"Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure."
);
}
}

/// The MAC key from `--key-file` or `--key` (hex), tagged as a MAC key with no stated strength
/// (a CLI is used for test vectors and scripting, so the commands build their keys allow-weak).
/// An all-zero key is accepted with a warning, see [`warn_if_zeroized_key`].
///
/// `len` is the algorithm's bound on the key length where it has one (BLAKE2b 1 to 64 bytes,
/// BLAKE2s 1 to 32, BLAKE3 exactly 32), refused here with a message naming the limit rather than
/// as the constructor's error. HMAC and KMAC pass `None`: HMAC hashes a key longer than its block
/// (RFC 2104 Section 2) and KMAC encodes the key's length (SP 800-185 Section 4.2), so any length
/// the secret buffer holds is accepted.
pub(crate) fn load_mac_key(
key: &Option<String>,
key_file: &Option<String>,
len: Option<(&str, RangeInclusive<usize>)>,
) -> SecretBytes {
let mut key = load_secret(key, key_file, "key");
if let Some((alg_name, range)) = len {
let got = key.key_len();
if !range.contains(&got) {
let (min_len, max_len) = (range.start(), range.end());
if min_len == max_len {
eprintln!("Error: {alg_name} requires a {min_len}-byte key; got {got} bytes.");
} else {
eprintln!(
"Error: {alg_name} requires a key of {min_len} to {max_len} bytes; got {got} bytes."
);
}
exit(-1);
}
}
warn_if_zeroized_key(&key);
do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else(
|e| {
eprintln!("Error: couldn't load the key: {e:?}");
exit(-1);
},
);
key
}

// ---- stdin as data ---------------------------------------------------------------------------

/// One read from stdin into `buf`; a read error is reported, not a panic. Returns 0 at the end.
Expand Down
39 changes: 10 additions & 29 deletions cli/src/mac_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@

use crate::helpers::block_mode_helpers::{load_iv, load_key_object_typed};
use crate::helpers::{
SecretBytes, decode_hex_arg, load_secret, stream_stdin, stream_xof, write_bytes_or_hex,
SecretBytes, decode_hex_arg, load_mac_key, stream_stdin, stream_xof, write_bytes_or_hex,
write_stdout,
};
use crate::sha3_cmd::{MAX_FIXED_OUTPUT_LEN, Strength};
use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
use bouncycastle::cipher::modes::{CMAC, GMAC};
use bouncycastle::core::errors::MACError;
use bouncycastle::core::hazmat::do_hazardous_operations;
use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType};
use bouncycastle::core::symmetric_key::{
FromAnyKeyMaterial, FromKeyMaterial, MacGenerationKey, MacVerificationKey,
Expand Down Expand Up @@ -43,27 +42,6 @@ pub(crate) enum HMACVariant {
SM3,
}

/// The MAC key from `--key-file` or `--key`, tagged as a MAC key. Any length the secret buffer
/// holds is accepted: HMAC hashes a key longer than its block (RFC 2104 Section 2), and KMAC
/// encodes the key's length (SP 800-185 Section 4.2).
fn load_mac_key(key: &Option<String>, key_file: &Option<String>) -> SecretBytes {
let mut key = load_secret(key, key_file, "key");
if key.key_type() == KeyType::Zeroized {
// Same stance as the cipher loaders: warn, then do what was asked. A CLI is used for
// test vectors and scripting, where an all-zero key is a legitimate thing to want.
eprintln!(
"Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure."
);
}
do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else(
|e| {
eprintln!("Error: couldn't load the key: {e:?}");
exit(-1);
},
);
key
}

fn mac_key_error(e: MACError) -> ! {
eprintln!("Error: couldn't build the MAC key: {e:?}");
exit(-1)
Expand All @@ -88,7 +66,7 @@ pub(crate) fn mac_cmd(
verify_val: &Option<String>,
output_hex: bool,
) {
let key = load_mac_key(key, key_file);
let key = load_mac_key(key, key_file, None);
match hmac_variant {
HMACVariant::SHA224 => hmac::<28, SHA224>(&key, verify_val, output_hex),
HMACVariant::SHA256 => hmac::<32, SHA256>(&key, verify_val, output_hex),
Expand Down Expand Up @@ -133,7 +111,7 @@ pub(crate) fn kmac_cmd(
);
exit(-1);
}
let key = load_mac_key(key, key_file);
let key = load_mac_key(key, key_file, None);
match strength {
Strength::Bits128 => {
let x = KMACXOF128::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e));
Expand Down Expand Up @@ -187,7 +165,7 @@ pub(crate) fn kmacxof_cmd(
key_file: &Option<String>,
output_hex: bool,
) {
let key = load_mac_key(key, key_file);
let key = load_mac_key(key, key_file, None);
match strength {
Strength::Bits128 => {
let x = KMACXOF128::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e));
Expand Down Expand Up @@ -354,7 +332,7 @@ pub(crate) fn poly1305_cmd(
verify_val: &Option<String>,
output_hex: bool,
) {
let key = load_mac_key(key, key_file);
let key = load_mac_key(key, key_file, None);
// No key policy, as the other MAC commands take their allow-weak forms: a CLI is used for test
// vectors and scripting, where a weak or all-zero key is a legitimate thing to want.
let Ok(bytes) = <&[u8; poly1305::KEY_LEN]>::try_from(key.ref_to_bytes()) else {
Expand All @@ -371,8 +349,11 @@ pub(crate) fn poly1305_cmd(
}

/// [`do_mac`] over the workers a reusable MAC key hands out.
fn do_mac_with_key<const N: usize, K>(key: &K, verify_val: &Option<String>, output_hex: bool)
where
pub(crate) fn do_mac_with_key<const N: usize, K>(
key: &K,
verify_val: &Option<String>,
output_hex: bool,
) where
K: MacGenerationKey<N> + MacVerificationKey<N>,
{
do_mac(|| key.create_generator(), || key.create_verifier(), verify_val, output_hex)
Expand Down
Loading
Loading