diff --git a/Cargo.toml b/Cargo.toml index 11e1f13cc..3611a583e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ bouncycastle = { path = "./" } bouncycastle-aes = { path = "./crypto/aes" } bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } +bouncycastle-chacha20 = { path = "./crypto/chacha20" } +bouncycastle-chacha20-poly1305-aead = { path = "./crypto/chacha20-poly1305-aead" } bouncycastle-cipher = { path = "./crypto/cipher" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } @@ -25,6 +27,7 @@ bouncycastle-mlkem = { path = "./crypto/mlkem" } bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" } bouncycastle-mldsa = { path = "./crypto/mldsa" } bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" } +bouncycastle-poly1305 = { path = "./crypto/poly1305" } bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } @@ -50,6 +53,8 @@ edition.workspace = true bouncycastle-aes.workspace = true bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true +bouncycastle-chacha20.workspace = true +bouncycastle-chacha20-poly1305-aead.workspace = true bouncycastle-cipher.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true @@ -60,6 +65,7 @@ bouncycastle-mldsa.workspace = true bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true bouncycastle-mlkem-lowmemory.workspace = true +bouncycastle-poly1305.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index e848ccc27..b75cde48c 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -6,6 +6,7 @@ * 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_CCM, AES_CFB, AES_CFB8, AES_CTR, and AES_GCM. * ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232). + * CHACHA20 -- ChaCha20, Poly1305 and ChaCha20-Poly1305 (RFC 8439). * Further memory usage improvements on ML-DSA / ML-KEM. New figures for the largest size are: * ML-DSA-87/Sign 118 kb, ML-DSA-87/Verify 212 kb * ML-DSA-87_lowmemory/Sign 25 kb, ML-DSA-87_lowmemory/Verify 21 kb diff --git a/cli/src/chacha20_cmd.rs b/cli/src/chacha20_cmd.rs new file mode 100644 index 000000000..564b995f0 --- /dev/null +++ b/cli/src/chacha20_cmd.rs @@ -0,0 +1,34 @@ +//! ChaCha20 (RFC 8439 Sec 2.4) encryption and decryption, streaming stdin to stdout. +//! +//! The command-line contract is the one [`crate::helpers::stream_mode_helpers`] describes for +//! `aes*-ctr`: `encrypt` writes the generated 12-byte nonce ahead of the ciphertext and `decrypt` +//! reads it back, unless `--iv` / `--iv-file` supplies the nonce, in which case only ciphertext is +//! written or read. Any input length is accepted, and the output is exactly as long as the input. +//! A message may be at most 2^32 64-byte blocks (256 GiB). +//! +//! # Warning +//! +//! ChaCha20 alone provides confidentiality only, and is as malleable as CTR: see +//! [`crate::aes_ctr_cmd`]. Use `chacha20-poly1305` unless the ciphertext is authenticated by +//! some other means. A supplied nonce must never repeat under one key: the keystream would repeat +//! with it. + +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::chacha20::{ChaCha20Key, KEY_LEN, NONCE_LEN}; + +pub(crate) fn chacha20_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + run_stream_mode::<_, KEY_LEN, NONCE_LEN>( + action, + &load_key_object::(key, key_file, "ChaCha20"), + load_iv::(iv, iv_file), + output_hex, + ); +} diff --git a/cli/src/chacha20_poly1305_cmd.rs b/cli/src/chacha20_poly1305_cmd.rs new file mode 100644 index 000000000..4dd0b3f78 --- /dev/null +++ b/cli/src/chacha20_poly1305_cmd.rs @@ -0,0 +1,38 @@ +//! ChaCha20-Poly1305 (RFC 8439 Sec 2.8) authenticated encryption and decryption, streaming stdin +//! to stdout. +//! +//! The framing, `--iv`, AAD flags and exit-code contract are those of +//! [`crate::helpers::aead_cipher_helpers`], shared with the `aes*-gcm` commands: `nonce || +//! ciphertext || tag` with a generated 12-byte nonce, or `ciphertext || tag` when `--iv` / +//! `--iv-file` supplies it, and a 16-byte tag either way. As there, a tag failure on `decrypt` is +//! reported after the plaintext has already reached stdout; check the exit code before using it. +//! +//! A supplied nonce must never repeat under one key: the two messages would share both the +//! keystream and the one-time Poly1305 key (RFC 8439 Sec 4). + +use crate::helpers::aead_cipher_helpers::{decrypt_aead, encrypt_aead, load_aad}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use bouncycastle::chacha20_poly1305_aead::{ChaCha20Poly1305Key, KEY_LEN, NONCE_LEN, TAG_LEN}; + +pub(crate) fn chacha20_poly1305_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + aad: &Option, + aad_file: &Option, + output_hex: bool, +) { + let key = load_key_object::(key, key_file, "ChaCha20-Poly1305"); + let nonce = load_iv::(iv, iv_file); + let aad = load_aad(aad, aad_file); + match action { + CipherDirection::Encrypt => { + encrypt_aead::<_, KEY_LEN, NONCE_LEN, TAG_LEN, TAG_LEN>(&key, nonce, &aad, output_hex) + } + CipherDirection::Decrypt => { + decrypt_aead::<_, KEY_LEN, NONCE_LEN, TAG_LEN, TAG_LEN>(&key, nonce, &aad, output_hex) + } + } +} diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs index 3574f6aa4..56ab18114 100644 --- a/cli/src/helpers/aead_cipher_helpers.rs +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -1,10 +1,10 @@ -//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`. +//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm` and `chacha20-poly1305`. //! -//! Parallel to [`crate::helpers::stream_mode_helpers`], but for a key bound to -//! [`bouncycastle::cipher::modes::GCM`] rather than a stream mode: GCM carries additional -//! authenticated data and a tag, neither of which the stream traits have room for, so this module -//! drives the key's AEAD workers through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which -//! add `do_update_aad` to the symmetric-cipher streaming methods. +//! Parallel to [`crate::helpers::stream_mode_helpers`], but for an AEAD key -- one bound to +//! [`bouncycastle::cipher::modes::GCM`], or a ChaCha20-Poly1305 key -- rather than a stream mode: +//! an AEAD carries additional authenticated data and a tag, neither of which the stream traits +//! have room for, so this module drives the key's AEAD workers through [`AEADCipherEncryptor`] / +//! [`AEADCipherDecryptor`], which add `do_update_aad` to the symmetric-cipher streaming methods. //! //! # On-the-wire format: `IV || ciphertext || tag` //! @@ -12,7 +12,8 @@ //! 16-byte tag once stdin is exhausted. `decrypt` reads the 12-byte IV first, then streams the //! rest of stdin through the inline decryptor. With `--iv`, the IV is the caller's and is //! neither written nor read: the format is `ciphertext || tag` (see -//! [`crate::helpers::block_mode_helpers`] for the flag). The decryptor, per +//! [`crate::helpers::block_mode_helpers`] for the flag). `chacha20-poly1305` frames its 12-byte +//! nonce the same way, `--iv` included. The decryptor, per //! [`SymmetricCipherDecryptor`]'s contract, //! holds back the last 16 bytes it has seen because they might be the tag -- and checks the tag on //! `do_decrypt_final`. @@ -29,14 +30,14 @@ //! # AAD //! //! `--aad ` or `--aad-file `; if neither is given, AAD is empty. Fed to the engine in -//! one call before any ciphertext, matching SP 800-38D Algorithm 4's requirement that AAD precede -//! data. +//! one call before any ciphertext, since both GCM (SP 800-38D Algorithm 4) and ChaCha20-Poly1305 +//! (RFC 8439 Sec 2.8) authenticate the AAD ahead of the ciphertext. //! //! `--aad-file` is read as raw bytes ([`read_from_file_raw`]), never hex-decoded. The //! hex-or-raw guess `--key-file` uses would change what is authenticated without any error: a //! binary header that happens to parse as hex text (`cafe`, or sixteen zero bytes, which the hex //! decoder skips) would be authenticated as its decoding, and the tag would not verify against any -//! other GCM implementation given the same file. +//! other implementation given the same file. use crate::helpers::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout}; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; @@ -81,6 +82,40 @@ pub fn encrypt_gcm( output_hex: bool, ) where K: AeadEncryptionKey, +{ + encrypt_aead::(key, nonce, aad, output_hex) +} + +/// Decrypts stdin to stdout under GCM: takes the 12-byte IV from `nonce` or else reads it +/// first, streams the rest through the inline decryptor, and checks the tag on +/// `do_decrypt_final`. See the module docs for why plaintext may already be written to stdout by +/// the time a tag failure is reported. +pub fn decrypt_gcm( + key: &K, + nonce: Option<[u8; GCM_NONCE_LEN]>, + aad: &[u8], + output_hex: bool, +) where + K: AeadDecryptionKey, +{ + decrypt_aead::(key, nonce, aad, output_hex) +} + +/// Encrypts stdin to stdout under any AEAD key: writes the generated nonce (unless `nonce` +/// supplies one), then the ciphertext as it streams, then the tag. +pub fn encrypt_aead< + K, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>( + key: &K, + nonce: Option<[u8; NONCE_LEN]>, + aad: &[u8], + output_hex: bool, +) where + K: AeadEncryptionKey, { let mut enc = match nonce { Some(nonce) => key.create_encryptor_nonce(&nonce), @@ -111,7 +146,8 @@ pub fn encrypt_gcm( if n == 0 { break; } - // GCM's encryptor holds nothing back, so `out` (as long as `buf`) always has room. + // The AEADs here hold nothing back on encryption, so `out` (as long as `buf`) always has + // room; one that did would get `OutputBufferTooSmall` below, not a truncated output. let written = enc.do_encrypt_out(&buf[..n], &mut out).unwrap_or_else(|e| { eprintln!("Error: encryption failed: {e:?}"); exit(-1); @@ -119,33 +155,41 @@ pub fn encrypt_gcm( write_bytes_or_hex(&out[..written], output_hex); } - // The detached final flushes nothing for GCM and returns the tag, written last. - let (_, tag) = enc.do_encrypt_final_detached_out(&mut [0u8; TAG_LEN]).unwrap_or_else(|e| { + // The detached final returns whatever ciphertext was held back (none, for the AEADs here) + // and the tag, written last. + let (last, last_len, tag) = enc.do_encrypt_final_detached().unwrap_or_else(|e| { eprintln!("Error: encryption failed: {e:?}"); exit(-1); }); + write_bytes_or_hex(&last[..last_len], output_hex); write_bytes_or_hex(&tag, output_hex); finish(output_hex); } -/// Decrypts stdin to stdout under GCM: takes the 12-byte IV from `nonce` or else reads it +/// Decrypts stdin to stdout under any AEAD key: takes the nonce from `nonce` or else reads it /// first, streams the rest through the inline decryptor, and checks the tag on /// `do_decrypt_final`. See the module docs for why plaintext may already be written to stdout by /// the time a tag failure is reported. -pub fn decrypt_gcm( +pub fn decrypt_aead< + K, + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>( key: &K, - nonce: Option<[u8; GCM_NONCE_LEN]>, + nonce: Option<[u8; NONCE_LEN]>, aad: &[u8], output_hex: bool, ) where - K: AeadDecryptionKey, + K: AeadDecryptionKey, { let nonce = nonce.unwrap_or_else(|| { - let mut nonce = [0u8; GCM_NONCE_LEN]; + let mut nonce = [0u8; NONCE_LEN]; if let Err(e) = io::stdin().read_exact(&mut nonce) { eprintln!( - "Error: input too short to contain the 12-byte IV that `encrypt` writes first \ - ({e}). If the IV is held separately, pass it with --iv." + "Error: input too short to contain the {NONCE_LEN}-byte IV that `encrypt` writes \ + first ({e}). If the IV is held separately, pass it with --iv." ); exit(-1); } @@ -176,12 +220,16 @@ pub fn decrypt_gcm( write_bytes_or_hex(&out, output_hex); } - if let Err(e) = dec.do_decrypt_final() { - // Whatever plaintext was already written above stands; the exit code is the signal a - // script must check (see the module docs). - io::stdout().flush().ok(); - eprintln!("Error: authentication failed: {e:?}"); - exit(-1); + match dec.do_decrypt_final() { + // Any plaintext still held back (none, for the AEADs here: the held bytes are the tag). + Ok((last, last_len)) => write_bytes_or_hex(&last[..last_len], output_hex), + Err(e) => { + // Whatever plaintext was already written above stands; the exit code is the signal a + // script must check (see the module docs). + io::stdout().flush().ok(); + eprintln!("Error: authentication failed: {e:?}"); + exit(-1); + } } finish(output_hex); diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 66e6d3b3c..dd3f87df4 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -9,6 +9,8 @@ use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; use bouncycastle::core::traits::MAC; use bouncycastle::hex; use bouncycastle::hmac::HmacKey; +use bouncycastle::poly1305; +use bouncycastle::poly1305::hazmat::key_from_bytes; use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256}; use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key}; use bouncycastle::sm3::SM3; @@ -121,6 +123,24 @@ pub(crate) fn kmac_cmd( } } +/// Poly1305 (RFC 8439 Sec 2.5) under a 32-byte one-time key, which must never MAC a second +/// message. The tag is always the full 16 bytes. +pub(crate) fn poly1305_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); + // 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 { + eprintln!("Error: Poly1305 needs a 32-byte one-time key, got {} bytes.", key.key_len()); + exit(-1) + }; + do_mac(key_from_bytes(bytes).into_mac(), verify_val, output_hex); +} + fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { // read the content to be MAC'd from stdin let mut buf: [u8; 1024] = [0u8; 1024]; diff --git a/cli/src/main.rs b/cli/src/main.rs index 1377e4b33..753f6063d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -6,6 +6,8 @@ mod aes_ctr_cmd; mod aes_ecb_cmd; mod aes_gcm_cmd; mod ascon_cmd; +mod chacha20_cmd; +mod chacha20_poly1305_cmd; mod encoders_cmd; mod helpers; mod hkdf_cmd; @@ -1006,6 +1008,115 @@ enum Subcommands { /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. Base64Encode, + /// ChaCha20 (RFC 8439 Sec 2.4), streaming stdin to stdout. + /// + /// On `encrypt`, a fresh nonce is generated and written as the FIRST 12 BYTES of the output; + /// on `decrypt` it is read back from the first 12 bytes of the input, so the two compose + /// directly in a pipeline. The block counter starts at 0. + /// + /// With `--iv` (hex) or `--iv-file` (raw bytes), the 12-byte nonce is supplied instead: + /// `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from + /// stdin, so a nonce left at the front of the input is decrypted as ciphertext. The nonce MUST + /// be unique per message under one key: a repeat gives the same keystream, and the XOR of the + /// two ciphertexts is then the XOR of the two plaintexts (RFC 8439 Sec 4). That is the + /// caller's responsibility, which the default takes care of. + /// + /// Input may be ANY length, up to 2^32 64-byte blocks (256 GiB) per message, and the + /// ciphertext is exactly as long as the plaintext. + /// + /// WARNING: ChaCha20 provides confidentiality only and is as malleable as CTR: flipping a + /// ciphertext bit flips the same plaintext bit. Use `chacha20-poly1305` unless the ciphertext + /// is authenticated separately. + /// + /// 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. + CHACHA20 { + #[arg(short, long)] + direction: CipherDirection, + + /// The 32-byte key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Supplied instead of generated: `encrypt` then writes none + /// and `decrypt` reads none. See the nonce paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex + /// decoding). If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// ChaCha20-Poly1305 (RFC 8439 Sec 2.8), streaming stdin to stdout. + /// + /// On `encrypt`, a fresh 12-byte nonce is generated and written as the FIRST 12 BYTES of the + /// output, the ciphertext follows, and the 16-byte tag is written last. `decrypt` reads the + /// nonce back from the first 12 bytes of input and streams the rest, checking the tag once + /// input is exhausted. + /// + /// With `--iv` (hex) or `--iv-file` (raw bytes), the nonce is supplied instead: `encrypt` uses + /// it and writes ONLY `ciphertext || tag`, and `decrypt` reads ONLY that from stdin, so a nonce + /// left at the front of the input fails the tag check. The nonce MUST be unique per message + /// under one key (RFC 8439 Sec 2.8), and a repeat is worse here than for `chacha20`: the two + /// messages then share the keystream, which reveals the XOR of their plaintexts, and the + /// one-time Poly1305 key (Sec 4), which must be unique per message (Sec 2.5) because two tags + /// under it let an attacker forge others. The default takes care of that; with `--iv` it is on + /// the caller. + /// + /// See `aes128-gcm` for the AAD flags and the streaming warning, which apply here unchanged. + /// + /// 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. + CHACHA20_POLY1305 { + #[arg(short, long)] + direction: CipherDirection, + + /// The 32-byte key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Supplied instead of generated: `encrypt` then writes none and + /// `decrypt` reads none. See the nonce paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex + /// decoding). If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. + #[arg(long)] + aad: Option, + + /// A file containing the additional authenticated data, as raw bytes (never hex-decoded). + /// If both aad and aad_file options are provided, the file will be used. + #[arg(long)] + aad_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The HashML-DSA-44 signature algorithm. HashMLDSA44 { action: MLDSAAction, @@ -1574,6 +1685,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify a Poly1305 tag (RFC 8439 Sec 2.5) over the content provided on stdin. + /// + /// WARNING: the key is ONE-TIME. MACing two messages under the same 32-byte key lets an + /// attacker forge tags. For general-purpose authentication use an HMAC or KMAC command, or + /// `chacha20-poly1305`, which derives a fresh Poly1305 key for every message. + POLY1305 { + /// The 32-byte one-time key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte one-time key in binary. + #[arg(short, long)] + key_file: Option, + + /// A 16-byte tag to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// Generate cryptographically-secure random bytes, seeded from the operating system's entropy source (/dev/random or equivalent). /// Uses the library's default 256-bit secure RNG algorithm. RNG { @@ -1894,6 +2030,23 @@ fn run() { Some(Subcommands::Base64Encode) => { encoders_cmd::base64_encode_cmd(); } + Some(Subcommands::CHACHA20 { direction, key, key_file, iv, iv_file, x }) => { + chacha20_cmd::chacha20_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::CHACHA20_POLY1305 { + direction, + key, + key_file, + iv, + iv_file, + aad, + aad_file, + x, + }) => { + chacha20_poly1305_cmd::chacha20_poly1305_cmd( + direction, key, key_file, iv, iv_file, aad, aad_file, *x, + ); + } Some(Subcommands::HashMLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { mldsa_cmd::hash_mldsa44_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); } @@ -1993,6 +2146,9 @@ fn run() { Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *x); } + Some(Subcommands::POLY1305 { key, key_file, verify, x }) => { + mac_cmd::poly1305_cmd(key, key_file, verify, *x) + } Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), Some(Subcommands::SHA3_224 { x }) => { sha3_cmd::sha3_cmd(224, *x); diff --git a/cli/tests/test_chacha20.sh b/cli/tests/test_chacha20.sh new file mode 100755 index 000000000..a44fc82a1 --- /dev/null +++ b/cli/tests/test_chacha20.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# The chacha20 subcommand, end to end through the binary. +# +# Framing: encrypt writes a fresh 12-byte nonce as the first bytes of its output and decrypt reads +# it back from the first 12 bytes of its input, unless `--iv` / `--iv-file` supplies the nonce, in +# which case only ciphertext is written or read. Any input length is accepted, with a ciphertext +# exactly as long as the plaintext, and the block counter starts at 0. Keys and data come from +# `bc-rust rng`; the fixed inputs are RFC 8439's own vectors. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +NONCE_LEN=12 +BLOCK_LEN=64 + +# ---- the RFC 8439 vectors -------------------------------------------------------------------- + +# Appendix A.2, Test Vector #1: all-zero key, nonce and plaintext, initial block counter 0. +A2_1_KEY=0000000000000000000000000000000000000000000000000000000000000000 +A2_1_NONCE=000000000000000000000000 +A2_1_PLAINTEXT=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +A2_1_CIPHERTEXT=76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586 + +# Sec 2.4.2, the sunscreen example. Its initial counter is 1, one block past the 0 this command +# starts at, so the tests put one block ahead of the message and drop it from the output. +S242_KEY=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f +S242_NONCE=000000000000004a00000000 +S242_PLAINTEXT=4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e +S242_CIPHERTEXT=6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74a35be6b40b8eedf2785e42874d + +test_a_supplied_nonce_matches_rfc8439_appendix_a2_vector_1() { + local got + got=$(unhex "$A2_1_PLAINTEXT" | hex_out "$BC_RUST" chacha20 -d encrypt --key "$A2_1_KEY" --iv "$A2_1_NONCE" -x) + assert_eq "$got" "$A2_1_CIPHERTEXT" "A.2 #1: encrypt writes the ciphertext alone" + got=$(unhex "$A2_1_CIPHERTEXT" | hex_out "$BC_RUST" chacha20 -d decrypt --key "$A2_1_KEY" --iv "$A2_1_NONCE" -x) + assert_eq "$got" "$A2_1_PLAINTEXT" "A.2 #1: decrypt reads the ciphertext alone" +} + +test_decrypt_takes_the_nonce_from_the_input_by_default() { + local got + got=$(unhex "$A2_1_NONCE$A2_1_CIPHERTEXT" | hex_out "$BC_RUST" chacha20 -d decrypt --key "$A2_1_KEY" -x) + assert_eq "$got" "$A2_1_PLAINTEXT" "A.2 #1 with its nonce ahead of the ciphertext" +} + +test_a_supplied_nonce_matches_rfc8439_section_2_4_2_from_counter_1() { + { head -c $BLOCK_LEN /dev/zero; unhex "$S242_PLAINTEXT"; } >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key "$S242_KEY" --iv "$S242_NONCE" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((BLOCK_LEN + 114)) "one block, then the 114-byte message" + tail -c +$((BLOCK_LEN + 1)) "$TMP/ct" >"$TMP/body" + assert_eq "$(hex "$TMP/body")" "$S242_CIPHERTEXT" "Sec 2.4.2: the ciphertext from counter 1" + + { head -c $BLOCK_LEN /dev/zero; unhex "$S242_CIPHERTEXT"; } >"$TMP/ct2" + "$BC_RUST" chacha20 -d decrypt --key "$S242_KEY" --iv "$S242_NONCE" <"$TMP/ct2" >"$TMP/out" + tail -c +$((BLOCK_LEN + 1)) "$TMP/out" >"$TMP/rec" + assert_eq "$(hex "$TMP/rec")" "$S242_PLAINTEXT" "Sec 2.4.2: the sunscreen text back" +} + +test_iv_file_holds_the_bytes_iv_gives_in_hex() { + unhex "$S242_NONCE" >"$TMP/nonce" + rng 100 >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key "$S242_KEY" --iv "$S242_NONCE" <"$TMP/pt" >"$TMP/a" + "$BC_RUST" chacha20 -d encrypt --key "$S242_KEY" --iv-file "$TMP/nonce" <"$TMP/pt" >"$TMP/b" + assert_same "$TMP/a" "$TMP/b" "--iv-file must be the same nonce as --iv" +} + +# As for the AES commands, `--iv-file` wins when both are given: the vector's nonce in the file +# gives the vector, and the vector's nonce on the command line is ignored for the file's. +test_iv_file_wins_over_iv_when_both_are_given() { + unhex "$A2_1_NONCE" >"$TMP/nonce" + rng 12 >"$TMP/other" + unhex "$A2_1_PLAINTEXT" >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key "$A2_1_KEY" --iv "$(hex "$TMP/other")" --iv-file "$TMP/nonce" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$A2_1_CIPHERTEXT" "the file's nonce must be the one used" + "$BC_RUST" chacha20 -d decrypt --key "$A2_1_KEY" --iv "$(hex "$TMP/other")" --iv-file "$TMP/nonce" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt must take the file's nonce too" + + "$BC_RUST" chacha20 -d encrypt --key "$A2_1_KEY" --iv "$A2_1_NONCE" --iv-file "$TMP/other" <"$TMP/pt" >"$TMP/both" + "$BC_RUST" chacha20 -d encrypt --key "$A2_1_KEY" --iv-file "$TMP/other" <"$TMP/pt" >"$TMP/file_only" + assert_same "$TMP/file_only" "$TMP/both" "the vector's nonce in --iv must lose to the file's" +} + +# ---- the nonce: generated by default, or supplied -------------------------------------------- + +test_the_default_writes_a_generated_nonce_ahead_of_the_ciphertext() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + 69)) "a 12-byte nonce plus a body as long as the plaintext" + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt consumes exactly 12 bytes of nonce" +} + +# A repeated nonce under one key repeats the keystream, so without `--iv` every run draws its own. +test_each_invocation_without_iv_uses_a_fresh_nonce() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + local i + : >"$TMP/nonces" + for i in $(seq 1 8); do + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + head -c $NONCE_LEN "$TMP/ct" | "$BC_RUST" hex-encode >>"$TMP/nonces" + echo >>"$TMP/nonces" + done + assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" +} + +# A supplied nonce is used exactly as given: under the nonce a default run generated, `--iv` gives +# that run's ciphertext without the nonce in front. +test_a_supplied_nonce_gives_the_default_output_without_the_nonce() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/framed" + slice "$TMP/framed" 0 $NONCE_LEN >"$TMP/nonce" + tail -c +$((NONCE_LEN + 1)) "$TMP/framed" >"$TMP/body" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv "$(hex "$TMP/nonce")" <"$TMP/pt" >"$TMP/ct" + assert_same "$TMP/body" "$TMP/ct" "--iv output must be the default body under the same nonce" + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt with the nonce supplied reads only ciphertext" +} + +# With `--iv`, decrypt does not look for a nonce in its input: one left there is decrypted as +# ciphertext, which gives 12 bytes too many and not the plaintext. +test_with_iv_a_nonce_left_in_the_input_is_decrypted_as_ciphertext() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" >"$TMP/ct" + cat "$TMP/nonce" "$TMP/ct" >"$TMP/framed" + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/framed" >"$TMP/rec" + assert_size "$TMP/rec" $((NONCE_LEN + 69)) "every input byte is taken as ciphertext" + tail -c +$((NONCE_LEN + 1)) "$TMP/rec" >"$TMP/tail" + assert_differs "$TMP/pt" "$TMP/tail" "the plaintext must not come back from the wrong framing" +} + +# What the help warns about: one key and nonce used twice give one keystream twice, so the XOR of +# the two ciphertexts is the XOR of the two plaintexts. +test_a_repeated_supplied_nonce_reveals_the_xor_of_the_plaintexts() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng 64 >"$TMP/p1" + rng 64 >"$TMP/p2" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/p1" >"$TMP/c1" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/p2" >"$TMP/c2" + assert_eq "$(xor_hex "$(hex "$TMP/c1")" "$(hex "$TMP/c2")")" \ + "$(xor_hex "$(hex "$TMP/p1")" "$(hex "$TMP/p2")")" "c1 xor c2 must be p1 xor p2" +} + +# ---- round trips ----------------------------------------------------------------------------- + +test_any_input_length_round_trips_with_and_without_iv() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + local len + for len in 0 1 63 64 65 127 128 129 1023 1024 1025 4099; do + rng "$len" >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + len)) "len $len: nonce plus an equal-length body" + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" "$len" "len $len: with --iv, the body alone" + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip with --iv" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng $((1024 * 1024 + 7)) >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "1 MiB + 7 must survive encrypt | decrypt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" \ + | "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "and with the nonce supplied on both sides" +} + +test_hex_output_composes_through_hex_decode() { + rng 32 >"$TMP/key" + rng 4099 >"$TMP/pt" + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (NONCE_LEN + 4099) + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +# ---- invalid input --------------------------------------------------------------------------- + +test_a_nonce_of_the_wrong_length_is_rejected() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + local len + for len in 0 11 13 16; do + rng "$len" >"$TMP/nonce" + expect_fail "a $len-byte --iv" \ + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv "$(hex "$TMP/nonce")" <"$TMP/pt" + assert_stderr_has "requires exactly 12" + expect_fail "a $len-byte --iv-file" \ + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" + assert_stderr_has "requires exactly 12" + done +} + +test_a_nonce_that_is_not_hex_is_rejected() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + local iv + for iv in zzzzzzzzzzzzzzzzzzzzzzzz 00112233445566778899aab; do + expect_fail "--iv $iv" \ + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv "$iv" <"$TMP/pt" + assert_stderr_has "not valid hex" + done +} + +# A nonce file written with `echo` rather than `echo -n` carries a newline, which is warned about +# and then, as the thirteenth byte, refused. +test_a_nonce_file_with_a_trailing_newline_is_warned_about() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + { rng 12; printf '\n'; } >"$TMP/nonce.nl" + expect_fail "a 12-byte nonce plus a newline" \ + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce.nl" <"$TMP/pt" + assert_stderr_has "newline" + assert_stderr_has "requires exactly 12" +} + +test_decrypt_input_shorter_than_the_nonce_is_rejected() { + rng 32 >"$TMP/key" + local len + for len in 0 1 11; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 12-byte nonce" \ + "$BC_RUST" chacha20 -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "12-byte IV" + done +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "a 16-byte key is not a ChaCha20 key" \ + "$BC_RUST" chacha20 -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" +} + +# ---- discoverability ------------------------------------------------------------------------- + +test_the_subcommand_is_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + grep -q "chacha20 " "$TMP/help" || fail "--help should list chacha20" +} + +test_per_command_help_documents_the_nonce_and_its_reuse() { + "$BC_RUST" chacha20 --help >"$TMP/help" + grep -q -- "--iv " "$TMP/help" || fail "help should list --iv" + grep -q -- "--iv-file" "$TMP/help" || fail "help should list --iv-file" + grep -q "If both iv and iv_file options are provided, the file will be used" "$TMP/help" \ + || fail "help should say --iv-file wins over --iv" + grep -qi "first 12 bytes" "$TMP/help" || fail "help should say the generated nonce is written first" + grep -q "MUST be unique" "$TMP/help" || fail "help should say a supplied nonce must not repeat" + grep -qiE "malleable|flipping" "$TMP/help" || fail "help should warn about malleability" +} + +run_all diff --git a/cli/tests/test_chacha20_poly1305.sh b/cli/tests/test_chacha20_poly1305.sh new file mode 100755 index 000000000..76a2509fe --- /dev/null +++ b/cli/tests/test_chacha20_poly1305.sh @@ -0,0 +1,316 @@ +#!/usr/bin/env bash +# The chacha20-poly1305 subcommand, end to end through the binary. +# +# Framing: encrypt writes a fresh 12-byte nonce, then the ciphertext (as long as the plaintext), +# then the 16-byte tag; decrypt reads the same layout back. With `--iv` / `--iv-file` the nonce is +# supplied instead and is neither written nor read, so the stream is `ciphertext || tag`. `--aad` +# (hex) or `--aad-file` (raw bytes) is authenticated but not encrypted. The fixed inputs are RFC +# 8439's Sec 2.8.2 and Appendix A.5 vectors; everything else comes from `bc-rust rng`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +NONCE_LEN=12 +TAG_LEN=16 + +# ---- the RFC 8439 vectors -------------------------------------------------------------------- + +# Sec 2.8.2. The nonce is the 32-bit fixed-common part 07000000 followed by the 64-bit IV. +S282_PLAINTEXT=4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e +S282_AAD=50515253c0c1c2c3c4c5c6c7 +S282_KEY=808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f +S282_NONCE=070000004041424344454647 +S282_CIPHERTEXT=d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116 +S282_TAG=1ae10b594f09e26a7e902ecbd0600691 + +# Appendix A.5, a decryption: the received tag is the one calculated there. +A5_KEY=1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0 +A5_CIPHERTEXT=64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b +A5_NONCE=000000000102030405060708 +A5_AAD=f33388860000000000004e91 +A5_TAG=eead9d67890cbb22392336fea1851f38 +A5_PLAINTEXT=496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d + +test_a_supplied_nonce_matches_rfc8439_section_2_8_2() { + local got + got=$(unhex "$S282_PLAINTEXT" | hex_out "$BC_RUST" chacha20-poly1305 -d encrypt \ + --key "$S282_KEY" --iv "$S282_NONCE" --aad "$S282_AAD" -x) + assert_eq "$got" "$S282_CIPHERTEXT$S282_TAG" "Sec 2.8.2: ciphertext || tag, with no nonce written" + got=$(unhex "$S282_CIPHERTEXT$S282_TAG" | hex_out "$BC_RUST" chacha20-poly1305 -d decrypt \ + --key "$S282_KEY" --iv "$S282_NONCE" --aad "$S282_AAD" -x) + assert_eq "$got" "$S282_PLAINTEXT" "Sec 2.8.2: decrypt reads ciphertext || tag alone" +} + +test_a_supplied_nonce_matches_rfc8439_appendix_a5() { + local got + got=$(unhex "$A5_CIPHERTEXT$A5_TAG" | hex_out "$BC_RUST" chacha20-poly1305 -d decrypt \ + --key "$A5_KEY" --iv "$A5_NONCE" --aad "$A5_AAD" -x) + assert_eq "$got" "$A5_PLAINTEXT" "A.5: the decryption" + got=$(unhex "$A5_PLAINTEXT" | hex_out "$BC_RUST" chacha20-poly1305 -d encrypt \ + --key "$A5_KEY" --iv "$A5_NONCE" --aad "$A5_AAD" -x) + assert_eq "$got" "$A5_CIPHERTEXT$A5_TAG" "A.5 run the other way" +} + +test_decrypt_takes_the_nonce_from_the_input_by_default() { + local got + got=$(unhex "$S282_NONCE$S282_CIPHERTEXT$S282_TAG" | hex_out "$BC_RUST" chacha20-poly1305 \ + -d decrypt --key "$S282_KEY" --aad "$S282_AAD" -x) + assert_eq "$got" "$S282_PLAINTEXT" "Sec 2.8.2 as nonce || ciphertext || tag" + got=$(unhex "$A5_NONCE$A5_CIPHERTEXT$A5_TAG" | hex_out "$BC_RUST" chacha20-poly1305 \ + -d decrypt --key "$A5_KEY" --aad "$A5_AAD" -x) + assert_eq "$got" "$A5_PLAINTEXT" "A.5 as nonce || ciphertext || tag" +} + +test_iv_file_holds_the_bytes_iv_gives_in_hex() { + unhex "$S282_NONCE" >"$TMP/nonce" + unhex "$S282_PLAINTEXT" >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key "$S282_KEY" --iv-file "$TMP/nonce" \ + --aad "$S282_AAD" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$S282_CIPHERTEXT$S282_TAG" "--iv-file must be the same nonce as --iv" +} + +# As for the AES commands, `--iv-file` wins when both are given: the vector's nonce in the file +# gives the vector, and the vector's nonce on the command line is ignored for the file's. +test_iv_file_wins_over_iv_when_both_are_given() { + unhex "$S282_NONCE" >"$TMP/nonce" + rng 12 >"$TMP/other" + unhex "$S282_PLAINTEXT" >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key "$S282_KEY" --iv "$(hex "$TMP/other")" \ + --iv-file "$TMP/nonce" --aad "$S282_AAD" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$S282_CIPHERTEXT$S282_TAG" "the file's nonce must be the one used" + "$BC_RUST" chacha20-poly1305 -d decrypt --key "$S282_KEY" --iv "$(hex "$TMP/other")" \ + --iv-file "$TMP/nonce" --aad "$S282_AAD" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt must take the file's nonce too" + + expect_fail "the vector's nonce in --iv must lose to the file's, and the tag then fails" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key "$S282_KEY" --iv "$S282_NONCE" \ + --iv-file "$TMP/other" --aad "$S282_AAD" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# ---- the nonce: generated by default, or supplied -------------------------------------------- + +test_the_default_writes_a_generated_nonce_ahead_of_the_ciphertext_and_tag() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + 69 + TAG_LEN)) "nonce, ciphertext and tag" + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "round trip" +} + +test_each_invocation_without_iv_uses_a_fresh_nonce() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + local i + : >"$TMP/nonces" + for i in $(seq 1 8); do + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + head -c $NONCE_LEN "$TMP/ct" | "$BC_RUST" hex-encode >>"$TMP/nonces" + echo >>"$TMP/nonces" + done + assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" +} + +# A supplied nonce is used exactly as given: under the nonce a default run generated, `--iv` gives +# that run's ciphertext and tag without the nonce in front. +test_a_supplied_nonce_gives_the_default_output_without_the_nonce() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/framed" + slice "$TMP/framed" 0 $NONCE_LEN >"$TMP/nonce" + tail -c +$((NONCE_LEN + 1)) "$TMP/framed" >"$TMP/sealed" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv "$(hex "$TMP/nonce")" \ + --aad deadbeef <"$TMP/pt" >"$TMP/ct" + assert_same "$TMP/sealed" "$TMP/ct" "--iv output must be the default ciphertext || tag under the same nonce" + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" \ + --aad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt with the nonce supplied reads only ciphertext || tag" +} + +test_a_supplied_nonce_with_no_message_writes_just_the_tag() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + : >"$TMP/empty" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" $TAG_LEN "an empty message under a supplied nonce is the tag alone" + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and decrypts back to nothing" +} + +# With `--iv`, decrypt does not look for a nonce in its input: one left there is taken as +# ciphertext, and the tag no longer verifies. +test_with_iv_a_nonce_left_in_the_input_fails_authentication() { + rng 32 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/framed" + slice "$TMP/framed" 0 $NONCE_LEN >"$TMP/nonce" + expect_fail "nonce || ciphertext || tag under --iv" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/framed" + assert_stderr_has "authentication failed" +} + +test_a_wrong_supplied_nonce_fails_authentication() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng 12 >"$TMP/other" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" >"$TMP/ct" + expect_fail "another nonce must not verify" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/other" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# What the help warns about: one key and nonce used twice give one keystream twice, so the XOR of +# the two ciphertexts is the XOR of the two plaintexts. The repeated one-time Poly1305 key is the +# other half of the damage, which a script cannot show as directly. +test_a_repeated_supplied_nonce_reveals_the_xor_of_the_plaintexts() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng 64 >"$TMP/p1" + rng 64 >"$TMP/p2" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/p1" >"$TMP/s1" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/p2" >"$TMP/s2" + slice "$TMP/s1" 0 64 >"$TMP/c1" + slice "$TMP/s2" 0 64 >"$TMP/c2" + assert_eq "$(xor_hex "$(hex "$TMP/c1")" "$(hex "$TMP/c2")")" \ + "$(xor_hex "$(hex "$TMP/p1")" "$(hex "$TMP/p2")")" "c1 xor c2 must be p1 xor p2" +} + +# ---- round trips ----------------------------------------------------------------------------- + +test_any_input_length_round_trips_with_and_without_iv() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + local len + for len in 0 1 15 16 17 63 64 65 1023 1024 1025 4099; do + rng "$len" >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + len + TAG_LEN)) "len $len: nonce, equal-length body, tag" + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" \ + --aad deadbeef <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((len + TAG_LEN)) "len $len: with --iv, body and tag alone" + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" \ + --aad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip with --iv" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng $((1024 * 1024 + 7)) >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "1 MiB + 7 must survive encrypt | decrypt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" \ + | "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "and with the nonce supplied on both sides" +} + +# ---- invalid input --------------------------------------------------------------------------- + +test_a_nonce_of_the_wrong_length_is_rejected() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + local len + for len in 0 11 13 16; do + rng "$len" >"$TMP/nonce" + expect_fail "a $len-byte --iv" \ + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv "$(hex "$TMP/nonce")" <"$TMP/pt" + assert_stderr_has "requires exactly 12" + expect_fail "a $len-byte --iv-file" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/pt" + assert_stderr_has "requires exactly 12" + done +} + +test_a_nonce_that_is_not_hex_is_rejected() { + rng 32 >"$TMP/key" + rng 64 >"$TMP/pt" + local iv + for iv in zzzzzzzzzzzzzzzzzzzzzzzz 00112233445566778899aab; do + expect_fail "--iv $iv" \ + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv "$iv" <"$TMP/pt" + assert_stderr_has "not valid hex" + done +} + +test_decrypt_input_shorter_than_the_nonce_is_rejected() { + rng 32 >"$TMP/key" + local len + for len in 0 1 11; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 12-byte nonce" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "12-byte IV" + done +} + +# With the nonce supplied there is no nonce in the stream, so the floor is the tag instead. +test_with_iv_decrypt_input_shorter_than_the_tag_is_rejected() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + local len + for len in 0 1 15; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 16-byte tag" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" <"$TMP/short" + assert_stderr_has "authentication failed" + done +} + +test_tampering_is_rejected_under_a_supplied_nonce() { + rng 32 >"$TMP/key" + rng 12 >"$TMP/nonce" + rng 69 >"$TMP/pt" + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" \ + --aad deadbeef <"$TMP/pt" >"$TMP/ct" + + cp "$TMP/ct" "$TMP/bad_ct" + flip_byte "$TMP/bad_ct" 0 + expect_fail "a flipped ciphertext bit must not verify" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" --aad deadbeef <"$TMP/bad_ct" + assert_stderr_has "authentication failed" + + cp "$TMP/ct" "$TMP/bad_tag" + flip_byte "$TMP/bad_tag" $(($(wc -c <"$TMP/ct") - 1)) + expect_fail "a flipped tag bit must not verify" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" --aad deadbeef <"$TMP/bad_tag" + assert_stderr_has "authentication failed" + + expect_fail "another AAD must not verify" \ + "$BC_RUST" chacha20-poly1305 -d decrypt --key-file "$TMP/key" --iv-file "$TMP/nonce" --aad 00112233 <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "a 16-byte key is not a ChaCha20-Poly1305 key" \ + "$BC_RUST" chacha20-poly1305 -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" +} + +# ---- discoverability ------------------------------------------------------------------------- + +test_the_subcommand_is_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + grep -q "chacha20-poly1305" "$TMP/help" || fail "--help should list chacha20-poly1305" +} + +test_per_command_help_documents_the_nonce_and_its_reuse() { + "$BC_RUST" chacha20-poly1305 --help >"$TMP/help" + grep -q -- "--iv " "$TMP/help" || fail "help should list --iv" + grep -q -- "--iv-file" "$TMP/help" || fail "help should list --iv-file" + grep -q "If both iv and iv_file options are provided, the file will be used" "$TMP/help" \ + || fail "help should say --iv-file wins over --iv" + grep -qi "first 12 bytes" "$TMP/help" || fail "help should say the generated nonce is written first" + grep -q "MUST be unique" "$TMP/help" || fail "help should say a supplied nonce must not repeat" + grep -q "Poly1305 key" "$TMP/help" || fail "help should say a repeat also reuses the Poly1305 key" + grep -q "aad" "$TMP/help" || fail "help should mention AAD" +} + +run_all diff --git a/cli/tests/test_poly1305.sh b/cli/tests/test_poly1305.sh new file mode 100755 index 000000000..6a62d7e8c --- /dev/null +++ b/cli/tests/test_poly1305.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# The poly1305 subcommand, end to end through the binary: a tag over random data under a random +# one-time key must verify with `-v`, and must stop verifying when the tag, the message or the key +# changes; RFC 8439's Appendix A.3 vectors must come out exactly; and a key that is not 32 bytes is +# refused. `-v` reports through the exit status alone. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ---- RFC 8439 Appendix A.3 ------------------------------------------------------------------- + +# Test Vector #1. +A3_1_KEY=0000000000000000000000000000000000000000000000000000000000000000 +A3_1_TEXT=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +A3_1_TAG=00000000000000000000000000000000 + +# Test Vector #4. +A3_4_KEY=1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0 +A3_4_TEXT=2754776173206272696c6c69672c20616e642074686520736c6974687920746f7665730a446964206779726520616e642067696d626c6520696e2074686520776162653a0a416c6c206d696d737920776572652074686520626f726f676f7665732c0a416e6420746865206d6f6d65207261746873206f757467726162652e +A3_4_TAG=4541669a7eaaee61e708dc7cbcc5eb62 + +test_round_trip() { + local tag + rng 32 >"$TMP/key" + rng 32 >"$TMP/other_key" + rng 100 >"$TMP/msg" + cp "$TMP/msg" "$TMP/other_msg" + flip_byte "$TMP/other_msg" 50 + + tag=$(hex_out "$BC_RUST" poly1305 -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" 32 "a 16-byte tag, as 32 hex characters" + + expect_ok "the tag must verify" "$BC_RUST" poly1305 -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "a modified tag must not verify" \ + "$BC_RUST" poly1305 -k "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg" + expect_fail "a modified message must not verify" \ + "$BC_RUST" poly1305 -k "$TMP/key" -v "$tag" <"$TMP/other_msg" + expect_fail "another key must not verify" \ + "$BC_RUST" poly1305 -k "$TMP/other_key" -v "$tag" <"$TMP/msg" +} + +# Test vector #1 is the all-zero key, which load_mac_key takes through its zeroized-key path: with +# r = 0 and s = 0 every tag is zero. +test_rfc8439_a3_vector_1_the_all_zero_key() { + local got + unhex "$A3_1_TEXT" >"$TMP/msg" + got=$(hex_out "$BC_RUST" poly1305 --key "$A3_1_KEY" -x <"$TMP/msg") + assert_eq "$got" "$A3_1_TAG" "A.3 #1 through --key" + unhex "$A3_1_KEY" >"$TMP/key" + got=$(hex_out "$BC_RUST" poly1305 -k "$TMP/key" -x <"$TMP/msg") + assert_eq "$got" "$A3_1_TAG" "A.3 #1 through --key-file" + expect_ok "the RFC's tag must verify" "$BC_RUST" poly1305 -k "$TMP/key" -v "$A3_1_TAG" <"$TMP/msg" +} + +# Test vector #4: both halves of the key non-zero, and a message ending in a partial block. +test_rfc8439_a3_vector_4() { + local got + unhex "$A3_4_TEXT" >"$TMP/msg" + got=$(hex_out "$BC_RUST" poly1305 --key "$A3_4_KEY" -x <"$TMP/msg") + assert_eq "$got" "$A3_4_TAG" "A.3 #4" + expect_ok "the RFC's tag must verify" \ + "$BC_RUST" poly1305 --key "$A3_4_KEY" -v "$A3_4_TAG" <"$TMP/msg" + expect_fail "the RFC's tag with its last bit flipped must not verify" \ + "$BC_RUST" poly1305 --key "$A3_4_KEY" -v "$(flip_hex "$A3_4_TAG" 15 128)" <"$TMP/msg" +} + +# A one-time key is exactly 32 bytes: 31 and 33 are refused by the subcommand's own check, and 65 +# by load_mac_key, which takes nothing over 64 bytes. +test_a_key_that_is_not_32_bytes_is_refused() { + rng 100 >"$TMP/msg" + local len + for len in 31 33; do + rng "$len" >"$TMP/key" + expect_fail "a $len-byte key" "$BC_RUST" poly1305 -k "$TMP/key" -x <"$TMP/msg" + assert_stderr_has "32-byte one-time key" + done + rng 65 >"$TMP/key" + expect_fail "a 65-byte key" "$BC_RUST" poly1305 -k "$TMP/key" -x <"$TMP/msg" + assert_stderr_has "64 bytes" +} + +run_all diff --git a/crypto/chacha20-poly1305-aead/Cargo.toml b/crypto/chacha20-poly1305-aead/Cargo.toml new file mode 100644 index 000000000..511f8ab6f --- /dev/null +++ b/crypto/chacha20-poly1305-aead/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bouncycastle-chacha20-poly1305-aead" +version.workspace = true +edition.workspace = true + +[features] +default = ["std"] +std = ["bouncycastle-core/std", "bouncycastle-chacha20/std"] + +[dependencies] +bouncycastle-chacha20.workspace = true +bouncycastle-cipher.workspace = true +bouncycastle-core.workspace = true +bouncycastle-poly1305.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 + +[[bench]] +name = "chacha20_poly1305_benches" +harness = false diff --git a/crypto/chacha20-poly1305-aead/benches/chacha20_poly1305_benches.rs b/crypto/chacha20-poly1305-aead/benches/chacha20_poly1305_benches.rs new file mode 100644 index 000000000..2e6f42a6e --- /dev/null +++ b/crypto/chacha20-poly1305-aead/benches/chacha20_poly1305_benches.rs @@ -0,0 +1,44 @@ +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_chacha20_poly1305_aead::ChaCha20Poly1305Key; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{AEADCipherEncryptor, RNG, SymmetricCipherEncryptor}; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_encrypt(c: &mut Criterion) { + let material = + KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::SymmetricCipherKey).unwrap(); + let key = ChaCha20Poly1305Key::from_key_material(&material).unwrap(); + let nonce = [0x24u8; 12]; + let data = random_data(DATA_LEN); + let mut out = vec![0u8; DATA_LEN]; + + let mut group = c.benchmark_group("chacha20_poly1305_aead::ChaCha20_Poly1305"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // A fixed nonce keeps OS randomness out of the timing; never do this outside a benchmark. + group.bench_function(format!("{DATA_LEN} bytes -- .do_encrypt_out()"), |b| { + b.iter(|| { + let mut enc = key.create_encryptor_nonce(&nonce); + enc.do_update_aad(b"header").unwrap(); + enc.do_encrypt_out(black_box(&data), &mut out).unwrap(); + black_box(enc.do_encrypt_final_detached().unwrap()); + black_box(&out); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_encrypt); +criterion_main!(benches); diff --git a/crypto/chacha20-poly1305-aead/src/lib.rs b/crypto/chacha20-poly1305-aead/src/lib.rs new file mode 100644 index 000000000..99d23d71b --- /dev/null +++ b/crypto/chacha20-poly1305-aead/src/lib.rs @@ -0,0 +1,750 @@ +//! ChaCha20-Poly1305 AEAD from [RFC 8439, section 2.8](https://www.rfc-editor.org/rfc/rfc8439#section-2.8). +//! +//! [`ChaCha20Poly1305Key`] is the key object: built once from 32 bytes of key material through +//! [`FromKeyMaterial`], it hands out [`ChaCha20_Poly1305`] / +//! [`ChaCha20_Poly1305`] workers through [`AeadEncryptionKey`] / +//! [`AeadDecryptionKey`], whose one-shots and the workers' [`AEADCipherEncryptor`] / +//! [`AEADCipherDecryptor`] streaming methods take the 16-byte tag inline or detached. Keys are 32 +//! bytes and nonces 12 bytes. Encryption draws the nonce from the RNG it is given; protocols that +//! supply their own may use [`AeadEncryptionKey::create_encryptor_nonce`], taking responsibility +//! for nonce uniqueness. A key/nonce may encrypt at most `64 * (2^32 - 1)` bytes. AAD must precede +//! all data updates, including empty updates. Empty AAD is always a no-op. Secret state is +//! zeroized on drop; streaming and output-buffer methods do not allocate. This is the IETF +//! construction, not the original 64-bit-nonce variant or XChaCha20. +//! +//! Both directions implement [`SuspendableKeyed`], so a message in progress can be suspended to a +//! byte array and resumed under the re-supplied key. +//! +//! # Usage Examples +//! +//! One-shot, with a detached tag: +//! +//! ``` +//! use bouncycastle_chacha20_poly1305_aead::ChaCha20Poly1305Key; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +//! let key = ChaCha20Poly1305Key::from_key_material(&material).unwrap(); +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let mut ciphertext = [0u8; 7]; +//! let (n, nonce, tag) = key.encrypt_detached_out(&mut rng, b"header", b"message", &mut ciphertext).unwrap(); +//! let mut plaintext = [0u8; 7]; +//! key.decrypt_detached_out(&nonce, b"header", &ciphertext[..n], &tag, &mut plaintext).unwrap(); +//! assert_eq!(&plaintext, b"message"); +//! ``` +//! +//! Streaming, with the tag inline after the ciphertext: +//! +//! ``` +//! use bouncycastle_chacha20_poly1305_aead::ChaCha20Poly1305Key; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +//! let key = ChaCha20Poly1305Key::from_key_material(&material).unwrap(); +//! let (mut enc, nonce) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! enc.do_update_aad(b"header").unwrap(); +//! let mut sealed = Vec::new(); +//! for piece in [&b"first part, "[..], &b"second part"[..]] { +//! let mut out = [0u8; 32]; +//! let n = enc.do_encrypt_out(piece, &mut out).unwrap(); +//! sealed.extend_from_slice(&out[..n]); +//! } +//! let (tag, _) = enc.do_encrypt_final().unwrap(); +//! sealed.extend_from_slice(&tag); +//! +//! let mut dec = key.create_decryptor(&nonce); +//! dec.do_update_aad(b"header").unwrap(); +//! let mut plaintext = vec![0u8; sealed.len()]; +//! let n = dec.do_decrypt_out(&sealed, &mut plaintext).unwrap(); +//! // Only once do_decrypt_final succeeds is plaintext[..n] authenticated. +//! let (_, m) = dec.do_decrypt_final().unwrap(); +//! assert_eq!(&plaintext[..n + m], b"first part, second part"); +//! ``` +//! +//! # Memory Usage +//! +//! | Type | In-memory size (bytes) | Suspended state size (bytes) | +//! |------|------------------------|------------------------------| +//! | [`ChaCha20Poly1305Key`] | 32 | -- | +//! | [`ChaCha20_Poly1305`] | 344 | [`SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN`] (176) | +//! | [`ChaCha20_Poly1305`] | 344 | [`SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN`] (193) | +//! +//! "In-memory size" is `core::mem::size_of` on a 64-bit target. There are no heap allocations and +//! no message-sized buffers, so this crate has no `mem_usage_benches` harness. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! - Never reuse a nonce under the same key. The key's `create_encryptor` and its one-shots draw +//! one; a caller of [`AeadEncryptionKey::create_encryptor_nonce`] or the `_nonce` one-shots is +//! responsible for uniqueness. Reuse exposes the XOR of the plaintexts and lets an attacker forge +//! tags. +//! - Streaming decryption releases **unauthenticated** bytes: do not use them until finalization +//! succeeds, and erase them on failure. The key's one-shots erase their output on tag failure. +//! - A suspended state holds live keystream and the Poly1305 accumulator: protect it as you would +//! the key, and resume it once. Two workers resumed from one state encrypt under one nonce. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +use bouncycastle_chacha20::hazmat::ChaCha20KeyStream; +use bouncycastle_chacha20::{ChaCha20, ChaCha20Key}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::KeyStream; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, +}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, MAC, RNG, StreamCipherDecryptor, + StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_poly1305::Poly1305; +use bouncycastle_poly1305::hazmat::key_from_bytes; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, add_lib_ver, check_lib_ver, +}; + +/// Key length in bytes. +pub const KEY_LEN: usize = 32; +/// Nonce length in bytes. +pub const NONCE_LEN: usize = 12; +/// Tag length, also the streaming final-buffer length, in bytes. +pub const TAG_LEN: usize = 16; +/// Maximum plaintext or ciphertext length (excluding the tag) per key/nonce. +pub const MAX_MESSAGE_LEN: u64 = u32::MAX as u64 * 64; + +/// ChaCha20-Poly1305 (RFC 8439 Sec 2.8) in one direction: `Dir` is [`Encrypting`] or +/// [`Decrypting`], and only that direction's methods exist on the type, so the wrong direction is a +/// compile error. One instance performs one operation under one (key, nonce) pair, and is made by +/// a [`ChaCha20Poly1305Key`]. +/// +/// The encrypting side holds nothing back: `FINAL_LEN` is `TAG_LEN`, so the inline +/// [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag. The decrypting side holds +/// back the last `TAG_LEN` bytes of ciphertext until the final, since until the stream ends they +/// may be the inline tag. +/// +/// ```compile_fail +/// # use bouncycastle_chacha20_poly1305_aead::ChaCha20Poly1305Key; +/// # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// # use bouncycastle_core::symmetric_key::{AeadDecryptionKey, FromKeyMaterial}; +/// # use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// # let material = KeyMaterial::<32>::from_bytes_as_type(&[7; 32], KeyType::SymmetricCipherKey).unwrap(); +/// # let key = ChaCha20Poly1305Key::from_key_material(&material).unwrap(); +/// let mut dec = key.create_decryptor(&[0u8; 12]); +/// dec.do_encrypt_out(&[0u8; 4], &mut [0u8; 4]); // no such method on the decrypting type +/// ``` +/// +/// A worker borrows its key for `'k`, and cannot be cloned, since two copies of one encryptor +/// would encrypt two messages under one nonce: +/// +/// ```compile_fail +/// # use bouncycastle_chacha20_poly1305_aead::ChaCha20_Poly1305; +/// # use bouncycastle_cipher::Encrypting; +/// fn cloneable() {} +/// cloneable::>(); +/// ``` +/// +/// which compiles without its last line: +/// +/// ``` +/// # use bouncycastle_chacha20_poly1305_aead::ChaCha20_Poly1305; +/// # use bouncycastle_cipher::Encrypting; +/// fn cloneable() {} +/// let _ = core::mem::size_of::>(); +/// ``` +#[allow(non_camel_case_types)] +pub struct ChaCha20_Poly1305<'k, Dir> { + // The data keystream, from block counter 1. Decrypting, it is RFC 8439 Sec 2.8's "ChaCha20 + // encryption function ... applied to the ciphertext": both directions are the same XOR. + cipher: ChaCha20<'k, Dir>, + mac: Poly1305, + aad_len: u64, + data_len: u64, + data_started: bool, + // Decrypting only: the newest up to `TAG_LEN` bytes of ciphertext, not yet released because + // they may be the inline tag. Ciphertext, so not a `Secret`; the encrypting side carries them + // unused, as `Gcm` and `Ascon_AEAD128` do. + held: [u8; TAG_LEN], + held_len: usize, +} + +impl Algorithm for ChaCha20_Poly1305<'_, Dir> { + const ALG_NAME: &'static str = "ChaCha20-Poly1305"; + // The cipher key has 256-bit strength; authentication is limited by the 128-bit tag. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +impl<'k, Dir> ChaCha20_Poly1305<'k, Dir> { + /// The setup of both directions from the message's keystream at block counter 0. + fn new(mut keystream: ChaCha20KeyStream<'k>) -> Self { + // RFC 8439 Sec 2.8: the one-time key is the first 32 bytes of block 0 (Sec 2.6.1, + // poly1305_key_gen), and the data is encrypted "with the initial counter set to 1". + // Taking block 0 leaves the keystream at counter 1, so the same keystream carries the data. + let mut block = Secret::new(); + // A derived one-time key, including the (possible) all-zero value. Key strength was + // already enforced on the parent cipher key; no entropy heuristic applies here. + let mac = key_from_bytes(poly1305_key_gen(&mut keystream, &mut block)).into_mac(); + Self { + cipher: ChaCha20::from_keystream(keystream), + mac, + aad_len: 0, + data_len: 0, + data_started: false, + held: [0u8; TAG_LEN], + held_len: 0, + } + } + + fn absorb_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + if aad.is_empty() { + return Ok(()); + } + if self.data_started { + return Err(SymmetricCipherError::StateError( + "AAD must precede ciphertext or plaintext", + )); + } + let length = self + .aad_len + .checked_add(aad.len() as u64) + .ok_or(SymmetricCipherError::StateError("ChaCha20-Poly1305 AAD length overflow"))?; + self.mac.do_update(aad); + self.aad_len = length; + Ok(()) + } + + // The cipher started at counter 1 and has processed exactly `data_len` bytes, so this is its + // own limit. Checking it here, before the MAC absorbs anything or the AAD phase ends, is what + // keeps a refused call free of side effects. + fn check_data_len(&self, length: usize) -> Result<(), SymmetricCipherError> { + if length as u64 > MAX_MESSAGE_LEN - self.data_len { + return Err(SymmetricCipherError::DataLimitExceeded); + } + Ok(()) + } + + fn start_data(&mut self) { + if !self.data_started { + self.mac.do_update(&[0u8; 15][..padding_len(self.aad_len)]); + self.data_started = true; + } + } + + /// The MAC with the rest of RFC 8439 Sec 2.8's input absorbed, the data's padding and the two + /// lengths, ready for its final. + fn finish_mac(mut self) -> Poly1305 { + self.start_data(); + self.mac.do_update(&[0u8; 15][..padding_len(self.data_len)]); + self.mac.do_update(&self.aad_len.to_le_bytes()); + self.mac.do_update(&self.data_len.to_le_bytes()); + self.mac + } + + fn finish_tag(self) -> [u8; TAG_LEN] { + let mut tag = [0u8; TAG_LEN]; + // infallible: Poly1305 refuses only a buffer shorter than its tag. + self.finish_mac().do_final_out(&mut tag).unwrap(); + tag + } +} + +impl ChaCha20_Poly1305<'_, Encrypting> { + fn encrypt_bytes(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + // do_encrypt_out, the only caller, has already checked the length. + self.start_data(); + self.cipher.do_encrypt_inplace(data)?; + self.mac.do_update(data); + self.data_len += data.len() as u64; + Ok(()) + } +} + +impl ChaCha20_Poly1305<'_, Decrypting> { + fn decrypt_bytes(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.check_data_len(data.len())?; + self.start_data(); + self.mac.do_update(data); + self.cipher.do_decrypt_inplace(data)?; + self.data_len += data.len() as u64; + Ok(()) + } + + fn verify_tag(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + // Poly1305's do_verify_final compares in constant time. + if self.finish_mac().do_verify_final(tag) { + Ok(()) + } else { + Err(SymmetricCipherError::AEADTagCheckFailed) + } + } +} + +fn padding_len(length: u64) -> usize { + ((16 - (length & 15)) & 15) as usize +} + +/// RFC 8439 Sec 2.6.1's `poly1305_key_gen`: block 0 of the message's keystream into `block`, whose +/// first 32 bytes are returned as the one-time key. Leaves `keystream` at counter 1. +fn poly1305_key_gen<'b>( + keystream: &mut ChaCha20KeyStream<'_>, + block: &'b mut Secret<[[u8; 64]; 1]>, +) -> &'b [u8; 32] { + keystream.apply_blocks(&mut **block); + // infallible: the slice is exactly the 32 bytes of the one-time key. + block[0][..32].try_into().unwrap() +} + +impl SymmetricCipherEncryptor for ChaCha20_Poly1305<'_, Encrypting> { + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + self.check_data_len(plaintext.len())?; + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.encrypt_bytes(out)?; + Ok(plaintext.len()) + } + + fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + Ok((self.finish_tag(), TAG_LEN)) + } + + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len.saturating_add(TAG_LEN) + } +} + +impl AEADCipherEncryptor for ChaCha20_Poly1305<'_, Encrypting> { + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.absorb_aad(aad) + } + + /// Nothing is ever held back to flush, so `ciphertext` is left zeroed. + fn do_encrypt_final_detached_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + Ok((0, self.finish_tag())) + } +} + +impl SymmetricCipherDecryptor for ChaCha20_Poly1305<'_, Decrypting> { + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len.saturating_sub(TAG_LEN - self.held_len) + } + + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let release = self.do_decrypt_out_len(ciphertext.len()); + if plaintext.len() < release { + return Err(SymmetricCipherError::OutputBufferTooSmall(release)); + } + self.check_data_len(release)?; + let from_held = release.min(self.held_len); + let from_input = release - from_held; + let out = &mut plaintext[..release]; + out[..from_held].copy_from_slice(&self.held[..from_held]); + out[from_held..].copy_from_slice(&ciphertext[..from_input]); + // Even a zero-length update ends the AAD phase, as required by the core traits. + self.decrypt_bytes(out)?; + let kept = self.held_len - from_held; + self.held.copy_within(from_held..self.held_len, 0); + let new_len = kept + ciphertext.len() - from_input; + self.held[kept..new_len].copy_from_slice(&ciphertext[from_input..]); + self.held_len = new_len; + Ok(release) + } + + fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + if self.held_len != TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + let tag = self.held; + self.verify_tag(&tag)?; + Ok(([0u8; TAG_LEN], 0)) + } + + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } +} + +impl AEADCipherDecryptor for ChaCha20_Poly1305<'_, Decrypting> { + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.absorb_aad(aad) + } + + fn do_decrypt_final_detached_out( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + plaintext.fill(0); + let n = self.held_len; + // Decrypted aside and copied out only once the tag verifies, so a failed check never + // writes unauthenticated bytes to `plaintext`. + let mut last: Secret<[u8; TAG_LEN]> = Secret::new(); + last[..n].copy_from_slice(&self.held[..n]); + self.decrypt_bytes(&mut last[..n])?; + self.verify_tag(tag)?; + plaintext[..n].copy_from_slice(&last[..n]); + Ok(n) + } +} + +/// The inner ChaCha20's share of a suspended state: its nonce, block counter and partly used +/// keystream block. +const CHACHA20_STATE_LEN: usize = + as SuspendableComponent<'static>>::STATE_LEN; +/// The Poly1305's share: its accumulator and buffered partial block. +const POLY1305_STATE_LEN: usize = >::STATE_LEN; + +/// Length in bytes of the serialized state of `ChaCha20_Poly1305`: the 3-byte library +/// version, the ChaCha20 and Poly1305 states, the AAD and data byte counts, and a call-state byte. +/// No key is in it: the key object is re-supplied to [`SuspendableKeyed::from_suspended`], and the +/// one-time Poly1305 key is re-derived from it and the nonce. +pub const SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN: usize = + LIB_VERSION_LEN + CHACHA20_STATE_LEN + POLY1305_STATE_LEN + 8 + 8 + 1; + +/// Length in bytes of the serialized state of `ChaCha20_Poly1305`: the encrypting +/// side's layout, then the up to `TAG_LEN` bytes it holds back and their count. +pub const SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN: usize = + SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN + TAG_LEN + 1; + +/// The suspended call-state byte: the direction's marker, with `DATA_STARTED` set once the AAD +/// phase has ended. Both markers are non-zero, so a zeroed byte is refused. Never renumber them, +/// or older states are misread. +const ENC_DIR_BITS: u8 = 0x10; +const DEC_DIR_BITS: u8 = 0x20; +const DATA_STARTED: u8 = 0x01; + +impl<'k> SuspendableKeyed<'k, SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN> + for ChaCha20_Poly1305<'k, Encrypting> +{ + type Key = ChaCha20Poly1305Key; + + fn suspend(self) -> [u8; SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN] { + self.suspend_as(ENC_DIR_BITS) + } + + /// A state suspended by the decrypting side is [`SuspendableError::InvalidData`] here. + fn from_suspended( + serialized_state: [u8; SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN], + key: &'k Self::Key, + ) -> Result { + Self::resume_as(&serialized_state, key, ENC_DIR_BITS) + } +} + +/// The encrypting side's layout, then the held-back bytes and their count; see +/// [`SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN`]. +impl<'k> SuspendableKeyed<'k, SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN> + for ChaCha20_Poly1305<'k, Decrypting> +{ + type Key = ChaCha20Poly1305Key; + + fn suspend(self) -> [u8; SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN] { + let mut out = [0u8; SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN]; + let (head, tail) = out.split_at_mut(SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN); + head.copy_from_slice(&self.suspend_as(DEC_DIR_BITS)); + tail[..TAG_LEN].copy_from_slice(&self.held); + tail[TAG_LEN] = self.held_len as u8; + out + } + + /// A state suspended by the encrypting side, or a held-back count above `TAG_LEN`, is + /// [`SuspendableError::InvalidData`]. + fn from_suspended( + serialized_state: [u8; SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN], + key: &'k Self::Key, + ) -> Result { + let (head, tail) = + serialized_state.split_at(SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN); + // infallible: `head` is exactly the encrypting side's length. + let mut dec = Self::resume_as(head.try_into().unwrap(), key, DEC_DIR_BITS)?; + dec.held.copy_from_slice(&tail[..TAG_LEN]); + dec.held_len = tail[TAG_LEN] as usize; + if dec.held_len > TAG_LEN { + return Err(SuspendableError::InvalidData); + } + Ok(dec) + } +} + +impl<'k, Dir> ChaCha20_Poly1305<'k, Dir> { + /// The body of both directions' `suspend`, up to the end of the encrypting side's layout; + /// `dir_bits` marks the direction. + fn suspend_as(&self, dir_bits: u8) -> [u8; SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN] { + let mut out = [0u8; SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN]; + let (cipher, rest) = add_lib_ver(&mut out).split_at_mut(CHACHA20_STATE_LEN); + self.cipher.write_state(cipher); + let (mac, rest) = rest.split_at_mut(POLY1305_STATE_LEN); + self.mac.write_state(mac); + let mut w = CursorMut::new(rest); + w.u64(self.aad_len); + w.u64(self.data_len); + w.u8(dir_bits | if self.data_started { DATA_STARTED } else { 0 }); + debug_assert!(w.is_done()); + out + } + + /// The body of both directions' `from_suspended`: a call-state byte not marked `dir_bits` is + /// refused, so a state never resumes in the other direction. + fn resume_as( + state: &[u8; SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN], + key: &'k ChaCha20Poly1305Key, + dir_bits: u8, + ) -> Result { + let (cipher_state, rest) = check_lib_ver(state, None)?.split_at(CHACHA20_STATE_LEN); + let (mac_state, rest) = rest.split_at(POLY1305_STATE_LEN); + let mut r = Cursor::new(rest); + let aad_len = r.u64(); + let data_len = r.u64(); + let call_state = r.u8(); + debug_assert!(r.is_done()); + if call_state & !DATA_STARTED != dir_bits { + return Err(SuspendableError::InvalidData); + } + let data_started = call_state & DATA_STARTED != 0; + if data_len > MAX_MESSAGE_LEN || (data_len != 0 && !data_started) { + return Err(SuspendableError::InvalidData); + } + let cipher = + as SuspendableComponent<'k>>::read_state(cipher_state, &key.key)?; + // The data keystream starts at counter 1 and has run one block per 64 bytes of data + // begun, so any other counter would repeat keystream, block 0 (the one-time key) included. + if cipher.keystream().remaining_blocks() != (1 << 32) - 1 - data_len.div_ceil(64) { + return Err(SuspendableError::InvalidData); + } + // The nonce leads ChaCha20's state, and block 0 under it is the one-time key, as in `new`. + // infallible: the slice is exactly NONCE_LEN bytes. + let nonce: &[u8; NONCE_LEN] = cipher_state[..NONCE_LEN].try_into().unwrap(); + let mut block = Secret::new(); + let one_time_key = poly1305_key_gen(&mut keystream_at(&key.key, nonce), &mut block); + let mac = >::read_state(mac_state, one_time_key)?; + Ok(Self { cipher, mac, aad_len, data_len, data_started, held: [0u8; TAG_LEN], held_len: 0 }) + } +} + +/// A ChaCha20-Poly1305 key: the 256-bit key of RFC 8439 Sec 2.8, validated once, from which the +/// encryptor and decryptor for each message are made. Built through [`FromKeyMaterial`] from +/// material tagged [`KeyType::SymmetricCipherKey`] of 256-bit strength. The key words live in a +/// `Secret`, zeroized on drop; there is no `Debug` and no way to read them back. +#[derive(Clone)] +pub struct ChaCha20Poly1305Key { + // The key as a ChaCha20 key, which is what it is (RFC 8439 Sec 2.8), so that a resumed worker's + // keystream comes back through ChaCha20's own resume. + key: ChaCha20Key, +} + +impl FromKeyMaterial for ChaCha20Poly1305Key { + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + /// # Errors + /// As [`ChaCha20Key`]'s. + fn from_key_material(key: &KeyMaterial) -> Result { + Ok(Self { key: ChaCha20Key::from_key_material(key)? }) + } +} + +impl AeadEncryptionKey for ChaCha20Poly1305Key { + type Encryptor<'k> + = ChaCha20_Poly1305<'k, Encrypting> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok((ChaCha20_Poly1305::new(keystream_at(&self.key, &nonce)), nonce)) + } + + /// Under a nonce the caller supplies, who takes on the obligation that it never repeat under + /// this key: RFC 8439 Sec 2.8's nonce is "different for each invocation with the same key". + fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { + ChaCha20_Poly1305::new(keystream_at(&self.key, nonce)) + } +} + +impl AeadDecryptionKey for ChaCha20Poly1305Key { + type Decryptor<'k> + = ChaCha20_Poly1305<'k, Decrypting> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + ChaCha20_Poly1305::new(keystream_at(&self.key, nonce)) + } +} + +/// The ChaCha20 keystream under `key` and `nonce` from block counter 0, taken from the key's own +/// decryptor: its one public way to start a keystream at a given nonce. Only the keystream is +/// kept, so the decryptor's direction is immaterial. +fn keystream_at<'k>(key: &'k ChaCha20Key, nonce: &[u8; NONCE_LEN]) -> ChaCha20KeyStream<'k> { + key.create_decryptor(nonce).keystream().clone() +} + +#[cfg(test)] +mod tests { + //! The message and AAD length limits. Reaching them through the public API means processing + //! 256 GiB of data, or 2^64 bytes of AAD, so these position the private state next to the + //! boundary instead: the "cannot be tested from integration tests" case QUALITY_AND_STYLE.md + //! carves out for unit tests. + + use super::*; + use bouncycastle_chacha20::hazmat::ChaCha20KeyState; + + fn key() -> ChaCha20KeyState { + let material = + KeyMaterial::::from_bytes_as_type(&[7; 32], KeyType::SymmetricCipherKey); + ChaCha20KeyState::new(&material.unwrap()).unwrap() + } + + fn fresh(key: &ChaCha20KeyState) -> ChaCha20_Poly1305<'_, Dir> { + ChaCha20_Poly1305::new(ChaCha20KeyStream::start_at(key, &[9; 12], 0)) + } + + /// One block of data left: the keystream at its last counter, and `data_len` to match. + fn near_limit(key: &ChaCha20KeyState) -> ChaCha20_Poly1305<'_, Dir> { + let mut cipher = fresh(key); + // `check_data_len` relies on MAX_MESSAGE_LEN being exactly the data keystream's capacity. + assert_eq!(cipher.cipher.keystream().remaining_blocks() * 64, MAX_MESSAGE_LEN); + cipher.cipher = + ChaCha20::from_keystream(ChaCha20KeyStream::start_at(key, &[9; 12], u32::MAX)); + cipher.data_len = MAX_MESSAGE_LEN - 64; + cipher + } + + #[test] + fn encryption_limit_errors_leave_state_and_output_untouched() { + let key = key(); + let mut enc = near_limit::(&key); + let mut out = [0xa5; 65]; + assert!(matches!( + enc.do_encrypt_out(&[0; 65], &mut out), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(out, [0; 65], "a refused call zeroizes its output buffer"); + assert!(!enc.data_started); + enc.do_update_aad(b"aad").unwrap(); + enc.do_encrypt_out(&[0; 63], &mut out[..63]).unwrap(); + assert_eq!(MAX_MESSAGE_LEN - enc.data_len, 1); + let mut short = [0xa5; 2]; + assert!(matches!( + enc.do_encrypt_out(&[0; 2], &mut short), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(short, [0; 2]); + enc.do_encrypt_out(&[0], &mut out[63..64]).unwrap(); + assert_eq!(enc.data_len, MAX_MESSAGE_LEN); + enc.do_encrypt_out(&[], &mut []).unwrap(); + let mut reference = near_limit::(&key); + reference.do_update_aad(b"aad").unwrap(); + let mut expected = [0; 64]; + reference.do_encrypt_out(&[0; 64], &mut expected).unwrap(); + assert_eq!(out[..64], expected); + assert_eq!(enc.do_encrypt_final().unwrap(), reference.do_encrypt_final().unwrap()); + } + + #[test] + fn inline_decryption_allows_the_last_ciphertext_block_plus_tag() { + let key = key(); + let mut enc = near_limit::(&key); + let mut inline = [0; 80]; + enc.do_encrypt_out(&[0x42; 64], &mut inline[..64]).unwrap(); + inline[64..].copy_from_slice(&enc.do_encrypt_final().unwrap().0); + let mut dec = near_limit::(&key); + let mut out = [0xa5; 65]; + assert!(matches!( + dec.do_decrypt_out(&[0; 81], &mut out), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(out, [0; 65], "a refused call zeroizes its output buffer"); + assert_eq!(dec.held_len, 0); + assert!(!dec.data_started); + assert_eq!(dec.do_decrypt_out(&inline, &mut out).unwrap(), 64); + assert_eq!(dec.data_len, MAX_MESSAGE_LEN); + assert_eq!(dec.do_decrypt_final().unwrap().1, 0); + assert_eq!(&out[..64], &[0x42; 64]); + assert_eq!(out[64], 0, "bytes past the returned count are zero"); + } + + #[test] + fn detached_final_checks_the_limit_and_clears_its_buffer() { + let key = key(); + let mut dec = near_limit::(&key); + // The update must reserve a possible inline tag. Only detached finalization can know + // that these extra 16 bytes are actually ciphertext exceeding the counter limit. + dec.do_decrypt_out(&[0; 80], &mut [0; 64]).unwrap(); + let mut out = [0xa5; 16]; + assert!(matches!( + dec.do_decrypt_final_detached_out(&[0; 16], &mut out), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert_eq!(out, [0; 16]); + } + + /// An encryptor that has used the whole message limit is a state the worker writes, so it + /// suspends and resumes, and the resumed worker still refuses more data. + #[test] + fn a_worker_at_the_message_limit_suspends_and_resumes() { + let state = key(); + let mut enc = near_limit::(&state); + enc.do_encrypt_out(&[0; 64], &mut [0; 64]).unwrap(); + assert_eq!(enc.data_len, MAX_MESSAGE_LEN); + let material = + KeyMaterial::::from_bytes_as_type(&[7; 32], KeyType::SymmetricCipherKey); + let key = ChaCha20Poly1305Key::from_key_material(&material.unwrap()).unwrap(); + let mut resumed = + ChaCha20_Poly1305::::from_suspended(enc.suspend(), &key).unwrap(); + assert!(matches!( + resumed.do_encrypt_out(&[0], &mut [0]), + Err(SymmetricCipherError::DataLimitExceeded) + )); + } + + #[test] + fn aad_length_overflow_is_rejected_before_absorption() { + let key = key(); + let mut cipher = fresh::(&key); + let mut reference = fresh::(&key); + cipher.aad_len = u64::MAX - 1; + reference.aad_len = u64::MAX - 1; + assert!(matches!(cipher.absorb_aad(&[1, 2]), Err(SymmetricCipherError::StateError(_)))); + assert_eq!(cipher.aad_len, u64::MAX - 1); + cipher.absorb_aad(&[1]).unwrap(); + reference.absorb_aad(&[1]).unwrap(); + cipher.absorb_aad(&[]).unwrap(); + assert_eq!(cipher.aad_len, u64::MAX); + assert_eq!(cipher.finish_tag(), reference.finish_tag()); + } +} diff --git a/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_bc-test-data.rs b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_bc-test-data.rs new file mode 100644 index 000000000..7320efdb1 --- /dev/null +++ b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_bc-test-data.rs @@ -0,0 +1,28 @@ +//! RFC fixtures live at bc-test-data/crypto/rfc8439. +mod common; + +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data_json; + +#[test] +fn rfc8439_aead_encryption_and_decryption() { + let Some(doc) = bc_test_data_json("crypto/rfc8439", "chacha20-poly1305-aead.json") else { + return; + }; + let cases = doc["tests"].as_array().unwrap(); + assert_eq!(cases.len(), 2); + for case in cases { + let key = common::key(&common::decode(case, "key")); + let nonce: [u8; 12] = common::decode(case, "nonce").try_into().unwrap(); + let tag: [u8; 16] = common::decode(case, "tag").try_into().unwrap(); + common::check_valid( + &key, + &nonce, + &common::decode(case, "aad"), + &common::decode(case, "msg"), + &common::decode(case, "ct"), + &tag, + &[1, 3, 15, 16, 17, 63, 64, 65, 1024], + ); + } + println!("RFC 8439 ChaCha20-Poly1305: {} vectors passed", cases.len()); +} diff --git a/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_tests.rs b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_tests.rs new file mode 100644 index 000000000..447c0ae04 --- /dev/null +++ b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_tests.rs @@ -0,0 +1,273 @@ +use bouncycastle_chacha20::ChaCha20; +use bouncycastle_chacha20_poly1305_aead::{ + ChaCha20_Poly1305, ChaCha20Poly1305Key, MAX_MESSAGE_LEN, + SUSPENDED_CHACHA20_POLY1305_DECRYPTOR_STATE_LEN as DEC_STATE_LEN, + SUSPENDED_CHACHA20_POLY1305_ENCRYPTOR_STATE_LEN as ENC_STATE_LEN, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::aead::{ + TestFrameworkAEADCipher, TestFrameworkAEADTaggedLayout, +}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; +use bouncycastle_rng::HashDRBG_SHA512; + +type Enc<'k> = ChaCha20_Poly1305<'k, Encrypting>; +type Dec<'k> = ChaCha20_Poly1305<'k, Decrypting>; + +fn material() -> KeyMaterial<32> { + KeyMaterial::<32>::from_bytes_as_type(&[7; 32], KeyType::SymmetricCipherKey).unwrap() +} + +fn key() -> ChaCha20Poly1305Key { + ChaCha20Poly1305Key::from_key_material(&material()).unwrap() +} + +#[test] +fn core_aead_and_symmetric_contracts() { + let key = key(); + TestFrameworkAEADCipher::new() + .test_key::<32, 12, 16, 16, _>(&key, &mut HashDRBG_SHA512::new_from_os()); + TestFrameworkAEADTaggedLayout::new().test_key::<32, 12, 16, 16, _>(&key); +} + +/// The key's construction suite: the type and strength policy of `from_key_material`. +#[test] +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::<32, ChaCha20Poly1305Key>(Enc::ALG_NAME, SecurityStrength::_256bit); +} + +/// Both directions of each cipher report one name, and the AEAD's is not the keystream's. +#[test] +fn the_two_algorithms_have_distinct_names() { + assert_eq!(Enc::ALG_NAME, "ChaCha20-Poly1305"); + assert_eq!(Dec::ALG_NAME, Enc::ALG_NAME); + assert_eq!(ChaCha20::<'static, Encrypting>::ALG_NAME, "ChaCha20"); + assert_eq!(ChaCha20::<'static, Decrypting>::ALG_NAME, "ChaCha20"); +} + +/// RFC 8439 Sec 2.8: "P_MAX (maximum size of the plaintext) is 274,877,906,880 bytes". +#[test] +fn max_message_len_is_the_rfc_p_max() { + assert_eq!(MAX_MESSAGE_LEN, 274_877_906_880); +} + +#[test] +fn failed_update_can_be_retried_without_ending_aad() { + let key = key(); + let nonce = [9; 12]; + let msg = [0x42; 65]; + let mut enc = key.create_encryptor_nonce(&nonce); + let mut short = [0xa5; 64]; + assert_eq!( + enc.do_encrypt_out(&msg, &mut short), + Err(SymmetricCipherError::OutputBufferTooSmall(65)) + ); + assert_eq!(short, [0; 64], "a refused call zeroizes its output buffer"); + enc.do_update_aad(b"aad").unwrap(); + let mut ct = [0u8; 65]; + enc.do_encrypt_out(&msg, &mut ct).unwrap(); + let (tag, _) = enc.do_encrypt_final().unwrap(); + let mut dec = key.create_decryptor(&nonce); + let mut short = [0xa5; 48]; + assert_eq!( + dec.do_decrypt_out(&ct, &mut short), + Err(SymmetricCipherError::OutputBufferTooSmall(49)) + ); + assert_eq!(short, [0; 48], "a refused call zeroizes its output buffer"); + dec.do_update_aad(b"aad").unwrap(); + let mut pt = [0u8; 65]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + let (last, m) = dec.do_decrypt_final_detached(&tag).unwrap(); + pt[n..n + m].copy_from_slice(&last[..m]); + assert_eq!(pt, msg); +} + +#[test] +fn key_capacity_does_not_replace_key_length() { + for len in 0..32 { + let mut short = material(); + short.set_key_len(len).unwrap(); + assert!(matches!( + ChaCha20Poly1305Key::from_key_material(&short), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength)) + )); + } +} + +/// The figures in the crate docs' Memory Usage table. +#[cfg(target_pointer_width = "64")] +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + assert_eq!(size_of::(), 32); + assert_eq!(size_of::>(), 344); + assert_eq!(size_of::>(), 344); + assert_eq!(ENC_STATE_LEN, 176); + assert_eq!(DEC_STATE_LEN, 193); +} + +/// The framework's suspend suite works on a clone of the value it is given, and a worker cannot be +/// cloned, since two copies would encrypt under one nonce. These stand in for one: a suspended +/// state that every resume puts through the worker's own `from_suspended`. +#[derive(Clone)] +struct SuspendedEnc([u8; ENC_STATE_LEN]); + +impl<'k> SuspendableKeyed<'k, ENC_STATE_LEN> for SuspendedEnc { + type Key = ChaCha20Poly1305Key; + fn suspend(self) -> [u8; ENC_STATE_LEN] { + self.0 + } + fn from_suspended( + state: [u8; ENC_STATE_LEN], + key: &'k Self::Key, + ) -> Result { + Enc::from_suspended(state, key).map(|enc| Self(enc.suspend())) + } +} + +#[derive(Clone)] +struct SuspendedDec([u8; DEC_STATE_LEN]); + +impl<'k> SuspendableKeyed<'k, DEC_STATE_LEN> for SuspendedDec { + type Key = ChaCha20Poly1305Key; + fn suspend(self) -> [u8; DEC_STATE_LEN] { + self.0 + } + fn from_suspended( + state: [u8; DEC_STATE_LEN], + key: &'k Self::Key, + ) -> Result { + Dec::from_suspended(state, key).map(|dec| Self(dec.suspend())) + } +} + +/// A worker part-way through its data, in each direction. +fn mid_stream_states(key: &ChaCha20Poly1305Key) -> ([u8; ENC_STATE_LEN], [u8; DEC_STATE_LEN]) { + let mut enc = key.create_encryptor_nonce(&[5; 12]); + enc.do_update_aad(b"header").unwrap(); + enc.do_encrypt_out(&[1; 70], &mut [0; 70]).unwrap(); + let mut dec = key.create_decryptor(&[5; 12]); + dec.do_update_aad(b"header").unwrap(); + dec.do_decrypt_out(&[1; 70], &mut [0; 54]).unwrap(); + (enc.suspend(), dec.suspend()) +} + +#[test] +fn the_suspended_states_pass_the_framework_suite() { + let key = key(); + let (enc, dec) = mid_stream_states(&key); + TestFrameworkSuspendableKeyedState::new().test(&SuspendedEnc(enc), &key); + TestFrameworkSuspendableKeyedState::new().test(&SuspendedDec(dec), &key); +} + +/// Each direction suspended part-way through the AAD and again part-way through the data, at block +/// boundaries and inside blocks, and resumed under the re-supplied key, gives the uninterrupted +/// output. +#[test] +fn suspend_and_resume_mid_stream_matches_an_uninterrupted_run() { + let key = key(); + let nonce = [5; 12]; + let aad: Vec = (0..37u8).collect(); + let msg: Vec = (0..150u8).map(|b| b.wrapping_mul(3)).collect(); + let mut sealed = vec![0u8; msg.len() + 16]; + key.encrypt_with_aad_nonce_out(&nonce, &aad, &msg, &mut sealed).unwrap(); + + for aad_split in [0, 1, 16, 20, 37] { + for split in [0, 1, 63, 64, 65, 70, 150] { + let mut enc = key.create_encryptor_nonce(&nonce); + enc.do_update_aad(&aad[..aad_split]).unwrap(); + let mut enc = Enc::from_suspended(enc.suspend(), &key).unwrap(); + enc.do_update_aad(&aad[aad_split..]).unwrap(); + let mut out = vec![0u8; sealed.len()]; + enc.do_encrypt_out(&msg[..split], &mut out[..split]).unwrap(); + let mut enc = Enc::from_suspended(enc.suspend(), &key).unwrap(); + enc.do_encrypt_out(&msg[split..], &mut out[split..msg.len()]).unwrap(); + out[msg.len()..].copy_from_slice(&enc.do_encrypt_final().unwrap().0); + assert_eq!(out, sealed, "encrypting, AAD split {aad_split}, data split {split}"); + } + for split in [0, 1, 15, 16, 63, 90, 150, 166] { + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(&aad[..aad_split]).unwrap(); + let mut dec = Dec::from_suspended(dec.suspend(), &key).unwrap(); + dec.do_update_aad(&aad[aad_split..]).unwrap(); + let mut out = vec![0u8; sealed.len()]; + let mut n = dec.do_decrypt_out(&sealed[..split], &mut out).unwrap(); + let mut dec = Dec::from_suspended(dec.suspend(), &key).unwrap(); + n += dec.do_decrypt_out(&sealed[split..], &mut out[n..]).unwrap(); + assert_eq!(dec.do_decrypt_final().unwrap().1, 0, "the held bytes were the tag"); + assert_eq!( + &out[..n], + &msg[..], + "decrypting, AAD split {aad_split}, data split {split}" + ); + } + } +} + +/// Resume refuses what the worker never writes: the other direction's state (each cut or padded to +/// the other's length), a zeroed or unknown call-state byte, data counted before the AAD phase +/// ended or past the message limit, a block counter out of step with the data, a Poly1305 state +/// out of its bounds, and a held-back count above the tag length. +#[test] +fn resume_refuses_states_the_worker_never_writes() { + // Offsets after the 3-byte version: ChaCha20's nonce and block counter lead its 92 bytes, then + // Poly1305's limbs, buffer and buffered length take 64, then the two counts and the call state. + const COUNTER: usize = 3 + 12; + const POLY1305: usize = 3 + 92; + const BUFFERED: usize = POLY1305 + 40 + 16; + const DATA_LEN: usize = ENC_STATE_LEN - 9; + const CALL_STATE: usize = ENC_STATE_LEN - 1; + let key = key(); + let (enc, dec) = mid_stream_states(&key); + let enc_refused = |s: [u8; ENC_STATE_LEN]| { + matches!(Enc::from_suspended(s, &key), Err(SuspendableError::InvalidData)) + }; + let dec_refused = |s: [u8; DEC_STATE_LEN]| { + matches!(Dec::from_suspended(s, &key), Err(SuspendableError::InvalidData)) + }; + let with = |offset: usize, value: u64| { + let mut s = enc; + s[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + s + }; + let with_byte = |offset: usize, value: u8| { + let mut s = enc; + s[offset] = value; + s + }; + assert!(Enc::from_suspended(enc, &key).is_ok()); + assert!(Dec::from_suspended(dec, &key).is_ok()); + + let mut enc_as_dec = [0u8; DEC_STATE_LEN]; + enc_as_dec[..ENC_STATE_LEN].copy_from_slice(&enc); + assert!(dec_refused(enc_as_dec), "an encrypting state, padded to the decrypting length"); + assert!(enc_refused(dec[..ENC_STATE_LEN].try_into().unwrap()), "a decrypting state, cut short"); + + assert!(enc_refused(with_byte(CALL_STATE, 0)), "a zeroed call state"); + assert!(enc_refused(with_byte(CALL_STATE, enc[CALL_STATE] ^ 0x40)), "an unknown marker"); + assert!(enc_refused(with_byte(CALL_STATE, enc[CALL_STATE] & !1)), "data before the AAD ended"); + assert!(enc_refused(with(DATA_LEN, MAX_MESSAGE_LEN + 1)), "data past the message limit"); + + let counter = u64::from_le_bytes(enc[COUNTER..COUNTER + 8].try_into().unwrap()); + assert_eq!(counter, 3, "block 0 is the one-time key, then 70 bytes take blocks 1 and 2"); + assert!(enc_refused(with(COUNTER, counter + 1)), "a counter ahead of the data"); + assert!(enc_refused(with(COUNTER, counter - 1)), "a counter behind it, repeating keystream"); + assert!(enc_refused(with(COUNTER, 0)), "block 0, the one-time key"); + + assert!(enc_refused(with(POLY1305, 1 << 26)), "a Poly1305 limb past 26 bits"); + assert!(enc_refused(with(BUFFERED, 16)), "a whole Poly1305 block buffered"); + + let mut held = dec; + held[DEC_STATE_LEN - 1] = 17; + assert!(dec_refused(held), "more held back than the tag"); +} diff --git a/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_wycheproof.rs b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_wycheproof.rs new file mode 100644 index 000000000..26748259b --- /dev/null +++ b/crypto/chacha20-poly1305-aead/tests/chacha20_poly1305_wycheproof.rs @@ -0,0 +1,90 @@ +//! Uses the sibling Wycheproof checkout. +//! No JSON vectors are embedded or packaged with this crate. Unsupported nonce/tag sizes +//! cannot be passed to the fixed-array API; those cases are counted and reported separately. +mod common; + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::symmetric_key::AeadDecryptionKey; +use bouncycastle_core::traits::{AEADCipherDecryptor, SymmetricCipherDecryptor}; +use bouncycastle_core_test_framework::test_data_loaders::wycheproof_json; + +#[test] +fn wycheproof_chacha20_poly1305() { + let Some(doc) = wycheproof_json("chacha20_poly1305_test.json") else { + return; + }; + assert_eq!(doc["algorithm"], "CHACHA20-POLY1305"); + let mut valid = 0; + let mut invalid = 0; + let mut unsupported = 0; + for group in doc["testGroups"].as_array().unwrap() { + for case in group["tests"].as_array().unwrap() { + let id = &case["tcId"]; + let valid_case = match case["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!("tcId {id}: unexpected result {other}"), + }; + let key_bytes = common::decode(case, "key"); + let nonce = common::decode(case, "iv"); + let tag = common::decode(case, "tag"); + if key_bytes.len() != 32 || nonce.len() != 12 || tag.len() != 16 { + assert!(!valid_case, "tcId {id}: unsupported parameters on a valid case"); + unsupported += 1; + continue; + } + let key = common::key(&key_bytes); + let nonce = nonce.try_into().unwrap(); + let tag = tag.try_into().unwrap(); + let aad = common::decode(case, "aad"); + let msg = common::decode(case, "msg"); + let ct = common::decode(case, "ct"); + if valid_case { + common::check_valid(&key, &nonce, &aad, &msg, &ct, &tag, &[1, 17, 65]); + valid += 1; + } else { + let mut out = vec![0xa5; ct.len()]; + assert_eq!( + key.decrypt_detached_out(&nonce, &aad, &ct, &tag, &mut out), + Err(SymmetricCipherError::AEADTagCheckFailed), + "tcId {id}" + ); + // The key's one-shot zeroizes the whole output buffer on entry, and leaves it so + // when the tag fails. + assert!(out.iter().all(|&b| b == 0), "tcId {id}"); + let mut inline = ct.clone(); + inline.extend_from_slice(&tag); + assert_eq!( + key.decrypt_with_aad_out(&nonce, &aad, &inline, &mut out), + Err(SymmetricCipherError::AEADTagCheckFailed), + "tcId {id}" + ); + assert!(out.iter().all(|&b| b == 0), "tcId {id}"); + let mut in_place = ct.clone(); + assert_eq!( + key.decrypt_detached_in_place(&nonce, &aad, &mut in_place, &tag), + Err(SymmetricCipherError::AEADTagCheckFailed), + "tcId {id}" + ); + assert!(in_place.iter().all(|&b| b == 0), "tcId {id}"); + + // The detached final-output buffer is also erased on authentication failure. + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(&aad).unwrap(); + dec.do_decrypt_out(&ct, &mut out).unwrap(); + let mut last = [0xa5; 16]; + assert_eq!( + dec.do_decrypt_final_detached_out(&tag, &mut last), + Err(SymmetricCipherError::AEADTagCheckFailed) + ); + assert_eq!(last, [0; 16]); + invalid += 1; + } + } + } + assert_eq!(valid + invalid + unsupported, doc["numberOfTests"].as_u64().unwrap()); + assert!(valid > 0 && invalid > 0); + println!( + "Wycheproof: {valid} valid and {invalid} invalid cases passed; {unsupported} cases excluded by fixed nonce/tag sizes" + ); +} diff --git a/crypto/chacha20-poly1305-aead/tests/common/mod.rs b/crypto/chacha20-poly1305-aead/tests/common/mod.rs new file mode 100644 index 000000000..1a9b229ee --- /dev/null +++ b/crypto/chacha20-poly1305-aead/tests/common/mod.rs @@ -0,0 +1,92 @@ +use bouncycastle_chacha20_poly1305_aead::ChaCha20Poly1305Key; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, FromKeyMaterial}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::test_data_loaders::Value; + +pub fn decode(case: &Value, field: &str) -> Vec { + bouncycastle_hex::decode(case[field].as_str().expect(field)).unwrap() +} + +pub fn key(bytes: &[u8]) -> ChaCha20Poly1305Key { + assert_eq!(bytes.len(), 32); + let mut key = KeyMaterial::<32>::new(); + do_hazardous_operations(&mut key, |key| { + key.set_bytes_as_type(bytes, KeyType::SymmetricCipherKey)?; + key.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + ChaCha20Poly1305Key::from_key_material(&key).unwrap() +} + +#[allow(clippy::too_many_arguments)] +pub fn check_valid( + key: &ChaCha20Poly1305Key, + nonce: &[u8; 12], + aad: &[u8], + msg: &[u8], + ct: &[u8], + tag: &[u8; 16], + chunks: &[usize], +) { + let mut inline = ct.to_vec(); + inline.extend_from_slice(tag); + let mut plaintext = vec![0xa5; msg.len() + 8]; + let n = key.decrypt_detached_out(nonce, aad, ct, tag, &mut plaintext).unwrap(); + assert_eq!(n, msg.len()); + assert_eq!(&plaintext[..n], msg); + assert_eq!(&plaintext[n..], &[0; 8], "bytes past the plaintext are zeroized"); + assert_eq!(key.decrypt_with_aad_out(nonce, aad, &inline, &mut plaintext).unwrap(), msg.len()); + assert_eq!(&plaintext[..msg.len()], msg); + let mut sealed = vec![0xa5; inline.len()]; + assert_eq!(key.encrypt_with_aad_nonce_out(nonce, aad, msg, &mut sealed).unwrap(), sealed.len()); + assert_eq!(sealed, inline); + + for &chunk in chunks { + let mut enc = key.create_encryptor_nonce(nonce); + for bytes in aad.chunks(chunk) { + enc.do_update_aad(bytes).unwrap(); + } + let mut ciphertext = vec![0xa5; msg.len()]; + let mut offset = 0; + enc.do_encrypt_out(&[], &mut []).unwrap(); + for bytes in msg.chunks(chunk) { + offset += enc.do_encrypt_out(bytes, &mut ciphertext[offset..]).unwrap(); + } + let mut last = [0xa5; 16]; + let (n, actual_tag) = enc.do_encrypt_final_detached_out(&mut last).unwrap(); + assert_eq!(n, 0); + assert_eq!(last, [0; 16], "the unused final buffer is zeroized"); + assert_eq!(offset, msg.len()); + assert_eq!(ciphertext, ct); + assert_eq!(&actual_tag, tag); + + for detached in [false, true] { + let mut dec = key.create_decryptor(nonce); + for bytes in aad.chunks(chunk) { + dec.do_update_aad(bytes).unwrap(); + } + let mut recovered = vec![0u8; msg.len()]; + let mut written = 0; + let input = if detached { ct } else { &inline }; + for bytes in input.chunks(chunk) { + let expected = dec.do_decrypt_out_len(bytes.len()); + let n = dec.do_decrypt_out(bytes, &mut recovered[written..]).unwrap(); + assert_eq!(n, expected); + written += n; + } + let (last, n) = if detached { + dec.do_decrypt_final_detached(tag).unwrap() + } else { + dec.do_decrypt_final().unwrap() + }; + recovered[written..written + n].copy_from_slice(&last[..n]); + assert_eq!(written + n, msg.len()); + assert_eq!(recovered, msg); + } + } +} diff --git a/crypto/chacha20/Cargo.toml b/crypto/chacha20/Cargo.toml new file mode 100644 index 000000000..91810d29b --- /dev/null +++ b/crypto/chacha20/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bouncycastle-chacha20" +version.workspace = true +edition.workspace = true + +[features] +default = ["std"] +std = ["bouncycastle-core/std"] + +[dependencies] +bouncycastle-cipher.workspace = true +bouncycastle-core.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 + +[[bench]] +name = "chacha20_benches" +harness = false diff --git a/crypto/chacha20/benches/chacha20_benches.rs b/crypto/chacha20/benches/chacha20_benches.rs new file mode 100644 index 000000000..8e59b16a3 --- /dev/null +++ b/crypto/chacha20/benches/chacha20_benches.rs @@ -0,0 +1,43 @@ +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_chacha20::ChaCha20Key; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +use bouncycastle_core::traits::{RNG, StreamCipherDecryptor}; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_chacha20(c: &mut Criterion) { + let material = + KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::SymmetricCipherKey).unwrap(); + let key = ChaCha20Key::from_key_material(&material).unwrap(); + let nonce = [0x24u8; 12]; + let data = random_data(DATA_LEN); + let mut buf = data.clone(); + + let mut group = c.benchmark_group("chacha20::ChaCha20"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // The decryptor takes the nonce, so each iteration starts a fresh keystream without drawing a + // nonce from the OS; the XOR is the same in both directions. + group.bench_function(format!("{DATA_LEN} bytes -- .do_decrypt_inplace()"), |b| { + b.iter(|| { + let mut cipher = key.create_decryptor(&nonce); + cipher.do_decrypt_inplace(black_box(&mut buf)).unwrap(); + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_chacha20); +criterion_main!(benches); diff --git a/crypto/chacha20/src/hazmat.rs b/crypto/chacha20/src/hazmat.rs new file mode 100644 index 000000000..f01daa779 --- /dev/null +++ b/crypto/chacha20/src/hazmat.rs @@ -0,0 +1,216 @@ +//! Raw primitives whose safe use is the caller's responsibility; see [`bouncycastle_core::hazmat`] +//! for what the path means and the supported uses. +//! +//! [`ChaCha20KeyState`] is the key, validated once, and [`ChaCha20KeyStream`] the keystream +//! positioned over it: the parts under [`ChaCha20Key`] and the +//! ChaCha20-Poly1305 key. Constructed directly, the keystream takes the nonce and initial block +//! counter from the caller; a key's `create_encryptor` draws the nonce, starts at counter 0 and +//! refuses to run past the counter, and is the cipher to use. + +use crate::{BLOCK_LEN, ChaCha20Key, KEY_LEN, NONCE_LEN}; +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::KeyStream; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{Cursor, CursorMut, SuspendableComponent}; + +/// One counter value per keystream block: RFC 8439 Sec 2.3, "Word 12 is a block counter". +const BLOCK_LIMIT: u64 = 1 << 32; + +/// The 256-bit ChaCha20 key of RFC 8439 Sec 2.3 as the eight words of the cipher state it fills, +/// validated once: the keyed primitive under [`ChaCha20Key`] and the +/// ChaCha20-Poly1305 key, as an expanded schedule is under an AES key. The words live in a +/// [`Secret`], zeroized on drop, and a [`ChaCha20KeyStream`] borrows them rather than copying them. +/// +/// # 🚨 Security Considerations 🚨 +/// The material of a ChaCha20-Poly1305 key makes the same state, and a keystream over it under +/// that key's nonces is the AEAD's own keystream; see [`ChaCha20KeyStream`]. +#[derive(Clone)] +pub struct ChaCha20KeyState { + key: Secret<[u32; 8]>, +} + +impl ChaCha20KeyState { + /// The key words of RFC 8439 Sec 2.3, "eight 32-bit little-endian integers". + /// + /// # Errors + /// * [`KeyMaterialError::InvalidLength`] if the key is not 32 bytes long. + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 256 bits. + pub fn new(key: &KeyMaterial) -> Result { + if key.key_len() != KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "ChaCha20 requires a symmetric cipher key", + ) + .into()); + } + if key.security_strength() < ChaCha20KeyStream::MAX_SECURITY_STRENGTH { + return Err( + KeyMaterialError::SecurityStrength("ChaCha20 requires a 256-bit key").into() + ); + } + let mut words: Secret<[u32; 8]> = Secret::new(); + for (word, bytes) in words.iter_mut().zip(key.ref_to_bytes().chunks_exact(4)) { + *word = u32::from_le_bytes(bytes.try_into().unwrap()); + } + Ok(Self { key: words }) + } +} + +/// The IETF ChaCha20 keystream of RFC 8439 Sec 2.4: the block function of Sec 2.3 run over +/// successive values of the 32-bit block counter, with a fixed key and 96-bit nonce. Use it +/// through [`ChaCha20`](crate::ChaCha20). +/// +/// # 🚨 Security Considerations 🚨 +/// A raw [`KeyStream`]: constructed directly, it takes the nonce from the caller, and running it +/// past the counter panics rather than returning an error. In particular, never use it with the +/// key and nonce of a ChaCha20-Poly1305 message: block 0 of that pair is the message's one-time +/// Poly1305 key (RFC 8439 Sec 2.6). See [`KeyStream`]'s security notes and +/// [`bouncycastle_core::hazmat`] for the supported uses. +/// +/// # State +/// +/// A reference to the [`ChaCha20KeyState`], the nonce and the next block counter. The nonce and +/// the counter are both public, so they are plain fields; no keystream is kept between calls. +#[derive(Clone)] +pub struct ChaCha20KeyStream<'k> { + key: &'k ChaCha20KeyState, + /// RFC 8439 Sec 2.3's words 13-15: the nonce, as little-endian words. + nonce: [u32; 3], + /// The counter of the *next* block. A `u64` so that the exhausted state, `2^32`, is + /// representable without wrapping into the 32-bit word 12. + next_counter: u64, +} + +impl<'k> ChaCha20KeyStream<'k> { + /// The keystream for `nonce` under `key`, whose first block uses the counter + /// `initial_counter`: RFC 8439 Sec 2.4's "32-bit initial counter. This can be set to any + /// number, but will usually be zero or one." + pub fn start_at( + key: &'k ChaCha20KeyState, + nonce: &[u8; NONCE_LEN], + initial_counter: u32, + ) -> Self { + Self::at(key, nonce, u64::from(initial_counter)) + } + + fn at(key: &'k ChaCha20KeyState, nonce: &[u8; NONCE_LEN], next_counter: u64) -> Self { + let mut words = [0u32; 3]; + for (word, bytes) in words.iter_mut().zip(nonce.chunks_exact(4)) { + *word = u32::from_le_bytes(bytes.try_into().unwrap()); + } + Self { key, nonce: words, next_counter } + } +} + +impl Algorithm for ChaCha20KeyStream<'_> { + const ALG_NAME: &'static str = "ChaCha20"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +impl KeyStream for ChaCha20KeyStream<'_> { + /// One block for every counter value left below `2^32`. + fn remaining_blocks(&self) -> u64 { + BLOCK_LIMIT.saturating_sub(self.next_counter) + } + + /// RFC 8439 Sec 2.4.1, `block ^ key_stream` for the next `blocks.len()` counter values. + fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]) { + // `StreamCipher` calls this with no blocks whenever a call ends inside the pending one, and + // then there is no state to build. + if blocks.is_empty() { + return; + } + // RFC 8439 Sec 2.3: constants, key, block counter, nonce, with word 12 rewritten for each + // block. Built here for this call, so that the key's words stay in the key; a `Secret`, + // since it holds them. + let mut state: Secret<[u32; 16]> = Secret::new(); + state[..4].copy_from_slice(&[0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]); + state[4..12].copy_from_slice(&*self.key.key); + state[13..].copy_from_slice(&self.nonce); + // Live keystream until it has been XORed in, so it is a `Secret`, one per call. + let mut working: Secret<[u32; 16]> = Secret::new(); + for block in blocks.iter_mut() { + // In release builds too: past the last counter, word 12 would wrap to 0 and repeat the + // keystream, which the trait lets an implementor refuse by panicking. + assert!(self.next_counter < BLOCK_LIMIT, "the caller checks remaining_blocks"); + state[12] = self.next_counter as u32; + block_function(&state, &mut working); + // Sec 2.3 serialises each word little-endian and Sec 2.4 XORs bytewise; XORing the + // little-endian word of plaintext with the keystream word is the same operation. + let (words, _) = block.as_chunks_mut::<4>(); + for (bytes, k) in words.iter_mut().zip(working.iter()) { + *bytes = (u32::from_le_bytes(*bytes) ^ k).to_le_bytes(); + } + self.next_counter += 1; + } + } +} + +/// RFC 8439 Sec 2.3: `out` = 20 rounds over `state`, then the original input words added back, +/// modulo 2^32. Serialisation is left to the caller. +// Kept out of line for speed, not correctness: inlined into `apply_blocks`, whose input state is a +// local, it ran about 1.45x slower (16 KiB, x86-64, measured 2026-10-21); out of line it matches +// the keystream that kept its state in a field. +#[inline(never)] +fn block_function(state: &[u32; 16], out: &mut [u32; 16]) { + out.copy_from_slice(state); + for _ in 0..10 { + quarter_round(out, 0, 4, 8, 12); + quarter_round(out, 1, 5, 9, 13); + quarter_round(out, 2, 6, 10, 14); + quarter_round(out, 3, 7, 11, 15); + quarter_round(out, 0, 5, 10, 15); + quarter_round(out, 1, 6, 11, 12); + quarter_round(out, 2, 7, 8, 13); + quarter_round(out, 3, 4, 9, 14); + } + for (word, input) in out.iter_mut().zip(state.iter()) { + *word = word.wrapping_add(*input); + } +} + +fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { + state[a] = state[a].wrapping_add(state[b]); + state[d] = (state[d] ^ state[a]).rotate_left(16); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_left(12); + state[a] = state[a].wrapping_add(state[b]); + state[d] = (state[d] ^ state[a]).rotate_left(8); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_left(7); +} + +/// The suspended state is the nonce and the next block counter; the key words are the +/// re-supplied key's. See [`bouncycastle_utils::suspendable_state`]. +impl<'k> SuspendableComponent<'k> for ChaCha20KeyStream<'k> { + const STATE_LEN: usize = NONCE_LEN + 8; + type Key = ChaCha20Key; + + fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + for word in &self.nonce { + w.bytes(&word.to_le_bytes()); + } + w.u64(self.next_counter); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let mut r = Cursor::new(state); + let nonce = r.array::(); + // The counter counts to `BLOCK_LIMIT` and stops there (the exhausted state, with + // `remaining_blocks() == 0`); anything past it is not a state this type produces. + let next_counter = r.u64(); + debug_assert!(r.is_done()); + if next_counter > BLOCK_LIMIT { + return Err(SuspendableError::InvalidData); + } + Ok(Self::at(&key.state, &nonce, next_counter)) + } +} diff --git a/crypto/chacha20/src/lib.rs b/crypto/chacha20/src/lib.rs new file mode 100644 index 000000000..5148dbf69 --- /dev/null +++ b/crypto/chacha20/src/lib.rs @@ -0,0 +1,140 @@ +//! IETF ChaCha20, as specified in [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439). +//! +//! [`ChaCha20Key`] is the key object: built once from 32 bytes of key material through +//! [`FromKeyMaterial`], it hands out [`ChaCha20`] stream ciphers through +//! [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`], drawing the nonce from the RNG it is +//! given and starting the block counter at zero. The raw keystream, with a caller-chosen nonce +//! and counter for protocols and test vectors, is [`hazmat::ChaCha20KeyStream`]. Only the RFC's +//! 32-byte key and 12-byte nonce variant is supported. +//! +//! # Usage Examples +//! +//! One-shot, in place: +//! +//! ``` +//! use bouncycastle_chacha20::ChaCha20Key; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +//! let key = ChaCha20Key::from_key_material(&material).unwrap(); +//! let mut message = *b"message"; +//! let (_, nonce) = key.encrypt_inplace(&mut HashDRBG_SHA512::new_from_os(), &mut message).unwrap(); +//! key.decrypt_inplace(&nonce, &mut message).unwrap(); +//! assert_eq!(&message, b"message"); +//! ``` +//! +//! Streaming, in pieces of any length: +//! +//! ``` +//! use bouncycastle_chacha20::ChaCha20Key; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! let material = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +//! let key = ChaCha20Key::from_key_material(&material).unwrap(); +//! let mut data = [0x5au8; 100]; +//! let (mut enc, nonce) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); +//! for piece in data.chunks_mut(7) { +//! enc.do_encrypt_inplace(piece).unwrap(); +//! } +//! let mut dec = key.create_decryptor(&nonce); +//! dec.do_decrypt_inplace(&mut data).unwrap(); +//! assert_eq!(data, [0x5au8; 100]); +//! ``` +//! +//! # Memory Usage +//! +//! | Type | In-memory size (bytes) | Suspended state size (bytes) | +//! |------|------------------------|------------------------------| +//! | [`ChaCha20Key`] | 32 | -- | +//! | [`ChaCha20`] | 104 | `ChaCha20::SUSPENDED_STATE_LEN` (95) | +//! | [`hazmat::ChaCha20KeyStream`] | 32 | -- | +//! +//! "In-memory size" is `core::mem::size_of` on a 64-bit target. There are no heap allocations and +//! no message-sized buffers, so this crate has no `mem_usage_benches` harness. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! - Never reuse a nonce under the same key, or overlap counter ranges for that key and nonce +//! through [`hazmat::ChaCha20KeyStream::start_at`]: the keystream repeats and the XOR of the two +//! plaintexts is revealed. +//! - ChaCha20 provides no authentication: flipping a ciphertext bit flips the same plaintext bit. +//! Use ChaCha20-Poly1305 unless the ciphertext is authenticated by other means. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod hazmat; + +use bouncycastle_cipher::stream::StreamCipher; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::RNG; +use hazmat::{ChaCha20KeyState, ChaCha20KeyStream}; + +/// Key length in bytes. +pub const KEY_LEN: usize = 32; +/// Nonce length in bytes. +pub const NONCE_LEN: usize = 12; +/// Keystream block length in bytes. +pub const BLOCK_LEN: usize = 64; + +/// ChaCha20 with a 96-bit nonce and initial block counter zero, as a [`ChaCha20Key`] makes it, +/// borrowing the key for `'k`. `Dir` is [`Encrypting`] or [`Decrypting`]. +/// +/// A message may be at most `2^32` blocks of 64 bytes; a call that would run past that returns +/// [`SymmetricCipherError::DataLimitExceeded`] and consumes nothing. +pub type ChaCha20<'k, Dir> = StreamCipher, Dir, NONCE_LEN, BLOCK_LEN>; + +/// A ChaCha20 key: the 256-bit key of RFC 8439 Sec 2.3, validated once, from which the stream +/// cipher for each message is made. Built through [`FromKeyMaterial`] from material tagged +/// [`KeyType::SymmetricCipherKey`] of 256-bit strength. The key words live in a `Secret`, +/// zeroized on drop; there is no `Debug` and no way to read them back. +#[derive(Clone)] +pub struct ChaCha20Key { + state: ChaCha20KeyState, +} + +impl FromKeyMaterial for ChaCha20Key { + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + /// # Errors + /// As [`ChaCha20KeyState::new`]. + fn from_key_material(key: &KeyMaterial) -> Result { + Ok(Self { state: ChaCha20KeyState::new(key)? }) + } +} + +impl SymmetricEncryptionKey for ChaCha20Key { + type Encryptor<'k> + = ChaCha20<'k, Encrypting> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok((ChaCha20::from_keystream(ChaCha20KeyStream::start_at(&self.state, &nonce, 0)), nonce)) + } +} + +impl SymmetricDecryptionKey for ChaCha20Key { + type Decryptor<'k> + = ChaCha20<'k, Decrypting> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + ChaCha20::from_keystream(ChaCha20KeyStream::start_at(&self.state, nonce, 0)) + } +} diff --git a/crypto/chacha20/tests/chacha20_bc-test-data.rs b/crypto/chacha20/tests/chacha20_bc-test-data.rs new file mode 100644 index 000000000..ab96070c0 --- /dev/null +++ b/crypto/chacha20/tests/chacha20_bc-test-data.rs @@ -0,0 +1,48 @@ +//! RFC vectors live in the sibling bc-test-data repository, never in the crate package. +use bouncycastle_chacha20::ChaCha20; +use bouncycastle_chacha20::hazmat::{ChaCha20KeyState, ChaCha20KeyStream}; +use bouncycastle_cipher::Encrypting; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::StreamCipherEncryptor; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data_json; + +#[test] +fn rfc8439_blocks_encryption_and_one_time_keys() { + let Some(document) = bc_test_data_json("crypto/rfc8439", "chacha20.json") else { return }; + let cases = document["tests"].as_array().unwrap(); + assert_eq!(cases.len(), 12); + for case in cases { + let decode = |field: &str| bouncycastle_hex::decode(case[field].as_str().unwrap()).unwrap(); + let id = case["id"].as_str().unwrap(); + let mut key = KeyMaterial::<32>::new(); + do_hazardous_operations(&mut key, |key| { + key.set_bytes_as_type(&decode("key"), KeyType::SymmetricCipherKey)?; + key.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + let state = ChaCha20KeyState::new(&key).unwrap(); + let nonce: [u8; 12] = decode("nonce").try_into().unwrap(); + let counter = u32::try_from(case["counter"].as_u64().unwrap()).unwrap(); + let message = decode("msg"); + let expected = decode("output"); + for chunk in [1, 3, 16, 63, 64, 65, 127, 512] { + let mut actual = message.clone(); + let mut cipher = at(&state, &nonce, counter); + for bytes in actual.chunks_mut(chunk) { + cipher.do_encrypt_inplace(bytes).unwrap(); + cipher.do_encrypt_inplace(&mut []).unwrap(); + } + assert_eq!(actual, expected, "RFC {id}, chunk {chunk}"); + at(&state, &nonce, counter).do_encrypt_inplace(&mut actual).unwrap(); + assert_eq!(actual, message, "RFC {id} decryption"); + } + } + println!("RFC 8439 ChaCha20: {} vectors passed", cases.len()); +} + +/// The RFC's vectors start at counters other than 0, which only the hazmat keystream takes. +fn at<'k>(state: &'k ChaCha20KeyState, nonce: &[u8; 12], counter: u32) -> ChaCha20<'k, Encrypting> { + ChaCha20::from_keystream(ChaCha20KeyStream::start_at(state, nonce, counter)) +} diff --git a/crypto/chacha20/tests/chacha20_tests.rs b/crypto/chacha20/tests/chacha20_tests.rs new file mode 100644 index 000000000..9672b9b4a --- /dev/null +++ b/crypto/chacha20/tests/chacha20_tests.rs @@ -0,0 +1,245 @@ +use bouncycastle_chacha20::hazmat::{ChaCha20KeyState, ChaCha20KeyStream}; +use bouncycastle_chacha20::{ChaCha20, ChaCha20Key}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::KeyStream; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{ + Algorithm, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, +}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; +use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_rng::HashDRBG_SHA512; + +fn material() -> KeyMaterial<32> { + KeyMaterial::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap() +} + +fn key() -> ChaCha20Key { + ChaCha20Key::from_key_material(&material()).unwrap() +} + +fn state() -> ChaCha20KeyState { + ChaCha20KeyState::new(&material()).unwrap() +} + +fn at<'k>(state: &'k ChaCha20KeyState, nonce: &[u8; 12], counter: u32) -> ChaCha20<'k, Encrypting> { + ChaCha20::from_keystream(ChaCha20KeyStream::start_at(state, nonce, counter)) +} + +#[test] +fn core_stream_and_symmetric_contracts() { + let key = key(); + let mut rng = HashDRBG_SHA512::new_from_os(); + TestFrameworkStreamCipher::new().test_key::<32, 12, ChaCha20Key>(&key, &mut rng); + TestFrameworkSymmetricCipher::new().test_key::<32, 12, 0, ChaCha20Key>(&key, &mut rng); + let state = state(); + TestFrameworkKeyStream::new() + .test::<12, 64, _>(|nonce| ChaCha20KeyStream::start_at(&state, nonce, 0)); +} + +/// The key's construction suite: the type and strength policy of `from_key_material`. +#[test] +fn the_key_enforces_the_material_policy() { + TestFrameworkFromKeyMaterial::new() + .test::<32, ChaCha20Key>(ChaCha20KeyStream::ALG_NAME, SecurityStrength::_256bit); +} + +#[test] +fn counter_exhaustion_is_atomic_and_retains_partial_blocks() { + let state = state(); + for used in [0, 1, 31, 63, 64] { + let nonce = [7; 12]; + let mut reference = [0x5a; 64]; + at(&state, &nonce, u32::MAX).do_encrypt_inplace(&mut reference).unwrap(); + let mut cipher = at(&state, &nonce, u32::MAX); + let mut actual = [0x5a; 64]; + cipher.do_encrypt_inplace(&mut actual[..used]).unwrap(); + + let mut too_long = vec![0xa5; 65 - used]; + assert!(matches!( + cipher.do_encrypt_inplace(&mut too_long), + Err(SymmetricCipherError::DataLimitExceeded) + )); + assert!(too_long.iter().all(|&b| b == 0xa5)); + cipher.do_encrypt_inplace(&mut actual[used..]).unwrap(); + assert_eq!(actual, reference); + assert_eq!(cipher.keystream().remaining_blocks(), 0); + assert_eq!(cipher.do_encrypt_inplace(&mut []).unwrap(), 0); + assert!(matches!( + cipher.do_encrypt_inplace(&mut [0]), + Err(SymmetricCipherError::DataLimitExceeded) + )); + } +} + +#[test] +fn last_two_blocks_work_in_arbitrary_chunks() { + let state = state(); + let nonce = [9; 12]; + let mut expected = [0u8; 128]; + at(&state, &nonce, u32::MAX - 1).do_encrypt_inplace(&mut expected).unwrap(); + for chunk in [1, 3, 63, 64, 65, 127] { + let mut cipher = at(&state, &nonce, u32::MAX - 1); + let mut out = [0u8; 128]; + for bytes in out.chunks_mut(chunk) { + cipher.do_encrypt_inplace(bytes).unwrap(); + } + assert_eq!(out, expected); + assert_eq!(cipher.keystream().remaining_blocks(), 0); + } + assert_eq!(key().create_decryptor(&nonce).keystream().remaining_blocks(), 1u64 << 32); + assert_eq!(ChaCha20KeyStream::start_at(&state, &nonce, 1).remaining_blocks(), u32::MAX as u64); +} + +/// The raw keystream run to its last counter value has nothing left, and a stream cipher made +/// over it refuses more data rather than carrying on. +#[test] +fn a_keystream_at_its_last_counter_has_no_blocks_left() { + let state = state(); + let mut ks = ChaCha20KeyStream::start_at(&state, &[7; 12], u32::MAX); + ks.apply_blocks(&mut [[0u8; 64]; 1]); + assert_eq!(ks.remaining_blocks(), 0); + assert!(matches!( + ChaCha20::::from_keystream(ks).do_encrypt_inplace(&mut [0u8; 1]), + Err(SymmetricCipherError::DataLimitExceeded) + )); +} + +/// Past the last counter value the raw keystream panics, as the trait allows, rather than +/// wrapping word 12 to 0 and repeating the keystream; in release builds too. +#[test] +#[should_panic(expected = "the caller checks remaining_blocks")] +fn applying_blocks_past_the_last_counter_panics() { + let state = state(); + let mut ks = ChaCha20KeyStream::start_at(&state, &[7; 12], u32::MAX); + ks.apply_blocks(&mut [[0u8; 64]; 2]); +} + +/// The key's workers start at block counter 0: two blocks in, they match a keystream started at +/// counter 2. +#[test] +fn start_at_matches_the_key_after_discarding_blocks() { + let nonce = [3; 12]; + let key = key(); + let state = state(); + let mut from_key = key.create_decryptor(&nonce); + from_key.do_decrypt_inplace(&mut [0u8; 128]).unwrap(); + let mut a = [0x42u8; 100]; + let mut b = a; + from_key.do_decrypt_inplace(&mut a).unwrap(); + at(&state, &nonce, 2).do_encrypt_inplace(&mut b).unwrap(); + assert_eq!(a, b); +} + +#[test] +fn key_capacity_does_not_replace_key_length() { + for len in 0..32 { + let mut short = material(); + short.set_key_len(len).unwrap(); + assert!(matches!( + ChaCha20KeyState::new(&short), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength)) + )); + assert!(matches!( + ChaCha20Key::from_key_material(&short), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength)) + )); + } +} + +/// Part of a message, then suspend a clone and resume it over the key it was made from: both +/// must finish identically, mid-block and at a block boundary, in both directions. +#[test] +fn suspend_and_resume_continues_the_keystream() { + const N: usize = ChaCha20::<'static, Encrypting>::SUSPENDED_STATE_LEN; + let key = key(); + let mut rng = HashDRBG_SHA512::new_from_os(); + for first in [0, 1, 63, 64, 65] { + let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); + let mut ciphertext = vec![0x5au8; 200]; + enc.do_encrypt_inplace(&mut ciphertext[..first]).unwrap(); + TestFrameworkSuspendableKeyedState::new().test::(&enc, &key); + let state: [u8; N] = enc.clone().suspend(); + let mut resumed = ChaCha20::::from_suspended(state, &key).unwrap(); + let mut rest = ciphertext[first..].to_vec(); + enc.do_encrypt_inplace(&mut ciphertext[first..]).unwrap(); + resumed.do_encrypt_inplace(&mut rest).unwrap(); + assert_eq!(rest, ciphertext[first..]); + + let mut dec = key.create_decryptor(&nonce); + dec.do_decrypt_inplace(&mut ciphertext[..first]).unwrap(); + const M: usize = ChaCha20::<'static, Decrypting>::SUSPENDED_STATE_LEN; + TestFrameworkSuspendableKeyedState::new().test::(&dec, &key); + let state: [u8; M] = dec.suspend(); + let mut resumed = ChaCha20::::from_suspended(state, &key).unwrap(); + resumed.do_decrypt_inplace(&mut ciphertext[first..]).unwrap(); + assert_eq!(ciphertext, vec![0x5au8; 200]); + } +} + +/// The suspended layout is frozen. After the version header: the nonce, the next block counter +/// (`u64`, little-endian), the pending keystream block, and how much of it is used (`u64`, +/// little-endian). Here 70 bytes in, block 0 is spent, block 1 is pending with 6 bytes used, and +/// the next counter is 2. +#[test] +fn the_suspended_layout_is_frozen() { + const N: usize = ChaCha20::<'static, Decrypting>::SUSPENDED_STATE_LEN; + let key = key(); + let nonce = [0x24; 12]; + let mut keystream = [0u8; 192]; + key.create_decryptor(&nonce).do_decrypt_inplace(&mut keystream).unwrap(); + + let mut dec = key.create_decryptor(&nonce); + dec.do_decrypt_inplace(&mut [0u8; 70]).unwrap(); + let state: [u8; N] = dec.suspend(); + let mut expected = nonce.to_vec(); + expected.extend_from_slice(&2u64.to_le_bytes()); + expected.extend_from_slice(&keystream[64..128]); + expected.extend_from_slice(&6u64.to_le_bytes()); + assert_eq!(state[3..], expected[..]); + + let mut resumed = ChaCha20::::from_suspended(state, &key).unwrap(); + let mut rest = [0u8; 122]; + resumed.do_decrypt_inplace(&mut rest).unwrap(); + assert_eq!(rest, keystream[70..]); +} + +/// A resumed state carrying a counter past `2^32` is not one this type writes, and a resumed +/// exhausted keystream stays exhausted. +#[test] +fn resumed_counter_is_bounded() { + const N: usize = ChaCha20::<'static, Encrypting>::SUSPENDED_STATE_LEN; + let key = key(); + let key_state = state(); + let mut cipher = at(&key_state, &[1; 12], u32::MAX); + cipher.do_encrypt_inplace(&mut [0u8; 64]).unwrap(); + let state: [u8; N] = cipher.suspend(); + let mut resumed = ChaCha20::::from_suspended(state, &key).unwrap(); + assert!(resumed.do_encrypt_inplace(&mut [0]).is_err()); + + // Layout: version header (3), nonce (12), next counter (8, little-endian), pending block, used. + let mut past = state; + past[3 + 12..3 + 20].copy_from_slice(&((1u64 << 32) + 1).to_le_bytes()); + assert!(ChaCha20::::from_suspended(past, &key).is_err()); +} + +/// The figures in the crate docs' Memory Usage table. +#[cfg(target_pointer_width = "64")] +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + assert_eq!(size_of::(), 32); + assert_eq!(size_of::>(), 32); + assert_eq!(size_of::>(), 104); + assert_eq!(size_of::>(), 104); + assert_eq!(ChaCha20::<'static, Encrypting>::SUSPENDED_STATE_LEN, 95); +} diff --git a/crypto/core-test-framework/src/mac.rs b/crypto/core-test-framework/src/mac.rs index 7067bf9e2..fe8c0fe94 100644 --- a/crypto/core-test-framework/src/mac.rs +++ b/crypto/core-test-framework/src/mac.rs @@ -1,5 +1,7 @@ -//! Generic behaviour tests for any MAC key object, through the [`MAC`] workers it hands out. +//! Generic behaviour tests for any MAC key object, through the [`MAC`] workers it hands out, and +//! for the worker of a one-time MAC, which has no reusable key object. +use bouncycastle_core::errors::MACError; use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; @@ -84,4 +86,57 @@ impl TestFrameworkMAC { assert_eq!(tag_b, key.mac(b"another message")); assert_eq!(key.mac(input), expected_output); } + + /// The [`MAC`] worker of a one-time MAC such as Poly1305, against the given input-output pair. + /// Its key authenticates one message and is consumed by the worker it makes, so there is no + /// key object to test: `new_worker` makes a fresh worker under the same key on every call. Its + /// tag cannot be truncated: an output buffer shorter than the tag must be refused with + /// [`MACError::InvalidLength`] and left zeroed. + pub fn test_one_time_mac( + &self, + new_worker: impl Fn() -> W, + input: &[u8], + expected_output: &[u8], + ) { + // the worker's consuming one-shots + assert_eq!(new_worker().mac(input), expected_output); + let mut out = vec![0u8; expected_output.len()]; + assert_eq!(new_worker().mac_out(input, &mut out).unwrap(), expected_output.len()); + assert_eq!(out, expected_output); + // an output buffer that's too small is refused and left zeroed + let mut out = vec![0xA5u8; expected_output.len() - 2]; + let result = new_worker().mac_out(input, &mut out); + assert!(matches!(result, Err(MACError::InvalidLength(_)))); + assert_eq!(out, vec![0u8; expected_output.len() - 2]); + // an output buffer that's too big gets the first L bytes filled and the rest zeroed + let mut out = vec![0xA5u8; 2 * expected_output.len()]; + assert_eq!(new_worker().mac_out(input, &mut out).unwrap(), expected_output.len()); + assert_eq!(&out[..expected_output.len()], expected_output); + assert_eq!(&out[expected_output.len()..], vec![0u8; expected_output.len()]); + assert!(new_worker().verify(input, expected_output)); + assert!(!new_worker().verify(input, &expected_output[..expected_output.len() - 1])); + assert!(!new_worker().verify(b"not the input", expected_output)); + + // the streaming methods + let mut worker = new_worker(); + let output_len = worker.output_len(); + worker.do_update(input); + let out = worker.do_final(); + assert_eq!(out, expected_output); + assert_eq!(output_len, out.len()); + + let mut worker = new_worker(); + worker.do_update(input); + let mut out = vec![0u8; worker.output_len()]; + let out_len = worker.do_final_out(&mut out).unwrap(); + assert_eq!(out, expected_output); + assert_eq!(out_len, out.len()); + + let mut worker = new_worker(); + worker.do_update(input); + assert!(worker.do_verify_final(expected_output)); + let mut worker = new_worker(); + worker.do_update(input); + assert!(!worker.do_verify_final(&expected_output[1..])); + } } diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index 8e3bf0775..87a961b2b 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -3,6 +3,7 @@ mod hash_factory_tests { use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; use bouncycastle_core::traits::MAC; + use bouncycastle_factory::FactoryError; use bouncycastle_factory::mac_factory::MACFactory; use bouncycastle_hex as hex; @@ -175,5 +176,17 @@ mod hash_factory_tests { ); } } + + /// A factory key authenticates any number of messages, which a Poly1305 key must never + /// do, so the name is refused even with a key every other MAC here accepts. + #[test] + fn poly1305_is_not_offered() { + let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::MACKey).unwrap(); + assert!(MACFactory::new(bouncycastle_sm3::hmac::HMAC_SM3_NAME, &key).is_ok()); + assert!(matches!( + MACFactory::new("Poly1305", &key), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); + } } } diff --git a/crypto/poly1305/Cargo.toml b/crypto/poly1305/Cargo.toml new file mode 100644 index 000000000..634e0c740 --- /dev/null +++ b/crypto/poly1305/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bouncycastle-poly1305" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.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 + +[[bench]] +name = "poly1305_benches" +harness = false diff --git a/crypto/poly1305/benches/poly1305_benches.rs b/crypto/poly1305/benches/poly1305_benches.rs new file mode 100644 index 000000000..1909c2793 --- /dev/null +++ b/crypto/poly1305/benches/poly1305_benches.rs @@ -0,0 +1,41 @@ +use bouncycastle_rng as rng; +use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_core::traits::{MAC, RNG}; +use bouncycastle_poly1305::hazmat::key_from_bytes; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_poly1305(c: &mut Criterion) { + // A fixed key is fine for timing; a real one-time key must never be reused. + let key = [0x42u8; 32]; + let data = random_data(DATA_LEN); + let mut tag = [0u8; 16]; + + let mut group = c.benchmark_group("poly1305::Poly1305"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function(format!("{DATA_LEN} bytes -- ::mac_out()"), |b| { + // A key is consumed by its one message, so each iteration is given its own. + b.iter_batched( + || key_from_bytes(&key).into_mac(), + |mac| { + mac.mac_out(black_box(&data), &mut tag).unwrap(); + black_box(&tag); + }, + BatchSize::SmallInput, + ) + }); + + group.finish(); +} + +criterion_group!(benches, bench_poly1305); +criterion_main!(benches); diff --git a/crypto/poly1305/src/hazmat.rs b/crypto/poly1305/src/hazmat.rs new file mode 100644 index 000000000..48cb72c20 --- /dev/null +++ b/crypto/poly1305/src/hazmat.rs @@ -0,0 +1,20 @@ +//! Raw primitives whose safe use is the caller's responsibility; see [`bouncycastle_core::hazmat`] +//! for what the path means and the supported uses. +//! +//! [`key_from_bytes`] is the only way to make a [`Poly1305Key`], because Poly1305's security rests +//! on something only the caller can know: that the key is fresh, unpredictable and used for one +//! message. It is for a construction that derives its own one-time key, as ChaCha20-Poly1305 does +//! (RFC 8439 Sec 2.6), and for test vectors. + +use crate::{KEY_LEN, Poly1305Key}; + +/// A [`Poly1305Key`] from the 32-byte one-time key `r || s` of RFC 8439 Sec 2.5, with no check of +/// its value: an all-zero key is accepted, as the RFC accepts it. +/// +/// # 🚨 Security Considerations 🚨 +/// The caller vouches for the key: it must be unpredictable, and authenticate one message only. +/// The key returned is consumed by the one worker it makes, but nothing stops a second key being +/// made from the same bytes. RFC 8439 Sec 2.6 derives a fresh key per message from ChaCha20. +pub fn key_from_bytes(key: &[u8; KEY_LEN]) -> Poly1305Key { + Poly1305Key::from_key_bytes(key) +} diff --git a/crypto/poly1305/src/lib.rs b/crypto/poly1305/src/lib.rs new file mode 100644 index 000000000..a6e4dfa95 --- /dev/null +++ b/crypto/poly1305/src/lib.rs @@ -0,0 +1,394 @@ +//! Poly1305 one-time authentication from [RFC 8439, section 2.5](https://www.rfc-editor.org/rfc/rfc8439#section-2.5). +//! +//! A [`Poly1305Key`] authenticates exactly one message: [`Poly1305Key::into_mac`] consumes it into +//! the one [`Poly1305`] worker it makes, which implements [`MAC`] with full 16-byte tags only: +//! truncation is rejected. Streaming and output-buffer methods do not allocate. Secret state is +//! zeroized on drop. +//! +//! A key is made from raw bytes by [`hazmat::key_from_bytes`], for a construction that derives a +//! fresh key for every message, as ChaCha20-Poly1305 does. For a key that authenticates any +//! number of messages, use a ChaCha20-Poly1305 key (`bouncycastle-chacha20-poly1305-aead`), which +//! derives the Poly1305 key from each nonce. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_poly1305::TAG_LEN; +//! use bouncycastle_poly1305::hazmat::key_from_bytes; +//! // Illustrative key only; a real one-time key is fresh, unpredictable and used once. +//! let one_time_key = [0x42; 32]; +//! let mut mac = key_from_bytes(&one_time_key).into_mac(); +//! mac.do_update(b"message"); +//! let mut tag = [0u8; TAG_LEN]; +//! mac.do_final_out(&mut tag).unwrap(); +//! assert!(key_from_bytes(&one_time_key).into_mac().verify(b"message", &tag)); +//! ``` +//! +//! # Memory Usage +//! +//! | Type | In-memory size (bytes) | +//! |------|------------------------| +//! | [`Poly1305Key`] | 88 | +//! | [`Poly1305`] | 192 | +//! +//! "In-memory size" is `core::mem::size_of` on a 64-bit target. There are no heap allocations in +//! the streaming and `*_out` APIs, so this crate has no `mem_usage_benches` harness. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! - A key must be used for **exactly one message**: tags on two different messages under one +//! key let an attacker forge others. A [`Poly1305Key`] is consumed by the one worker it makes, +//! so it cannot authenticate a second message, but nothing stops a second key being made from +//! the same bytes: that is the caller's to prevent, which is why the constructor is in +//! [`hazmat`]. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +extern crate alloc; + +pub mod hazmat; + +use alloc::vec::Vec; +use bouncycastle_core::errors::{MACError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, MAC}; +use bouncycastle_utils::ct::{self, Condition}; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, SuspendableComponent, bounded_usize, +}; + +/// One-time key length in bytes. +pub const KEY_LEN: usize = 32; +/// Authentication tag length in bytes. +pub const TAG_LEN: usize = 16; +/// Message block length in bytes. +const BLOCK_LEN: usize = 16; + +const MASK: u64 = (1 << 26) - 1; + +/// A full block's 2^128, as bit 24 of the top radix-2^26 limb. +const FULL_BLOCK_BIT: u64 = 1 << 24; + +/// A Poly1305 key: the one-time key `r || s` of RFC 8439 Sec 2.5, clamped and split, made by +/// [`hazmat::key_from_bytes`] and consumed by the one [`Poly1305`] worker it makes. The key words +/// live in [`Secret`]s, zeroized on drop; there is no `Debug` and no way to read them back. +/// +/// A key authenticates one message, so it cannot be used again once consumed, cannot be cloned, +/// and is neither a [`MacGenerationKey`](bouncycastle_core::symmetric_key::MacGenerationKey) nor a +/// [`MacVerificationKey`](bouncycastle_core::symmetric_key::MacVerificationKey), whose keys +/// authenticate any number. None of these compiles: +/// +/// ```compile_fail +/// # use bouncycastle_core::traits::MAC; +/// # use bouncycastle_poly1305::hazmat::key_from_bytes; +/// let key = key_from_bytes(&[0x42; 32]); +/// let first = key.into_mac().mac(b"first message"); +/// let second = key.into_mac().mac(b"second message"); +/// ``` +/// ```compile_fail +/// # use bouncycastle_poly1305::Poly1305Key; +/// fn cloneable() {} +/// cloneable::(); +/// ``` +/// ```compile_fail +/// # use bouncycastle_core::symmetric_key::MacGenerationKey; +/// # use bouncycastle_poly1305::Poly1305Key; +/// fn generation_key() {} +/// generation_key::(); +/// ``` +/// ```compile_fail +/// # use bouncycastle_core::symmetric_key::MacVerificationKey; +/// # use bouncycastle_poly1305::Poly1305Key; +/// fn verification_key() {} +/// verification_key::(); +/// ``` +/// +/// Each compiles without its last line, as does [`Poly1305`]'s: +/// +/// ``` +/// # use bouncycastle_core::symmetric_key::{MacGenerationKey, MacVerificationKey}; +/// # use bouncycastle_core::traits::MAC; +/// # use bouncycastle_poly1305::hazmat::key_from_bytes; +/// # use bouncycastle_poly1305::{Poly1305, Poly1305Key}; +/// let key = key_from_bytes(&[0x42; 32]); +/// let first = key.into_mac().mac(b"first message"); +/// fn cloneable() {} +/// fn generation_key() {} +/// fn verification_key() {} +/// ``` +pub struct Poly1305Key { + /// `r`, clamped, in five radix-2^26 limbs. + r: Secret<[u64; 5]>, + /// `5 * r[1..5]`, the multipliers of the product terms that wrap past 2^130; see + /// `Poly1305::process_block`. Computed once per key rather than per block. + r5: Secret<[u64; 4]>, + /// `s`, the half of the key added at the end, as four little-endian words. + pad: Secret<[u32; 4]>, +} + +impl Poly1305Key { + /// RFC 8439 Sec 2.5's `clamp(r)` and its split of the key into `r` and `s`. + fn from_key_bytes(bytes: &[u8; KEY_LEN]) -> Self { + let mut r: Secret<[u64; 5]> = Secret::new(); + // RFC 8439 clamping, expressed directly in the radix-2^26 representation. + r[0] = load32(&bytes[0..4]) & 0x3ffffff; + r[1] = (load32(&bytes[3..7]) >> 2) & 0x3ffff03; + r[2] = (load32(&bytes[6..10]) >> 4) & 0x3ffc0ff; + r[3] = (load32(&bytes[9..13]) >> 6) & 0x3f03fff; + r[4] = (load32(&bytes[12..16]) >> 8) & 0x00fffff; + let mut pad: Secret<[u32; 4]> = Secret::new(); + for (word, chunk) in pad.iter_mut().zip(bytes[16..].chunks_exact(4)) { + *word = u32::from_le_bytes(chunk.try_into().unwrap()); + } + let mut r5: Secret<[u64; 4]> = Secret::new(); + for (r5, r) in r5.iter_mut().zip(&r[1..]) { + *r5 = 5 * r; + } + Self { r, r5, pad } + } + + /// Consumes the key into the [`MAC`] worker for its one message, which can compute the tag + /// or check one. + pub fn into_mac(self) -> Poly1305 { + Poly1305::from_key(self) + } +} + +/// The worker for a single Poly1305 message, made by [`Poly1305Key::into_mac`], using five +/// radix-2^26 limbs and 64-bit products. It cannot be cloned, since two workers under one key +/// could authenticate two messages: +/// +/// ```compile_fail +/// # use bouncycastle_poly1305::Poly1305; +/// fn cloneable() {} +/// cloneable::(); +/// ``` +/// +/// The implementation has no secret-dependent branches or memory indices. Multiplication +/// must have operand-independent timing on the target processor for constant-time execution. +pub struct Poly1305 { + // The key the worker was made from, moved in: a key makes one worker, so there is nothing to + // borrow it from. + key: Poly1305Key, + h: Secret<[u64; 5]>, + /// `process_block`'s product accumulator: a function of `h` and `r`, so kept in a `Secret` + /// that lives with the instance rather than being re-allocated and zeroized every block. + product: Secret<[u64; 5]>, + buffer: Secret<[u8; BLOCK_LEN]>, + buffered: usize, +} + +impl Algorithm for Poly1305 { + const ALG_NAME: &'static str = "Poly1305"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Poly1305 { + /// A fresh message under `key`. + fn from_key(key: Poly1305Key) -> Self { + Self { key, h: Secret::new(), product: Secret::new(), buffer: Secret::new(), buffered: 0 } + } + + /// RFC 8439 Sec 2.5's `Acc = ((Acc+block)*r) % p` for one 16-byte block, with `high_bit` its + /// "one bit beyond the number of octets": `FULL_BLOCK_BIT` for a full block, or 0 for the + /// final partial one, which carries that bit as a `0x01` byte the caller has written in. + fn process_block(&mut self, block: &[u8; BLOCK_LEN], high_bit: u64) { + self.h[0] += load32(&block[0..4]) & MASK; + self.h[1] += (load32(&block[3..7]) >> 2) & MASK; + self.h[2] += (load32(&block[6..10]) >> 4) & MASK; + self.h[3] += (load32(&block[9..13]) >> 6) & MASK; + self.h[4] += (load32(&block[12..16]) >> 8) | high_bit; + + // 2^130 = 5 (mod 2^130 - 5). Each sum is below 2^58, so u64 suffices. + let product = &mut *self.product; + *product = [0; 5]; + for i in 0..5 { + for j in 0..5 { + let index = i + j; + if index < 5 { + product[index] += self.h[i] * self.key.r[j]; + } else { + // index >= 5 implies j >= 1. The same value as `5 * h[i] * r[j]`. + product[index - 5] += self.h[i] * self.key.r5[j - 1]; + } + } + } + for i in 0..4 { + self.h[i] = product[i] & MASK; + product[i + 1] += product[i] >> 26; + } + self.h[4] = product[4] & MASK; + self.h[0] += 5 * (product[4] >> 26); + self.h[1] += self.h[0] >> 26; + self.h[0] &= MASK; + } + + /// Consumes this message and returns its full authentication tag: the body of the `MAC` + /// impl's three finals. + fn finalize(mut self) -> [u8; TAG_LEN] { + if self.buffered != 0 { + // The last, partial block: Sec 2.5.1's `| [0x01]` after its bytes, then zeros. + let mut block = core::mem::replace(&mut self.buffer, Secret::new()); + block[self.buffered] = 1; + block[self.buffered + 1..].fill(0); + self.process_block(&block, 0); + } + for i in 1..4 { + self.h[i + 1] += self.h[i] >> 26; + self.h[i] &= MASK; + } + self.h[0] += 5 * (self.h[4] >> 26); + self.h[4] &= MASK; + self.h[1] += self.h[0] >> 26; + self.h[0] &= MASK; + + // Compute h - p and select it iff h >= p, without branching on the accumulator. + let mut reduced: Secret<[u64; 5]> = Secret::new(); + reduced[0] = self.h[0] + 5; + for i in 0..4 { + reduced[i + 1] = self.h[i + 1] + (reduced[i] >> 26); + reduced[i] &= MASK; + } + reduced[4] = reduced[4].wrapping_sub(1 << 26); + // h >= p iff the borrow out of h - p is clear, i.e. iff reduced[4]'s top bit is 0; so h is + // kept iff that bit is set. + let keep_h = Condition::::from_msb(reduced[4]); + for i in 0..5 { + self.h[i] = keep_h.select(self.h[i], reduced[i]); + } + + let mut words: Secret<[u64; 4]> = Secret::new(); + words[0] = (self.h[0] | (self.h[1] << 26)) & 0xffffffff; + words[1] = ((self.h[1] >> 6) | (self.h[2] << 20)) & 0xffffffff; + words[2] = ((self.h[2] >> 12) | (self.h[3] << 14)) & 0xffffffff; + words[3] = ((self.h[3] >> 18) | (self.h[4] << 8)) & 0xffffffff; + let mut tag = [0u8; TAG_LEN]; + for i in 0..4 { + words[i] += u64::from(self.key.pad[i]); + if i != 0 { + words[i] += words[i - 1] >> 32; + } + tag[4 * i..4 * i + 4].copy_from_slice(&(words[i] as u32).to_le_bytes()); + } + tag + } +} + +fn load32(bytes: &[u8]) -> u64 { + u64::from(u32::from_le_bytes(bytes.try_into().unwrap())) +} + +impl MAC for Poly1305 { + fn output_len(&self) -> usize { + TAG_LEN + } + + fn mac(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn mac_out(mut self, data: &[u8], out: &mut [u8]) -> Result { + self.do_update(data); + self.do_final_out(out) + } + + fn verify(mut self, data: &[u8], mac: &[u8]) -> bool { + self.do_update(data); + self.do_verify_final(mac) + } + + fn do_update(&mut self, mut data: &[u8]) { + // A partial block left by an earlier call is topped up first, and is the only data + // that goes through the buffer apart from this call's own tail. + if self.buffered != 0 { + let n = (BLOCK_LEN - self.buffered).min(data.len()); + self.buffer[self.buffered..self.buffered + n].copy_from_slice(&data[..n]); + self.buffered += n; + data = &data[n..]; + if self.buffered < BLOCK_LEN { + return; + } + let block = core::mem::replace(&mut self.buffer, Secret::new()); + self.process_block(&block, FULL_BLOCK_BIT); + self.buffered = 0; + } + let (blocks, tail) = data.as_chunks::(); + for block in blocks { + self.process_block(block, FULL_BLOCK_BIT); + } + self.buffer[..tail.len()].copy_from_slice(tail); + self.buffered = tail.len(); + } + + fn do_final(self) -> Vec { + self.finalize().to_vec() + } + + /// Writes the full 16-byte tag and clears the rest of `out`. Short buffers are cleared + /// and rejected: Poly1305 tags must not be truncated. + fn do_final_out(self, out: &mut [u8]) -> Result { + out.fill(0); + if out.len() < TAG_LEN { + return Err(MACError::InvalidLength("Poly1305 requires a full 16-byte tag")); + } + out[..TAG_LEN].copy_from_slice(&self.finalize()); + Ok(TAG_LEN) + } + + fn do_verify_final(self, mac: &[u8]) -> bool { + ct::ct_eq_bytes(&self.finalize(), mac) + } + + fn max_security_strength(&self) -> SecurityStrength { + Self::MAX_SECURITY_STRENGTH + } +} + +/// The suspended state is the accumulator `h` as five little-endian `u64` limbs, the buffered +/// partial block and its length, for a construction that suspends a message in progress, as +/// ChaCha20-Poly1305 does. The one-time key is not in it: `Key` is the 32-byte key, re-supplied on +/// resume. There is no `SuspendableKeyed` impl, since a [`Poly1305Key`] is consumed by the one +/// worker it makes and so cannot be re-supplied. +/// +/// # 🚨 Security Considerations 🚨 +/// Resuming makes a worker from the raw key bytes, as [`hazmat::key_from_bytes`] does, with the +/// same obligation: the key authenticates one message, so resume a state once, under the key it +/// was suspended with. +impl<'k> SuspendableComponent<'k> for Poly1305 { + const STATE_LEN: usize = 5 * 8 + BLOCK_LEN + 8; + type Key = [u8; KEY_LEN]; + + fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + for limb in self.h.iter() { + w.u64(*limb); + } + w.bytes(&*self.buffer); + w.u64(self.buffered as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &'k Self::Key) -> Result { + let mut mac = Poly1305::from_key(Poly1305Key::from_key_bytes(key)); + let mut r = Cursor::new(state); + for (i, limb) in mac.h.iter_mut().enumerate() { + *limb = r.u64(); + // process_block leaves every limb within 26 bits but h[1], which can carry a little + // over; anything wider is not a state it writes, and could overflow its products. + let max = if i == 1 { (1 << 27) - 1 } else { MASK }; + if *limb > max { + return Err(SuspendableError::InvalidData); + } + } + (*mac.buffer).copy_from_slice(r.bytes(BLOCK_LEN)); + // A full block is processed as soon as it is complete, so at most 15 bytes are buffered. + mac.buffered = bounded_usize(r.u64(), BLOCK_LEN - 1)?; + debug_assert!(r.is_done()); + Ok(mac) + } +} diff --git a/crypto/poly1305/tests/poly1305_bc-test-data.rs b/crypto/poly1305/tests/poly1305_bc-test-data.rs new file mode 100644 index 000000000..ed3fa6782 --- /dev/null +++ b/crypto/poly1305/tests/poly1305_bc-test-data.rs @@ -0,0 +1,41 @@ +//! RFC known-answer vectors are loaded from bc-test-data, with no embedded fixture data. +use bouncycastle_core::traits::MAC; +use bouncycastle_core_test_framework::mac::TestFrameworkMAC; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data_json; +use bouncycastle_poly1305::hazmat::key_from_bytes; + +#[test] +fn rfc8439_poly1305_including_reduction_edge_cases() { + let Some(document) = bc_test_data_json("crypto/rfc8439", "poly1305.json") else { return }; + let cases = document["tests"].as_array().unwrap(); + assert_eq!(cases.len(), 12); + for case in cases { + let decode = |field: &str| bouncycastle_hex::decode(case[field].as_str().unwrap()).unwrap(); + let key: [u8; 32] = decode("key").try_into().unwrap(); + let msg = decode("msg"); + let tag = decode("tag"); + let id = case["id"].as_str().unwrap(); + // A published key, so a test may make as many keys from it as it likes. + let generator = || key_from_bytes(&key).into_mac(); + // Under `r = 0` (A.3.1, A.3.2) the tag is `s` whatever the message, which the generic + // suite's check that another message gets another tag cannot allow for. + if key[..16] != [0; 16] { + TestFrameworkMAC::new().test_one_time_mac(generator, &msg, &tag); + } + assert_eq!(generator().mac(&msg), tag, "RFC {id}"); + let mut out = [0xa5; 24]; + assert_eq!(generator().mac_out(&msg, &mut out).unwrap(), 16); + assert_eq!(&out[..16], tag); + assert_eq!(&out[16..], &[0; 8]); + assert!(key_from_bytes(&key).into_mac().verify(&msg, &tag)); + // Every split of each published message exercises full and partial buffered blocks. + for split in 0..=msg.len() { + let mut mac = generator(); + mac.do_update(&msg[..split]); + mac.do_update(&[]); + mac.do_update(&msg[split..]); + assert_eq!(mac.do_final(), tag, "RFC {id}, split {split}"); + } + } + println!("RFC 8439 Poly1305: {} vectors passed", cases.len()); +} diff --git a/crypto/poly1305/tests/poly1305_tests.rs b/crypto/poly1305/tests/poly1305_tests.rs new file mode 100644 index 000000000..1d053aafb --- /dev/null +++ b/crypto/poly1305/tests/poly1305_tests.rs @@ -0,0 +1,229 @@ +use bouncycastle_core::errors::{MACError, SuspendableError}; +use bouncycastle_core::traits::MAC; +use bouncycastle_core_test_framework::mac::TestFrameworkMAC; +use bouncycastle_poly1305::hazmat::key_from_bytes; +use bouncycastle_poly1305::{Poly1305, Poly1305Key}; +use bouncycastle_utils::suspendable_state::SuspendableComponent; + +/// A public test key, so these tests make as many keys from it as they like; a real one-time key +/// authenticates one message. +const KEY: [u8; 32] = [0x42; 32]; + +fn generator() -> Poly1305 { + key_from_bytes(&KEY).into_mac() +} + +fn verify(msg: &[u8], tag: &[u8]) -> bool { + key_from_bytes(&KEY).into_mac().verify(msg, tag) +} + +/// The framework's API-contract checks, which need no external data. The tag's correctness is +/// pinned by the RFC 8439 vectors in poly1305_bc-test-data.rs, which run the same suite. +#[test] +fn core_mac_contract() { + let msg = b"framework message spanning more than one 16-byte block"; + let tag = generator().mac(msg); + TestFrameworkMAC::new().test_one_time_mac(generator, msg, &tag); +} + +#[test] +fn empty_message_returns_the_additive_key_half() { + assert_eq!(generator().do_final(), KEY[16..]); +} + +#[test] +fn streaming_and_output_buffer_contract() { + for len in [0, 1, 15, 16, 17, 31, 32, 33, 63, 64, 65, 255, 1025] { + let msg: Vec = (0..len).map(|i| (i as u8).wrapping_mul(7)).collect(); + let expected = generator().mac(&msg); + for chunk in [1, 3, 15, 16, 17, 64, 129] { + let mut mac = generator(); + assert_eq!(mac.output_len(), 16); + mac.do_update(&[]); + for bytes in msg.chunks(chunk) { + mac.do_update(bytes); + mac.do_update(&[]); + } + let mut out = [0xa5; 32]; + assert_eq!(mac.do_final_out(&mut out).unwrap(), 16); + assert_eq!(&out[..16], expected); + assert_eq!(&out[16..], &[0; 16]); + } + assert!(verify(&msg, &expected)); + for bit in 0..128 { + let mut bad = expected.clone(); + bad[bit / 8] ^= 1 << (bit % 8); + assert!(!verify(&msg, &bad)); + } + assert!(!verify(&msg, &expected[..15])); + assert!(!verify(&msg, &[0; 17])); + } +} + +/// One message fed in pieces of 1, 15, 16, 17 and 33 bytes, which between them top up a buffered +/// block exactly, pass whole blocks straight from the input, and leave a tail before and after a +/// top-up, gives the one-shot tag. +#[test] +fn pieces_of_1_15_16_17_and_33_bytes_give_the_one_shot_tag() { + let pieces = [1, 15, 16, 17, 33]; + let msg: Vec = + (0..pieces.iter().sum()).map(|i: usize| (i as u8).wrapping_mul(13)).collect(); + let expected = generator().mac(&msg); + let mut mac = generator(); + let mut rest = &msg[..]; + for n in pieces { + let (piece, tail) = rest.split_at(n); + mac.do_update(piece); + rest = tail; + } + assert_eq!(mac.do_final(), expected); +} + +const STATE_LEN: usize = >::STATE_LEN; + +/// A message suspended part-way, with or without a partial block buffered, resumes under its key +/// to the tag the uninterrupted message gets. The key is not in the state: another key gives +/// another tag. +#[test] +fn a_suspended_message_resumes_to_the_same_tag() { + let msg: Vec = (0..100u8).collect(); + let expected = generator().mac(&msg); + for split in [0, 1, 15, 16, 17, 33, 100] { + let mut mac = generator(); + mac.do_update(&msg[..split]); + let mut state = [0u8; STATE_LEN]; + mac.write_state(&mut state); + let mut resumed = Poly1305::read_state(&state, &KEY).unwrap(); + resumed.do_update(&msg[split..]); + assert_eq!(resumed.do_final(), expected, "split at {split}"); + + let mut other = Poly1305::read_state(&state, &[0x24; 32]).unwrap(); + other.do_update(&msg[split..]); + assert_ne!(other.do_final(), expected, "split at {split}, another key"); + } +} + +/// The layout: the five limbs of `h`, the 16-byte buffer and the buffered length, each as written +/// by the worker. A limb wider than the worker leaves it, or a whole buffered block, is refused. +#[test] +fn a_state_outside_the_bounds_the_worker_keeps_is_refused() { + const MASK: u64 = (1 << 26) - 1; + assert_eq!(STATE_LEN, 5 * 8 + 16 + 8); + let mut mac = generator(); + mac.do_update(&[0x5a; 21]); + let mut state = [0u8; STATE_LEN]; + mac.write_state(&mut state); + let with = |offset: usize, value: u64| { + let mut s = state; + s[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); + s + }; + let refused = |s: [u8; STATE_LEN]| { + matches!(Poly1305::read_state(&s, &KEY), Err(SuspendableError::InvalidData)) + }; + assert!(Poly1305::read_state(&state, &KEY).is_ok()); + for limb in [0, 2, 3, 4] { + assert!(Poly1305::read_state(&with(8 * limb, MASK), &KEY).is_ok(), "h[{limb}] at 26 bits"); + assert!(refused(with(8 * limb, MASK + 1)), "h[{limb}] past 26 bits"); + } + // h[1] alone can carry over 26 bits. + assert!(Poly1305::read_state(&with(8, (1 << 27) - 1), &KEY).is_ok()); + assert!(refused(with(8, 1 << 27))); + assert!(Poly1305::read_state(&with(56, 15), &KEY).is_ok()); + assert!(refused(with(56, 16)), "a whole block is never left buffered"); +} + +#[test] +fn full_tags_are_required() { + for len in 0..16 { + let mut out = vec![0xa5; len]; + assert!(matches!( + generator().mac_out(b"message", &mut out), + Err(MACError::InvalidLength(_)) + )); + assert!(out.iter().all(|&b| b == 0)); + } +} + +/// The all-zero key, which RFC 8439 accepts and only the caller can refuse: with `r = 0` and +/// `s = 0` every tag is zero. +#[test] +fn the_all_zero_key_is_accepted() { + assert_eq!(key_from_bytes(&[0; 32]).into_mac().mac(b"zero-key test"), [0; 16]); +} + +/// One full block under a key whose clamped `r` is the single limb `r0 < 2^26` and whose `s` is +/// zero, chosen (by an offline search) so that the block's product carries out of the top limb +/// and leaves `h0` within 5 of 2^26. Those are the only inputs that reach finalize's last +/// `h1 += h0 >> 26` with a non-zero carry, which no RFC or Wycheproof vector does. +/// +/// The expected tag is computed by `reference_tag`, an independent big-integer evaluation of +/// RFC 8439 Sec 2.5's `acc = ((acc + block) * r) % p`, `tag = (acc + s) mod 2^128`. +#[test] +fn final_carry_out_of_h0() { + for (r0, block) in [ + (0x3fffffe_u32, "eb25b4d74b68af97d05e2fa1bd5e427b"), + (0x3ffffff_u32, "01000005000014000050000040010000"), + ] { + let block: [u8; 16] = bouncycastle_hex::decode(block).unwrap().try_into().unwrap(); + let mut key = [0u8; 32]; + key[..4].copy_from_slice(&r0.to_le_bytes()); + assert_eq!( + key_from_bytes(&key).into_mac().mac(&block), + reference_tag(r0, &block), + "r0 = {r0:#x}" + ); + } +} + +/// RFC 8439 Sec 2.5 for one full block, `r = r0 < 2^26` and `s = 0`, over little-endian 32-bit +/// limbs: `acc = ((block + 2^128) * r0) mod (2^130 - 5)`, `tag = acc mod 2^128`. +fn reference_tag(r0: u32, block: &[u8; 16]) -> [u8; 16] { + // x = block + 2^128: the block with the 0x01 byte appended. + let mut x = [0u64; 5]; + for (limb, bytes) in x.iter_mut().zip(block.chunks_exact(4)) { + *limb = u64::from(u32::from_le_bytes(bytes.try_into().unwrap())); + } + x[4] = 1; + // v = x * r0, in six limbs. + let mut v = [0u64; 6]; + let mut carry = 0; + for (out, limb) in v.iter_mut().zip(x) { + let t = limb * u64::from(r0) + carry; + *out = t & 0xffff_ffff; + carry = t >> 32; + } + v[5] = carry; + // v = hi * 2^130 + lo, and 2^130 = 5 (mod p): v becomes lo + 5 * hi, below 2^131. + let hi = (v[4] >> 2) | (v[5] << 30); + v[4] &= 3; + let mut carry = 5 * hi; + for limb in &mut v[..5] { + let t = *limb + carry; + *limb = t & 0xffff_ffff; + carry = t >> 32; + } + // Subtract p = 2^130 - 5 while v >= p. + const P: [u64; 5] = [0xffff_fffb, 0xffff_ffff, 0xffff_ffff, 0xffff_ffff, 3]; + while (0..5).rev().find(|&i| v[i] != P[i]).is_none_or(|i| v[i] > P[i]) { + let mut borrow = 0; + for i in 0..5 { + let t = (v[i] + (1 << 32)) - P[i] - borrow; + v[i] = t & 0xffff_ffff; + borrow = 1 - (t >> 32); + } + } + let mut tag = [0u8; 16]; + for (bytes, limb) in tag.chunks_exact_mut(4).zip(v) { + bytes.copy_from_slice(&(limb as u32).to_le_bytes()); + } + tag +} + +/// The figures in the crate docs' Memory Usage table. +#[cfg(target_pointer_width = "64")] +#[test] +fn sizes_match_the_documented_memory_table() { + assert_eq!(core::mem::size_of::(), 88); + assert_eq!(core::mem::size_of::(), 192); +} diff --git a/src/lib.rs b/src/lib.rs index ae250f332..068807c3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ pub use bouncycastle_aes as aes; pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; +pub use bouncycastle_chacha20 as chacha20; +pub use bouncycastle_chacha20_poly1305_aead as chacha20_poly1305_aead; pub use bouncycastle_cipher as cipher; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory; @@ -11,6 +13,7 @@ pub use bouncycastle_mldsa as mldsa; pub use bouncycastle_mldsa_lowmemory as mldsa_lowmemory; pub use bouncycastle_mlkem as mlkem; pub use bouncycastle_mlkem_lowmemory as mlkem_lowmemory; +pub use bouncycastle_poly1305 as poly1305; pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3;