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
5ab0043
Add KeyWrapper/KeyUnwrapper traits for SP 800-38F key wrap (#150)
Sep 30, 2026
95f67d9
Add AES-KW and AES-KWP (SP 800-38F) over ElectronicCodeBook (#150)
Sep 30, 2026
1362d0a
core: KeyWrapper/KeyUnwrapper are workers over a borrowed key state; …
dghgit Oct 26, 2026
02e8652
core-test-framework: the key-wrap suite runs a key object through the…
dghgit Oct 26, 2026
60d41f7
cipher: KW and KWP as mode types over the key path; Kw and Kwp borrow…
dghgit Oct 26, 2026
56e43eb
aes: AES*Key<KW> and AES*Key<KWP> wrap keys; the AES_KW_* / AES_KWP_*…
dghgit Oct 26, 2026
dc47bf5
cli: the aes*-kw and aes*-kwp commands take -d encrypt|decrypt and bu…
dghgit Oct 26, 2026
c0da939
cipher: the key-wrap workers' bare constructors leave the public API
dghgit Oct 31, 2026
1c45b85
150-key-wrap: the merge-time adaptations to feature/key-rework-memory…
dghgit Nov 3, 2026
38427f5
150-key-wrap: the adaptations to release/0.1.4alpha, carried over the…
dghgit Nov 5, 2026
9857503
core: KeyWrapper and KeyUnwrapper are dyn-compatible, as the other wo…
dghgit Nov 5, 2026
e09776d
cipher: the key-wrap `_out` methods zeroize the whole output buffer b…
dghgit Nov 5, 2026
1a5de48
core-test-framework: the key-wrap suite checks that a refused `_out` …
dghgit Nov 5, 2026
6672352
cipher: the key-wrap length helpers leave the public API; the modes d…
dghgit Nov 5, 2026
687c21e
core: the key-wrap roles gain the allocating `wrap` / `unwrap` one-sh…
dghgit Nov 5, 2026
81d0949
core-test-framework: the key-wrap suite covers the allocating `wrap` …
dghgit Nov 5, 2026
6ffae4c
aes: the AES_KW_128 run-time-length example uses the allocating pair
dghgit Nov 5, 2026
198577a
cli: the aes*-kw and aes*-kwp commands call the key's `wrap` / `unwrap`
dghgit Nov 5, 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 alpha_0.1.4_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Major features

* CHACHA20 -- ChaCha20, Poly1305 and ChaCha20-Poly1305 (RFC 8439).
* AES Key Wrap -- AES_KW and AES_KWP (NIST SP 800-38F; RFC 3394 / RFC 5649) over any 128-bit block permutation,
with the `KeyWrapper` / `KeyUnwrapper` core traits and the `aes*-kw` / `aes*-kwp` CLI commands.

## Minor features / bug fixes

Expand Down
176 changes: 176 additions & 0 deletions cli/src/aes_kw_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//! AES key wrap (KW, NIST SP 800-38F Sec 6.2 / RFC 3394) and key wrap with padding (KWP, Sec 6.3
//! / RFC 5649): `aes{128,192,256}-{kw,kwp} -d encrypt|decrypt`, stdin to stdout. `encrypt` wraps
//! and `decrypt` unwraps: the spelling is shared with every other cipher command, and SP 800-38F
//! itself calls the two halves authenticated encryption and decryption (KW-AE, KW-AD).
//!
//! Key loading is shared with the other AES commands ([`crate::helpers::block_mode_helpers::load_key_object`]),
//! so `--key` / `--key-file` behave identically. Everything else differs from the streaming
//! commands, because key wrap is a one-shot algorithm:
//!
//! # Not streaming
//!
//! The wrapping function makes six passes over the whole input (SP 800-38F Algorithm 1), so there
//! is no way to emit output before all of stdin has been read. These commands read stdin to the
//! end and then write the result in one go. That is fine for what key wrap is for -- a key, or a
//! small encoded private key -- and the algorithm's own limits (below) keep it bounded.
//!
//! # No IV
//!
//! Key wrap is deterministic: the same input under the same KEK gives the same output, and there
//! is nothing to prepend or strip. The output is one 8-byte semiblock longer than the input for
//! KW, and the input rounded up to a multiple of 8 plus 8 for KWP.
//!
//! # Input lengths
//!
//! `aes*-kw` wraps 2 or more whole 8-byte semiblocks (16, 24, 32, ... bytes: the lengths of
//! AES, HMAC and other symmetric keys) and unwraps ciphertexts of 3 or more. `aes*-kwp` wraps
//! anything from 1 byte to 2^32 - 1 bytes and unwraps ciphertexts of 2 or more whole semiblocks.
//! Other lengths are rejected with an explanation rather than padded or truncated.
//!
//! # Unwrap fails closed
//!
//! Both algorithms are authenticated. A ciphertext that was tampered with, was wrapped under a
//! different KEK, or was produced by the other algorithm (KW vs KWP) makes `decrypt` exit
//! non-zero with nothing written to stdout. It does not say which of those it was; that is
//! deliberate.
//!
//! # Binary in, binary out
//!
//! stdin is read as binary. `-x` renders the output as hex. For hex input, pipe through
//! `hex-decode` first:
//!
//! ```text
//! echo -n 00112233445566778899aabbccddeeff | bc-rust hex-decode \
//! | bc-rust aes128-kw -d encrypt --key 000102030405060708090a0b0c0d0e0f -x
//! ```

use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object};
use crate::helpers::write_bytes_or_hex;
use bouncycastle::aes::{AES128Key, AES192Key, AES256Key};
use bouncycastle::cipher::modes::{KW, KWP};
use bouncycastle::core::errors::SymmetricCipherError;
use bouncycastle::core::symmetric_key::{KeyUnwrappingKey, KeyWrappingKey};
use std::io::Read;
use std::process::exit;

pub(crate) fn aes128_kw_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 16>(
direction,
&load_key_object::<AES128Key<KW>, 16>(key, key_file, "AES-128-KW"),
output_hex,
"AES-128-KW",
);
}

pub(crate) fn aes192_kw_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 24>(
direction,
&load_key_object::<AES192Key<KW>, 24>(key, key_file, "AES-192-KW"),
output_hex,
"AES-192-KW",
);
}

pub(crate) fn aes256_kw_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 32>(
direction,
&load_key_object::<AES256Key<KW>, 32>(key, key_file, "AES-256-KW"),
output_hex,
"AES-256-KW",
);
}

pub(crate) fn aes128_kwp_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 16>(
direction,
&load_key_object::<AES128Key<KWP>, 16>(key, key_file, "AES-128-KWP"),
output_hex,
"AES-128-KWP",
);
}

pub(crate) fn aes192_kwp_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 24>(
direction,
&load_key_object::<AES192Key<KWP>, 24>(key, key_file, "AES-192-KWP"),
output_hex,
"AES-192-KWP",
);
}

pub(crate) fn aes256_kwp_cmd(
direction: &CipherDirection,
key: &Option<String>,
key_file: &Option<String>,
output_hex: bool,
) {
run::<_, 32>(
direction,
&load_key_object::<AES256Key<KWP>, 32>(key, key_file, "AES-256-KWP"),
output_hex,
"AES-256-KWP",
);
}

/// Reads all of stdin, wraps or unwraps it under `kek`, and writes the result. `alg` names the
/// algorithm in error messages.
fn run<K, const KEK_LEN: usize>(direction: &CipherDirection, kek: &K, output_hex: bool, alg: &str)
where
K: KeyWrappingKey<KEK_LEN> + KeyUnwrappingKey<KEK_LEN>,
{
let mut input = Vec::new();
if let Err(e) = std::io::stdin().read_to_end(&mut input) {
eprintln!("Error: couldn't read stdin: {e}");
exit(-1);
}

let result = match direction {
CipherDirection::Encrypt => kek.wrap(&input),
CipherDirection::Decrypt => kek.unwrap(&input),
};

match result {
Ok(output) => write_bytes_or_hex(&output, output_hex),
Err(SymmetricCipherError::DecryptionFailed) => {
eprintln!(
"Error: {alg} unwrap failed: the input is not an authentic {alg} ciphertext under \
this key (it was tampered with, wrapped under a different key, or produced by a \
different algorithm)."
);
exit(-1);
}
Err(SymmetricCipherError::InvalidInputLength(why)) => {
eprintln!("Error: {alg}: input of {} bytes rejected: {why}.", input.len());
exit(-1);
}
Err(e) => {
eprintln!("Error: {alg} failed: {e:?}");
exit(-1);
}
}
}
179 changes: 179 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod aes_cfb_cmd;
mod aes_ctr_cmd;
mod aes_ecb_cmd;
mod aes_gcm_cmd;
mod aes_kw_cmd;
mod ascon_cmd;
mod chacha20_cmd;
mod chacha20_poly1305_cmd;
Expand Down Expand Up @@ -2182,6 +2183,166 @@ enum Subcommands {
x: bool,
},

/// AES-128 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout,
/// `-d decrypt` unwraps it.
///
/// Wraps a key -- or any data that is a whole number of 8-byte blocks, at least 16 bytes --
/// under the given key-encryption key (KEK). The output is 8 bytes longer than the input.
/// There is NO IV: wrapping is deterministic, and nothing is prepended on `encrypt` or
/// consumed on `decrypt`.
///
/// Unwrapping is authenticated. A ciphertext that was tampered with, wrapped under a different
/// KEK, or produced by aes*-kwp makes the command exit non-zero with nothing written. Input to
/// `decrypt` must be a whole number of 8-byte blocks, at least 24 bytes.
///
/// This command is not streaming: all of stdin is read before anything is written, because
/// the algorithm makes six passes over the whole input. For data that is not a multiple of
/// 8 bytes, use aes128-kwp.
///
/// 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.
AES128_KW {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte AES key-encryption 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 16-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// AES-192 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout,
/// `-d decrypt` unwraps it.
///
/// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming
/// note; only the key length differs.
AES192_KW {
#[arg(short, long)]
direction: CipherDirection,

/// The 24-byte AES key-encryption 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 24-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// AES-256 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout,
/// `-d decrypt` unwraps it.
///
/// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming
/// note; only the key length differs.
AES256_KW {
#[arg(short, long)]
direction: CipherDirection,

/// The 32-byte AES key-encryption 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 32-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// AES-128 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps
/// stdin to stdout, `-d decrypt` unwraps it.
///
/// As aes128-kw, but for data of any length from 1 byte up: the input is padded to a whole
/// number of 8-byte blocks and the output is that length plus 8. Its ciphertexts are not
/// interchangeable with aes128-kw's. Input to `decrypt` must be a whole number of 8-byte
/// blocks, at least 16 bytes.
///
/// See `aes128-kw` for the absence of an IV, the authenticated `decrypt` and the non-streaming
/// note.
AES128_KWP {
#[arg(short, long)]
direction: CipherDirection,

/// The 16-byte AES key-encryption 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 16-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// AES-192 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps
/// stdin to stdout, `-d decrypt` unwraps it.
///
/// See `aes128-kwp`; only the key length differs.
AES192_KWP {
#[arg(short, long)]
direction: CipherDirection,

/// The 24-byte AES key-encryption 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 24-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// AES-256 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps
/// stdin to stdout, `-d decrypt` unwraps it.
///
/// See `aes128-kwp`; only the key length differs.
AES256_KWP {
#[arg(short, long)]
direction: CipherDirection,

/// The 32-byte AES key-encryption 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 32-byte AES key-encryption key (raw bytes; hex with --hex-input).
/// If both key and key_file options are provided, the file will be used.
#[arg(short, long)]
key_file: Option<String>,

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

/// The ML-KEM-512 key encapsulation algorithm.
MLKEM512 {
action: mlkem_cmd::MLKEMAction,
Expand Down Expand Up @@ -3008,6 +3169,24 @@ fn run() {
mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile };
mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA87_SET, action, &files, None, *deterministic, *x);
}
Some(Subcommands::AES128_KW { direction, key, key_file, x }) => {
aes_kw_cmd::aes128_kw_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES192_KW { direction, key, key_file, x }) => {
aes_kw_cmd::aes192_kw_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES256_KW { direction, key, key_file, x }) => {
aes_kw_cmd::aes256_kw_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES128_KWP { direction, key, key_file, x }) => {
aes_kw_cmd::aes128_kwp_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES192_KWP { direction, key, key_file, x }) => {
aes_kw_cmd::aes192_kwp_cmd(direction, key, key_file, *x);
}
Some(Subcommands::AES256_KWP { direction, key, key_file, x }) => {
aes_kw_cmd::aes256_kwp_cmd(direction, key, key_file, *x);
}
Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => {
mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x);
}
Expand Down
Loading
Loading