From fbf8147a7ad8bf3b2500f8c4b8e0e481778b308b Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Thu, 3 Sep 2026 17:29:06 +1000 Subject: [PATCH 1/4] Initial add of revamped CFB mode based off of aes-tweaks (#103) Squash of the original two commits (7f96e1b "Initial add of revamped CFB mode based off of aes-tweaks" and 2a5d834 "Restored file changes on 3 files to resolve failing checks issues"), whose net effect is this change; the first had reverted three files the second restored. Rebased onto feature/aes-tweaks at 0d06cf9; the port to the in-place block cipher API follows in a separate commit. --- alpha_0.1.3_release_notes.md | 110 +++- cli/src/aes_cbc_cmd.rs | 282 +--------- cli/src/aes_cfb_cmd.rs | 75 +++ cli/src/block_mode_cmd.rs | 263 +++++++++ cli/src/main.rs | 102 +++- cli/tests/aes_cfb_cli_tests.rs | 444 ++++++++++++++++ crypto/aes-lowmemory/src/cfb.rs | 84 +++ crypto/aes-lowmemory/src/lib.rs | 11 +- crypto/modes/Cargo.toml | 2 + crypto/modes/benches/modes_benches.rs | 192 ++++++- crypto/modes/src/cfb.rs | 279 ++++++++++ crypto/modes/src/lib.rs | 199 +++++-- crypto/modes/tests/acvp_cfb_tests.rs | 305 +++++++++++ crypto/modes/tests/cfb_tests.rs | 620 ++++++++++++++++++++++ crypto/modes/tests/common/mod.rs | 47 ++ crypto/modes/tests/sp800_38a_cfb_tests.rs | 377 +++++++++++++ 16 files changed, 3063 insertions(+), 329 deletions(-) create mode 100644 cli/src/aes_cfb_cmd.rs create mode 100644 cli/src/block_mode_cmd.rs create mode 100644 cli/tests/aes_cfb_cli_tests.rs create mode 100644 crypto/aes-lowmemory/src/cfb.rs create mode 100644 crypto/modes/src/cfb.rs create mode 100644 crypto/modes/tests/acvp_cfb_tests.rs create mode 100644 crypto/modes/tests/cfb_tests.rs create mode 100644 crypto/modes/tests/sp800_38a_cfb_tests.rs diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index d74c766d..7e2e41c3 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -26,26 +26,37 @@ permutation (NIST FIPS 197), re-exported from the umbrella crate. * Deliberately ships no CLI subcommand, no factory entry and no `core` cipher-trait impls: a raw permutation can only offer ECB, and those are mode-of-operation concerns. `Algorithm` is implemented (name and security strength); per-mode OIDs and the `BlockCipherEncryptor` / `BlockCipherDecryptor` impls belong to the mode crates. +* Ships the type aliases `AES_CBC_128` / `AES_CBC_192` / `AES_CBC_256` and `AES_CFB_128` / + `AES_CFB_192` / `AES_CFB_256`, which fill in the const parameters of `bouncycastle-modes`' `Cbc` + and `Cfb` and leave the direction as the type parameter. They are aliases only -- no new engine + code, and each one's doctest round-trips and shows that a misaligned length fails to compile. New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of operation -(NIST SP 800-38A), currently **CBC** (Sec 6.2). Re-exported from the umbrella crate. - -* `Cbc` over any `BlockPermutation`, so the crate depends on no - concrete cipher. The direction is a type parameter: `BlockCipherEncryptor` is implemented only - for `Cbc<_, Encrypting, _, _>` and `BlockCipherDecryptor` only for `Cbc<_, Decrypting, _, _>`, - making a wrong-direction call a compile error rather than a runtime check. -* **The IV is generated, never accepted.** SP 800-38A Sec 5.3 requires the CBC IV to be +(NIST SP 800-38A), providing **CBC** (Sec 6.2) and **CFB128** (Sec 6.3). Re-exported from the +umbrella crate. + +* `Cbc` and `Cfb` over any + `BlockPermutation`, so the crate depends on no concrete cipher. The direction is a type parameter: + `BlockCipherEncryptor` is implemented only for `<_, Encrypting, _, _>` and `BlockCipherDecryptor` + only for `<_, Decrypting, _, _>`, making a wrong-direction call a compile error rather than a + runtime check. The two types have identical APIs and identical size, so swapping one for the other + is a one-word change. +* **The IV is generated, never accepted.** SP 800-38A Sec 5.3 requires the CBC *and CFB* IV to be *unpredictable*, not merely unique, so `do_encrypt_init` draws one from the library's default OS-backed DRBG (Appendix C's second recommended method) and returns it; there is no API for supplying your own. Known-answer tests drive `do_encrypt_init_rng` with a fixed-output test RNG. + This matters more for CFB than for CBC: CFB XORs a keystream, so a repeated key-and-IV pair leaks + `P1 XOR P1'` outright rather than merely whether the blocks were equal. * **Parallel decryption.** Sec 6.2 notes CBC decryption's inverse cipher calls can run in parallel, so `do_decrypt_blocks[_out]` walks the ciphertext in pairs through `BlockPermutation::decrypt_blocks2`, with a one-block remainder for odd `N`. Measured against an otherwise identical permutation that does not override the pair methods, this is **1.83x** the decryption throughput (67.9 vs 37.1 MiB/s, AES-128, 16 KiB, N=8). CBC encryption is serial by construction and does not use it. -* Strictly block-aligned, as Sec 5.2 requires of CBC. Arbitrary-length data needs a padding layer, - which does not exist in this workspace yet; when it lands, CBC gets it by being wrapped. +* Strictly block-aligned, as Sec 5.2 requires of CBC. Arbitrary-length data goes through + `bouncycastle-padding`'s `PaddedEncryptor` / `PaddedDecryptor`, which wrap either mode; no padding + logic lives in this crate. `crypto/modes/tests/cfb_tests.rs` round-trips every length from 0 to + `3 * BLOCK_LEN + 1` through PKCS7 to pin that the two crates compose. * Verified against all six SP 800-38A Appendix F.2 vectors (CBC-AES128/192/256, Encrypt and Decrypt), each checked in one call, one block at a time, in a `3 + 1` grouping that exercises the pair remainder, and through the `_out` variant. Appendix D error propagation is tested @@ -58,29 +69,90 @@ New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of op file, the CBC one carries only the answer against a `tcId`, so the request and response files are joined; the 6 MCT groups are skipped and the count reported. These vectors were already in `bc-test-data` and previously unused. -* No CFB yet -- see the crate docs' "Not yet implemented". - -`cli`: three new subcommands, `aes128-cbc`, `aes192-cbc` and `aes256-cbc`, each taking `encrypt` or -`decrypt` and streaming stdin to stdout in 1 KiB chunks. - +CFB (`Cfb`), SP 800-38A Sec 6.3: + +* **Full-block segment only.** Sec 6.3 parameterises CFB by a segment size `s` with `1 <= s <= b`; + `Cfb` implements `s = b` -- CFB128 for AES -- because that is the only segment size that is + block-aligned and therefore the only one that fits `BlockCipherEncryptor` / + `BlockCipherDecryptor`. With `s = b` the spec's `LSB_{b-s}(I_{j-1}) | C#_{j-1}` collapses to + `Ij = C_{j-1}` and `MSB_s(Oj)` to `Oj`, which the module docs derive step by step. **CFB8 and + CFB1 are different, non-interoperable modes and are not provided**; they need a `StreamCipher` + shape, and both the crate docs and the CLI help say so explicitly. +* **Decryption uses the forward cipher function.** Sec 6.3 applies `CIPH_K` in both directions, so + `Cfb<_, Decrypting, _, _>` never calls `decrypt_block` or `decrypt_blocks2`. This is pinned by a + test permutation whose inverse methods panic, run over both the pair and single-block paths -- so + the claim is enforced rather than merely documented. +* **Parallel decryption**, via `encrypt_blocks2`: Sec 6.3 notes CFB decryption's forward cipher + calls "can be performed in parallel if the input blocks are first constructed (in series) from the + IV and the ciphertext", and with `s = b` those input blocks simply *are* the IV followed by the + ciphertext. Measured against an otherwise identical permutation that does not override the pair + methods, this is **2.08x** the decryption throughput (110.9 vs 53.3 MiB/s, AES-128, 16 KiB, N=8). + In the same run CFB decryption was **1.37x** CBC decryption (110.9 vs 80.8 MiB/s), because the + bit-sliced engine's forward direction is cheaper than its inverse and CFB only ever needs the + forward one. CFB encryption is serial by construction and does not use the pair path -- verified, + not assumed: the swapped-pair test permutation produces identical ciphertext under `Cfb` encrypt. +* Same size as `Cbc` -- one permutation plus one block of feedback (192/224/256 B for + AES-128/192/256) -- because the keystream block `Oj` is recomputed per call and lives only in a + local, so no keystream outlives the call that used it. +* Verified against all six SP 800-38A **Appendix F.3.13-F.3.18** vectors (CFB128-AES128/192/256, + Encrypt and Decrypt) in the same four groupings as CBC. F.3 additionally tabulates the *output + blocks* -- the keystream -- so those are checked against the raw permutation too + (`Oj == CIPH_K(I_j)` and `Cj == Pj XOR Oj` for all four segments of all three key lengths), which + pins the mode's internals and not just its final output. As a transcription cross-check, CFB128 + is required to agree with **Appendix F.4.1 (OFB)** on the first block -- both compute + `C1 = P1 XOR CIPH_K(IV)` -- and to disagree from the second. +* Also verified against the **2138 NIST ACVP `ACVP-AES-CFB128` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 54 of them spanning 2-10 blocks), each run twice, block by + block and in pairs with a remainder. The 6 MCT groups are skipped and the count reported. These + vectors were already in `bc-test-data` and previously unused. +* Appendix D error propagation is tested in the direction that distinguishes CFB from CBC. Table D.2 + gives CFB "SBE in the decryption of Cj": every one of the 128 bit positions of `C2` is flipped and + required to flip *exactly* that bit of `P2` (the block the attacker aimed at, unlike CBC where it + lands in `P3`), to randomise `P3`, and to leave `P1` and `P4` untouched. The IV case is checked + with real AES, where a corrupted IV must *randomise* `P1` rather than flip a bit in place, and + must not affect any later block -- with `s = b`, Appendix D's "first `i/s` (rounding up)" + segments is one segment for every bit position. +* Mutation-tested: `cargo mutants -p bouncycastle-modes` reports **0 surviving mutants** (72 + mutants, 39 caught, 33 unviable), including every `^`-to-`|`/`&` substitution and every + keystream-stubbing mutant in `cfb.rs`. +* Still not implemented, and listed in the crate docs: the CFB segment sizes below the block size + (`s = 8`, `s = 1`), and ECB, OFB and CTR. + +`cli`: six new subcommands -- `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `aes128-cfb`, `aes192-cfb` +and `aes256-cfb` -- each taking `encrypt` or `decrypt` and streaming stdin to stdout in 1 KiB +chunks. + +* All the mode-independent plumbing -- key loading, stdin framing, block-alignment enforcement, + hex/binary output -- lives once in `cli/src/block_mode_cmd.rs`, generic over the mode via + `BlockCipherEncryptor` / `BlockCipherDecryptor`. `aes_cbc_cmd.rs` and `aes_cfb_cmd.rs` are thin + dispatchers over it, so the two commands cannot drift apart on the parts that affect correctness. * Key from `--key` (hex) or `--key-file` (binary or hex), with the usual note that secrets on the command line end up in shell history. The key length must match the variant exactly. * **The IV travels in the ciphertext**: since there is no API for supplying one, `encrypt` writes the generated IV as the first 16 bytes of its output and `decrypt` reads it back from the first 16 bytes of its input, so `encrypt | decrypt` composes with no `--iv` flag anywhere. The IV need not be secret (SP 800-38A Sec 5.3), so this is sound. -* Input must be a whole number of 16-byte blocks. Unaligned input is rejected with a message - pointing at the missing padding layer rather than being silently padded. +* Input must be a whole number of 16-byte blocks. Unaligned input is rejected with a message saying + the commands apply no padding rather than being silently padded. +* The `-cfb` commands are **CFB128**, and both the subcommand help and the alignment error name the + segment size, because `CFB8` and `CFB1` are different modes that would silently produce + incompatible output. * Reads need not respect block boundaries: bytes accumulate in a 1 KiB buffer that goes through the flat `do_*_out::<1024>` when full, and the whole-block remainder at end of input goes one block at a time; verified by round-tripping 64 KiB through `dd bs=3`. -* Verified against SP 800-38A F.2: prepending the spec's IV to the spec's ciphertext and running - `decrypt` reproduces the spec's plaintext for all three key lengths. The `encrypt` direction was - cross-checked against an independent CBC implementation under the IV the CLI generated. +* Verified against SP 800-38A F.2 (CBC) and F.3.13/F.3.15/F.3.17 (CFB128): prepending the spec's IV + to the spec's ciphertext and running `decrypt` reproduces the spec's plaintext for all three key + lengths in both modes. The CBC `encrypt` direction was cross-checked against an independent CBC + implementation under the IV the CLI generated. * `cli/tests/aes_cbc_cli_tests.rs` (16 tests) drives the built binary as a subprocess via `CARGO_BIN_EXE_bc-rust`, so all of the above is asserted by `cargo test` rather than by hand: the F.2 vectors, round trips across the chunk boundary, a fresh IV per invocation, hex/binary agreement, `--key-file` in both hex and binary, and every error path with its message. +* `cli/tests/aes_cfb_cli_tests.rs` (18 tests) mirrors that suite -- the shared plumbing is generic + over the mode, so a wiring mistake in the CFB dispatcher would not show up in the CBC tests -- and + adds three CFB-specific checks: the F.3 vectors, the Appendix D single-bit malleability observed + end to end through the pipe, and a guard that a CFB ciphertext does not decrypt as CBC or vice + versa (neither mode is authenticated, so the mismatch is otherwise silent). `core`: new `BlockPermutation` trait (`crypto/core/src/traits.rs`), the raw keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode is built on. diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index d532a31a..1176026c 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -1,288 +1,64 @@ //! AES-CBC encryption and decryption, streaming stdin to stdout. //! -//! # The IV travels in the ciphertext +//! Only the mode wiring lives here: the IV convention, key loading, stdin framing and +//! block-alignment enforcement are all in [`crate::block_mode_cmd`], shared with the `aes*-cfb` +//! commands. See that module for the command-line contract. //! -//! There is no `--iv` flag, and that is deliberate: `bouncycastle-modes` has no API for a -//! caller-supplied IV, because NIST SP 800-38A Sec 5.3 requires the CBC IV to be *unpredictable* -//! rather than merely unique. `encrypt` therefore generates one from the OS-backed DRBG and writes -//! it as the **first block of the output**; `decrypt` reads it back from the **first block of the -//! input**. So the two compose directly: -//! -//! ```text -//! bc-rust aes128-cbc encrypt --key-file k.bin < plain.bin > cipher.bin -//! bc-rust aes128-cbc decrypt --key-file k.bin < cipher.bin > plain.bin -//! ``` -//! -//! The IV is not secret (Sec 5.3), so shipping it in the clear is correct. Its *integrity* is not -//! protected, and neither is the ciphertext's -- see the warning below. -//! -//! # Input must be block-aligned -//! -//! CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and this workspace has no padding -//! layer yet, so input that is not a multiple of 16 bytes is rejected rather than silently padded. -//! Padding is the caller's business until `PaddedEncryptor`/`PaddedDecryptor` land. -//! -//! # Binary in, binary out -//! -//! stdin is read as binary so the commands compose in a pipeline. `-x` renders the *output* as hex. -//! For hex input, pipe through `hex-decode` first: -//! -//! ```text -//! cat cipher.hex | bc-rust hex-decode | bc-rust aes256-cbc decrypt --key-file k.bin -//! ``` +//! CBC (NIST SP 800-38A Sec 6.2) provides confidentiality only. It does not detect tampering, and +//! neither the ciphertext nor the IV is authenticated -- a flipped ciphertext bit flips the same bit +//! of the *next* block's plaintext (Appendix D). Do not decrypt data you have not authenticated +//! separately. -use crate::helpers::write_bytes_or_hex; +use crate::block_mode_cmd::{BLOCK_LEN, BlockModeAction, decrypt_stream, encrypt_stream, load_key}; use bouncycastle::aes_lowmemory::{Aes128, Aes192, Aes256}; -use bouncycastle::core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle::core::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, -}; -use bouncycastle::hex; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::BlockPermutation; use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; -use clap::ValueEnum; -use std::io::{Read, Write}; -use std::process::exit; -use std::{fs, io}; -/// The AES block length in bytes. -const BLOCK_LEN: usize = 16; - -/// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. -/// -/// A full chunk goes through `do_*::` in one call, in place, which for decryption means -/// 32 pairs down the `decrypt_blocks2` path. The at-most-63-block tail at end of input goes one -/// block at a time; it is bounded, so its cost does not scale with the input. -const CHUNK_LEN: usize = 64 * BLOCK_LEN; - -#[derive(ValueEnum, Clone, Debug)] -pub(crate) enum AESCBCAction { - /// Encrypt stdin to stdout under CBC mode. - /// A freshly generated IV is written as the first 16 bytes of the output, so that `decrypt` - /// can read it back. Input length must be a multiple of 16 bytes. - Encrypt, - /// Decrypt stdin to stdout under CBC mode. - /// The first 16 bytes of input are taken as the IV, as written by `encrypt`. The remaining - /// length must be a multiple of 16 bytes. - Decrypt, -} +/// Names the mode in error messages. +const MODE: &str = "CBC"; pub(crate) fn aes128_cbc_cmd( - action: &AESCBCAction, + action: &BlockModeAction, key: &Option, key_file: &Option, output_hex: bool, ) { - let key = load_key::<16>(key, key_file, "AES-128"); - match action { - AESCBCAction::Encrypt => encrypt_stream::(&key, output_hex), - AESCBCAction::Decrypt => decrypt_stream::(&key, output_hex), - } + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); } pub(crate) fn aes192_cbc_cmd( - action: &AESCBCAction, + action: &BlockModeAction, key: &Option, key_file: &Option, output_hex: bool, ) { - let key = load_key::<24>(key, key_file, "AES-192"); - match action { - AESCBCAction::Encrypt => encrypt_stream::(&key, output_hex), - AESCBCAction::Decrypt => decrypt_stream::(&key, output_hex), - } + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); } pub(crate) fn aes256_cbc_cmd( - action: &AESCBCAction, + action: &BlockModeAction, key: &Option, key_file: &Option, output_hex: bool, ) { - let key = load_key::<32>(key, key_file, "AES-256"); - match action { - AESCBCAction::Encrypt => encrypt_stream::(&key, output_hex), - AESCBCAction::Decrypt => decrypt_stream::(&key, output_hex), - } + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); } -/// Loads the key from `--key` (hex) or `--key-file` (binary or hex), and checks its length. -/// -/// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the -/// wrong length is a mistake rather than something to truncate or pad. -fn load_key( - key: &Option, - key_file: &Option, - alg: &str, -) -> KeyMaterial { - let key_bytes: Vec = if let Some(key_file) = key_file { - // A file may hold raw bytes or hex; try hex first, as the other commands do. - let raw = fs::read(key_file).unwrap_or_else(|e| { - eprintln!("Error: couldn't read key file '{key_file}': {e}"); - exit(-1); - }); - match hex::decode(&raw) { - Ok(decoded) => decoded, - Err(_) => raw, - } - } else if let Some(key) = key { - hex::decode(key).unwrap_or_else(|_| { - eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); - exit(-1); - }) - } else { - eprintln!("Error: either `--key` or `--key-file` must be supplied."); - exit(-1); - }; - - if key_bytes.len() != KEY_LEN { - eprintln!("Error: {alg} needs a {KEY_LEN}-byte key, got {} bytes.", key_bytes.len()); - exit(-1); - } - - // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what - // the engine requires -- except for an all-zero key, which it marks Zeroized instead. - let mut key = - KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }); - - if key.key_type() != KeyType::SymmetricCipherKey { - // Same stance as `helpers::parse_seed`: warn, then do what was asked. A CLI is used for - // test vectors and scripting, where an all-zero key is a legitimate thing to want. - eprintln!( - "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." - ); - do_hazardous_operations(&mut key, |key| { - key.set_key_type(KeyType::SymmetricCipherKey)?; - key.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) - }) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't tag the key: {e:?}"); - exit(-1); - }); - } - - key -} - -/// Encrypts stdin to stdout, writing the generated IV first. -fn encrypt_stream(key: &KeyMaterial, output_hex: bool) -where - P: BlockPermutation, -{ - let (mut enc, iv) = Cbc::::do_encrypt_init(key) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't start encryption: {e:?}"); - exit(-1); - }); - - // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. - write_bytes_or_hex(&iv, output_hex); - - // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. - stream_aligned(|data| { - if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { - // Cannot fail: CBC has no per-IV data limit. - enc.do_encrypt(chunk).unwrap(); - } else { - // The bounded tail at end of input: whole blocks, fewer than a chunk. - for block in data.as_chunks_mut::().0 { - enc.do_encrypt(block).unwrap(); - } - } - write_bytes_or_hex(data, output_hex); - }); - - finish(output_hex); -} - -/// Decrypts stdin to stdout, taking the IV from the first block of input. -fn decrypt_stream(key: &KeyMaterial, output_hex: bool) -where +/// Dispatches to the shared streaming loops with `Cbc` filled in as the mode. +fn run( + action: &BlockModeAction, + key: &KeyMaterial, + output_hex: bool, +) where P: BlockPermutation, { - // The leading block is the IV, not ciphertext. - let mut iv = [0u8; BLOCK_LEN]; - if let Err(e) = io::stdin().read_exact(&mut iv) { - eprintln!( - "Error: input too short to contain the {BLOCK_LEN}-byte IV that `encrypt` writes \ - as its first block ({e})." - ); - exit(-1); - } - - let mut dec = Cbc::::do_decrypt_init(key, &iv) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't start decryption: {e:?}"); - exit(-1); - }); - - stream_aligned(|data| { - if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { - // A full chunk is 32 pairs, so this is the `decrypt_blocks2` path. - dec.do_decrypt(chunk).unwrap(); - } else { - for block in data.as_chunks_mut::().0 { - dec.do_decrypt(block).unwrap(); - } - } - write_bytes_or_hex(data, output_hex); - }); - - finish(output_hex); -} - -/// Reads stdin and hands it to `process` in block-aligned pieces, mutably so it can be transformed -/// in place: a full `CHUNK_LEN` bytes each time one has accumulated, then once more at end of input -/// with whatever whole blocks remain (fewer than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the -/// buffer until it is full -- so a block split across two reads needs no special handling. -/// -/// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because CBC is not -/// defined on a partial block and there is no padding layer to appeal to. -fn stream_aligned(mut process: impl FnMut(&mut [u8])) { - let mut buf = [0u8; CHUNK_LEN]; - let mut filled = 0usize; - - loop { - let n = io::stdin().read(&mut buf[filled..]).unwrap_or_else(|e| { - eprintln!("Error: failed to read from stdin: {e}"); - exit(-1); - }); - if n == 0 { - break; + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN>(key, output_hex, MODE) } - filled += n; - if filled == CHUNK_LEN { - process(&mut buf); - filled = 0; + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN>(key, output_hex, MODE) } } - - if !filled.is_multiple_of(BLOCK_LEN) { - eprintln!( - "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ - CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and this build has no \ - padding layer, so the input must be padded by the caller.", - filled % BLOCK_LEN - ); - exit(-1); - } - if filled != 0 { - process(&mut buf[..filled]); - } -} - -/// Flushes stdout, and adds the trailing newline the hex-output commands all emit. -fn finish(output_hex: bool) { - if output_hex { - println!(); - } - io::stdout().flush().unwrap_or_else(|e| { - eprintln!("Error: failed to flush stdout: {e}"); - exit(-1); - }); } diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs new file mode 100644 index 00000000..fac417ab --- /dev/null +++ b/cli/src/aes_cfb_cmd.rs @@ -0,0 +1,75 @@ +//! AES-CFB128 encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading, stdin framing and +//! block-alignment enforcement are all in [`crate::block_mode_cmd`], shared with the `aes*-cbc` +//! commands. See that module for the command-line contract. +//! +//! # Which CFB +//! +//! These commands are **CFB128**: the segment size is the full 16-byte block (`s = b` in NIST +//! SP 800-38A Sec 6.3). That is the only segment size `bouncycastle-modes` provides, because it is +//! the only block-aligned one. SP 800-38A also defines `s = 8` and `s = 1`, which are *not* +//! interoperable with these commands -- if you need `CFB8` or `CFB1`, this is not it. +//! +//! # Warning +//! +//! CFB provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. CFB's malleability is more directly exploitable than CBC's: Appendix D, +//! Table D.2 gives "SBE in the decryption of Cj" -- flipping a ciphertext bit flips the *same* bit +//! of the plaintext in the *same* block, so an attacker edits the block they aimed at, at the cost +//! of randomising the next one. Do not decrypt data you have not authenticated separately. + +use crate::block_mode_cmd::{BLOCK_LEN, BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::BlockPermutation; +use bouncycastle::modes::{Cfb, Decrypting, Encrypting}; + +/// Names the mode in error messages. Spelled with the segment size, because `CFB8` and `CFB1` are +/// different modes and a bare "CFB" in a diagnostic would be ambiguous. +const MODE: &str = "CFB128"; + +pub(crate) fn aes128_cfb_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_cfb_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_cfb_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Cfb` filled in as the mode. +fn run( + action: &BlockModeAction, + key: &KeyMaterial, + output_hex: bool, +) where + P: BlockPermutation, +{ + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN>(key, output_hex, MODE) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN>(key, output_hex, MODE) + } + } +} diff --git a/cli/src/block_mode_cmd.rs b/cli/src/block_mode_cmd.rs new file mode 100644 index 00000000..bb21795c --- /dev/null +++ b/cli/src/block_mode_cmd.rs @@ -0,0 +1,263 @@ +//! Shared plumbing for the block-cipher-mode subcommands: `aes{128,192,256}-{cbc,cfb}`. +//! +//! Everything here is mode-independent -- key loading, stdin framing, block-alignment enforcement, +//! output formatting -- and is generic over the mode via [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`]. `aes_cbc_cmd` and `aes_cfb_cmd` are thin dispatchers over it, so the +//! two commands cannot drift apart on the parts that matter for correctness. +//! +//! # The IV travels in the ciphertext +//! +//! There is no `--iv` flag, and that is deliberate: `bouncycastle-modes` has no API for a +//! caller-supplied IV, because NIST SP 800-38A Sec 5.3 requires the CBC and CFB IV to be +//! *unpredictable* rather than merely unique. `encrypt` therefore generates one from the OS-backed +//! DRBG and writes it as the **first block of the output**; `decrypt` reads it back from the +//! **first block of the input**. So the two compose directly: +//! +//! ```text +//! bc-rust aes128-cbc encrypt --key-file k.bin < plain.bin > cipher.bin +//! bc-rust aes128-cbc decrypt --key-file k.bin < cipher.bin > plain.bin +//! ``` +//! +//! The IV is not secret (Sec 5.3), so shipping it in the clear is correct. Its *integrity* is not +//! protected, and neither is the ciphertext's -- see the warnings on each subcommand. +//! +//! # Input must be block-aligned +//! +//! Both modes are defined here only on whole blocks (SP 800-38A Sec 5.2), and these commands apply +//! no padding, so input that is not a multiple of 16 bytes is rejected rather than silently padded. +//! Padding is the caller's business; the library offers `bouncycastle-padding` for it, but wiring a +//! padding scheme into the CLI would change the on-the-wire format and is a separate decision. +//! +//! # Binary in, binary out +//! +//! stdin is read as binary so the commands compose in a pipeline. `-x` renders the *output* as hex. +//! For hex input, pipe through `hex-decode` first: +//! +//! ```text +//! cat cipher.hex | bc-rust hex-decode | bc-rust aes256-cbc decrypt --key-file k.bin +//! ``` + +use crate::helpers::write_bytes_or_hex; +use bouncycastle::core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle::core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength}; +use bouncycastle::hex; +use clap::ValueEnum; +use std::io::{Read, Write}; +use std::process::exit; +use std::{fs, io}; + +/// The AES block length in bytes. +pub(crate) const BLOCK_LEN: usize = 16; + +/// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. +/// +/// A full chunk goes through `do_*_out::` in one call, which for decryption means 32 +/// pairs down the mode's two-block path. The at-most-63-block tail at end of input goes one block +/// at a time; it is bounded, so its cost does not scale with the input. +pub(crate) const CHUNK_LEN: usize = 64 * BLOCK_LEN; + +/// Which direction to run. Shared by every mode subcommand. +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum BlockModeAction { + /// Encrypt stdin to stdout. + /// A freshly generated IV is written as the first 16 bytes of the output, so that `decrypt` + /// can read it back. Input length must be a multiple of 16 bytes. + Encrypt, + /// Decrypt stdin to stdout. + /// The first 16 bytes of input are taken as the IV, as written by `encrypt`. The remaining + /// length must be a multiple of 16 bytes. + Decrypt, +} + +/// Loads the key from `--key` (hex) or `--key-file` (binary or hex), and checks its length. +/// +/// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the +/// wrong length is a mistake rather than something to truncate or pad. +pub(crate) fn load_key( + key: &Option, + key_file: &Option, + alg: &str, +) -> KeyMaterial { + let key_bytes: Vec = if let Some(key_file) = key_file { + // A file may hold raw bytes or hex; try hex first, as the other commands do. + let raw = fs::read(key_file).unwrap_or_else(|e| { + eprintln!("Error: couldn't read key file '{key_file}': {e}"); + exit(-1); + }); + match hex::decode(&raw) { + Ok(decoded) => decoded, + Err(_) => raw, + } + } else if let Some(key) = key { + hex::decode(key).unwrap_or_else(|_| { + eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); + exit(-1); + }) + } else { + eprintln!("Error: either `--key` or `--key-file` must be supplied."); + exit(-1); + }; + + if key_bytes.len() != KEY_LEN { + eprintln!("Error: {alg} needs a {KEY_LEN}-byte key, got {} bytes.", key_bytes.len()); + exit(-1); + } + + // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what + // the engine requires -- except for an all-zero key, which it marks Zeroized instead. + let mut key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); + + if key.key_type() != KeyType::SymmetricCipherKey { + // Same stance as `helpers::parse_seed`: warn, then do what was asked. A CLI is used for + // test vectors and scripting, where an all-zero key is a legitimate thing to want. + eprintln!( + "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." + ); + do_hazardous_operations(&mut key, |key| { + key.set_key_type(KeyType::SymmetricCipherKey)?; + key.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) + }) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the key: {e:?}"); + exit(-1); + }); + } + + key +} + +/// Encrypts stdin to stdout under the mode `E`, writing the generated IV first. +/// +/// `mode` names the mode in error messages ("CBC", "CFB128"); it has no effect on the output. +pub(crate) fn encrypt_stream( + key: &KeyMaterial, + output_hex: bool, + mode: &str, +) where + E: BlockCipherEncryptor, +{ + let (mut enc, iv) = E::do_encrypt_init(key).unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + + // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. + write_bytes_or_hex(&iv, output_hex); + + let mut out = [0u8; CHUNK_LEN]; + + stream_aligned(mode, |data| match <&[u8; CHUNK_LEN]>::try_from(data) { + Ok(chunk) => { + // Cannot fail: the mode's block methods are infallible for a constructed value. + enc.do_encrypt_out(chunk, &mut out).unwrap(); + write_bytes_or_hex(&out, output_hex); + } + Err(_) => { + // The bounded tail at end of input: whole blocks, fewer than a chunk. + for block in data.as_chunks::().0 { + write_bytes_or_hex(&enc.do_encrypt(block).unwrap(), output_hex); + } + } + }); + + finish(output_hex); +} + +/// Decrypts stdin to stdout under the mode `D`, taking the IV from the first block of input. +pub(crate) fn decrypt_stream( + key: &KeyMaterial, + output_hex: bool, + mode: &str, +) where + D: BlockCipherDecryptor, +{ + // The leading block is the IV, not ciphertext. + let mut iv = [0u8; BLOCK_LEN]; + if let Err(e) = io::stdin().read_exact(&mut iv) { + eprintln!( + "Error: input too short to contain the {BLOCK_LEN}-byte IV that `encrypt` writes \ + as its first block ({e})." + ); + exit(-1); + } + + let mut dec = D::do_decrypt_init(key, &iv).unwrap_or_else(|e| { + eprintln!("Error: couldn't start decryption: {e:?}"); + exit(-1); + }); + + let mut out = [0u8; CHUNK_LEN]; + + stream_aligned(mode, |data| match <&[u8; CHUNK_LEN]>::try_from(data) { + Ok(chunk) => { + // A full chunk is 32 pairs, so this is the mode's two-block path. + dec.do_decrypt_out(chunk, &mut out).unwrap(); + write_bytes_or_hex(&out, output_hex); + } + Err(_) => { + for block in data.as_chunks::().0 { + write_bytes_or_hex(&dec.do_decrypt(block).unwrap(), output_hex); + } + } + }); + + finish(output_hex); +} + +/// Reads stdin and hands it to `process` in block-aligned pieces: a full `CHUNK_LEN` bytes each time +/// one has accumulated, then once more at end of input with whatever whole blocks remain (fewer +/// than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the +/// buffer until it is full -- so a block split across two reads needs no special handling. +/// +/// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because neither mode is +/// defined on a partial block and these commands do not pad. +fn stream_aligned(mode: &str, mut process: impl FnMut(&[u8])) { + let mut buf = [0u8; CHUNK_LEN]; + let mut filled = 0usize; + + loop { + let n = io::stdin().read(&mut buf[filled..]).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }); + if n == 0 { + break; + } + filled += n; + if filled == CHUNK_LEN { + process(&buf); + filled = 0; + } + } + + if filled % BLOCK_LEN != 0 { + eprintln!( + "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ + {mode} is defined only on whole blocks (SP 800-38A Sec 5.2), and these commands apply \ + no padding, so the input must be padded by the caller.", + filled % BLOCK_LEN + ); + exit(-1); + } + if filled != 0 { + process(&buf[..filled]); + } +} + +/// Flushes stdout, and adds the trailing newline the hex-output commands all emit. +fn finish(output_hex: bool) { + if output_hex { + println!(); + } + io::stdout().flush().unwrap_or_else(|e| { + eprintln!("Error: failed to flush stdout: {e}"); + exit(-1); + }); +} diff --git a/cli/src/main.rs b/cli/src/main.rs index b7ade957..a2fdc8b9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,4 +1,6 @@ mod aes_cbc_cmd; +mod aes_cfb_cmd; +mod block_mode_cmd; mod encoders_cmd; mod helpers; mod hkdf_cmd; @@ -9,7 +11,7 @@ mod rng_cmd; mod sha2_cmd; mod sha3_cmd; -use crate::aes_cbc_cmd::AESCBCAction; +use crate::block_mode_cmd::BlockModeAction; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; use clap::{Parser, Subcommand}; @@ -280,7 +282,7 @@ enum Subcommands { /// compose directly in a pipeline. There is deliberately no `--iv` flag. /// /// Input must be a whole number of 16-byte blocks: CBC is defined only on whole blocks and - /// this build has no padding layer, so unaligned input is rejected rather than padded. + /// these commands apply no padding, so unaligned input is rejected rather than padded. /// /// WARNING: CBC provides confidentiality only. It does not detect tampering, and neither the /// ciphertext nor the IV is authenticated. Do not decrypt data you have not authenticated @@ -289,7 +291,7 @@ enum Subcommands { /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. AES128_CBC { - action: AESCBCAction, + action: BlockModeAction, /// The 16-byte AES key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -311,7 +313,7 @@ enum Subcommands { /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the /// key length differs. AES192_CBC { - action: AESCBCAction, + action: BlockModeAction, /// The 24-byte AES key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -333,7 +335,88 @@ enum Subcommands { /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the /// key length differs. AES256_CBC { - action: AESCBCAction, + action: BlockModeAction, + + /// The 32-byte AES 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 AES 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// The segment size is the full block, i.e. CFB128. SP 800-38A's 8-bit and 1-bit CFB variants + /// are different modes and are NOT interoperable with this command. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two + /// compose directly in a pipeline. There is deliberately no `--iv` flag. + /// + /// Input must be a whole number of 16-byte blocks: this command is block-aligned and applies + /// no padding, so unaligned input is rejected rather than padded. + /// + /// WARNING: CFB provides confidentiality only. It does not detect tampering, and neither the + /// ciphertext nor the IV is authenticated. Flipping a ciphertext bit flips the same bit of the + /// plaintext in the same block, so tampering is directly exploitable. Do not decrypt data you + /// have not 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. + AES128_CFB { + action: BlockModeAction, + + /// The 16-byte AES 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 16-byte AES 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// See `aes128-cfb` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + AES192_CFB { + action: BlockModeAction, + + /// The 24-byte AES 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 24-byte AES 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// See `aes128-cfb` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + AES256_CFB { + action: BlockModeAction, /// The 32-byte AES key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -652,6 +735,15 @@ fn main() { Some(Subcommands::AES256_CBC { action, key, key_file, x }) => { aes_cbc_cmd::aes256_cbc_cmd(action, key, key_file, *x); } + Some(Subcommands::AES128_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes128_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes192_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes256_cfb_cmd(action, key, key_file, *x); + } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem512_cmd(action, skfile, pkfile, ctfile, *x); } diff --git a/cli/tests/aes_cfb_cli_tests.rs b/cli/tests/aes_cfb_cli_tests.rs new file mode 100644 index 00000000..4637c0b9 --- /dev/null +++ b/cli/tests/aes_cfb_cli_tests.rs @@ -0,0 +1,444 @@ +//! Tests for the `aes128-cfb` / `aes192-cfb` / `aes256-cfb` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- the IV riding in the first block, block-alignment +//! enforcement, exit codes, key loading -- none of which is reachable from the library API. +//! +//! The commands share all of that plumbing with `aes*-cbc` (`cli/src/block_mode_cmd.rs`), so this +//! file deliberately repeats the CBC suite's coverage rather than assuming it: the shared code is +//! generic over the mode, and a wiring mistake in the CFB dispatcher would not show up in the CBC +//! tests. What is *not* shared, and is tested only here, is the F.3 vectors, the CFB-specific +//! Appendix D error propagation, and the guard that CFB and CBC ciphertexts are not interchangeable. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::Write; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// SP 800-38A Appendix F IV, shared by every F.3 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four SP 800-38A Appendix F plaintext blocks. +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +); + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// F.3.13 CFB128-AES128.Encrypt ciphertext. +const CT_128: &str = concat!( + "3b3fd92eb72dad20333449f8e83cfb4a", + "c8a64537a0b3a93fcde3cdad9f1ce58b", + "26751f67a3cbb140b1808cf187a4f4df", + "c04b05357c5d1c0eeac4c66f9ff7f2e6", +); +/// F.3.15 CFB128-AES192.Encrypt ciphertext. +const CT_192: &str = concat!( + "cdc80d6fddf18cab34c25909c99a4174", + "67ce7f7f81173621961a2b70171d3d7a", + "2e1e8a1dd59b88b1c8e60fed1efac4c9", + "c05f9f9ca9834fa042ae8fba584b09ff", +); +/// F.3.17 CFB128-AES256.Encrypt ciphertext. +const CT_256: &str = concat!( + "dc7e84bfda79164b7ecd8486985d3860", + "39ffed143b28b1c832113c6331e5407b", + "df10132415e54b92a13ed0a8267ae2f9", + "75a385741ab9cef82031623d55b1e471", +); + +/// F.2.1 CBC-AES128.Encrypt ciphertext, for the cross-mode guard. +const CBC_CT_128: &str = concat!( + "7649abac8119b246cee98e9b12e9197d", + "5086cb9b507219ee95db113a917678b2", + "73bed6b8e3c1743b7116e69e22229516", + "3ff1caa1681fac09120eca307586e1a7", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + child + .stdin + .as_mut() + .expect("stdin piped") + .write_all(stdin_bytes) + .expect("failed to write to stdin"); + + child.wait_with_output().expect("failed to wait for bc-rust") +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- the SP 800-38A F.3 vectors, through the CLI ----------------------------------------- + +/// `decrypt` reproduces the spec plaintext when handed the spec's IV followed by the spec's +/// ciphertext, for F.3.13/F.3.15/F.3.17 (CFB128-AES128/192/256). +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it cannot be +/// asked to reproduce a published ciphertext. `encrypt` is covered by the round-trip tests below +/// and, at the library level, by `crypto/modes/tests/sp800_38a_cfb_tests.rs`. +#[test] +fn decrypt_matches_sp800_38a_f3_vectors() { + for (cmd, key, ct) in [ + ("aes128-cfb", KEY_128, CT_128), + ("aes192-cfb", KEY_192, CT_192), + ("aes256-cfb", KEY_256, CT_256), + ] { + // The CLI expects the IV as the first block of its input, which is exactly how `encrypt` + // emits it. + let input = unhex(&format!("{IV}{ct}")); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!( + tohex(&out), + PLAINTEXT, + "{cmd} decrypt should reproduce the Appendix F.3 plaintext" + ); + } +} + +/// The same, with `-x`, which should give the identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_128}")); + let binary = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + let hex_out = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128, "-x"], &input); + + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + assert_eq!(hex_str.trim_end(), tohex(&binary)); + assert_eq!(hex_str.trim_end(), PLAINTEXT); +} + +// ---- round trips ------------------------------------------------------------------------ + +/// `encrypt | decrypt` recovers the input, for all three key lengths. +/// +/// Also checks the output length: the ciphertext is one block longer than the plaintext, because +/// the IV is prepended. +#[test] +fn encrypt_then_decrypt_round_trips() { + for (cmd, key) in [("aes128-cfb", KEY_128), ("aes192-cfb", KEY_192), ("aes256-cfb", KEY_256)] { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len() + 16, + "{cmd}: output should be the 16-byte IV plus the ciphertext" + ); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// Round trips at sizes that straddle the 1 KiB streaming chunk and the block boundary. +/// +/// 1024 is exactly one chunk; 1040 is a chunk plus one block, which exercises the tail path; 4112 +/// is four chunks plus a block; 65536 is many chunks. +#[test] +fn round_trips_across_chunk_boundaries() { + for size in [16usize, 32, 1024, 1040, 4096, 4112, 65536] { + let plaintext = pseudo_random(size, size as u32); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{size} bytes should round trip"); + } +} + +/// A fresh IV per invocation, so the same plaintext under the same key gives different output. +/// +/// This matters even more for CFB than for CBC: CFB XORs a keystream, so a repeated key-and-IV pair +/// leaks the XOR of the two plaintexts outright, not merely whether blocks were equal. +#[test] +fn each_invocation_uses_a_fresh_iv() { + let plaintext = unhex(PLAINTEXT); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + let iv = ciphertext[..16].to_vec(); + assert!(seen.insert(iv), "the CLI reused an IV across invocations"); + // ...and the body differs too, not just the IV. + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +// ---- key handling ----------------------------------------------------------------------- + +/// `--key-file` accepts both a hex file and a raw binary file, and agrees with `--key`. +#[test] +fn key_file_accepts_hex_and_binary() { + let dir = std::env::temp_dir().join(format!("bc_rust_cfb_cli_key_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let hex_path = dir.join("key.hex"); + let bin_path = dir.join("key.bin"); + std::fs::write(&hex_path, KEY_128).expect("write hex key"); + std::fs::write(&bin_path, unhex(KEY_128)).expect("write binary key"); + + let input = unhex(&format!("{IV}{CT_128}")); + let expected = unhex(PLAINTEXT); + + for path in [&hex_path, &bin_path] { + let out = run_ok(&["aes128-cfb", "decrypt", "--key-file", path.to_str().unwrap()], &input); + assert_eq!(out, expected, "--key-file {path:?}"); + } + + std::fs::remove_dir_all(&dir).ok(); +} + +/// A key of the wrong length for the chosen variant is rejected, naming both lengths. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let stderr = run_err(&["aes256-cfb", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + assert!(stderr.contains("32-byte key"), "stderr should name the expected length: {stderr}"); + assert!(stderr.contains("16 bytes"), "stderr should name the supplied length: {stderr}"); +} + +/// Omitting the key entirely is an error, not a default. +#[test] +fn a_missing_key_is_rejected() { + let stderr = run_err(&["aes128-cfb", "encrypt"], &unhex(PLAINTEXT)); + assert!(stderr.contains("--key"), "stderr should mention the key options: {stderr}"); +} + +/// An all-zero key warns but proceeds, matching `helpers::parse_seed`'s stance. NIST publishes +/// all-zero-key vectors, so refusing outright would make some of them untestable from the CLI. +#[test] +fn an_all_zero_key_warns_but_proceeds() { + let zero_key = "0".repeat(32); + let out = run(&["aes128-cfb", "encrypt", "--key", &zero_key], &unhex(PLAINTEXT)); + assert!(out.status.success(), "an all-zero key should still work"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.to_lowercase().contains("warning"), "an all-zero key should warn: {stderr}"); + assert_eq!(out.stdout.len(), 16 + 64, "IV plus four ciphertext blocks"); +} + +// ---- block alignment and framing -------------------------------------------------------- + +/// Input that is not a whole number of blocks is rejected, with a message that explains why rather +/// than just failing. These commands are the `s = b` CFB variant, so they need whole blocks and +/// they do not pad. +#[test] +fn unaligned_input_is_rejected_with_an_explanation() { + for extra in [1usize, 7, 15] { + let plaintext = pseudo_random(32 + extra, extra as u32); + let stderr = run_err(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); + assert!( + stderr.contains("padding"), + "stderr should point at padding being the caller's job: {stderr}" + ); + assert!(stderr.contains("CFB128"), "stderr should name the mode: {stderr}"); + } +} + +/// Decrypt input shorter than the IV it must start with is rejected, and says so. +#[test] +fn decrypt_input_shorter_than_the_iv_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err(&["aes128-cfb", "decrypt", "--key", KEY_128], &pseudo_random(len, 1)); + assert!( + stderr.contains("IV"), + "stderr should explain the missing IV (len {len}): {stderr}" + ); + } +} + +/// Decrypt input that carries the IV but then an unaligned body is rejected too. +#[test] +fn decrypt_rejects_an_unaligned_body() { + let mut input = unhex(IV); + input.extend_from_slice(&pseudo_random(20, 3)); // 20 is not a multiple of 16 + let stderr = run_err(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); +} + +/// Empty input to `encrypt` produces just the IV: zero blocks in, zero blocks out. +/// +/// Worth pinning because it is the one input length that is block-aligned but has no blocks, and +/// it is easy for a streaming loop to mishandle. +#[test] +fn empty_input_produces_only_the_iv() { + let out = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &[]); + assert_eq!(out.len(), 16, "empty input should yield exactly the IV"); + + // ...and feeding that straight back gives empty output. + let back = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &out); + assert!(back.is_empty(), "decrypting an IV with no body should give nothing"); +} + +// ---- SP 800-38A Appendix D, through the CLI ---------------------------------------------- + +/// Appendix D, Table D.2 for CFB: a bit error in `Cj` gives "SBE in the decryption of `Cj`" -- +/// **specific** bit errors, i.e. the very same bit position -- plus random bit errors in `Cj+1`, +/// and nothing beyond that (with `s = b`, `b/s` is 1). +/// +/// This is the property that makes CFB tampering directly exploitable, which is why the subcommand +/// help warns about it, and it is also a sharp end-to-end check that the CLI is running CFB rather +/// than CBC: under CBC the controlled flip would land in `Pj+1`, not `Pj`. +#[test] +fn a_ciphertext_bit_flip_flips_the_same_plaintext_bit() { + let plaintext = unhex(PLAINTEXT); + let mut input = unhex(&format!("{IV}{CT_128}")); + + // Byte 3 of the second ciphertext block. Input layout is IV | C1 | C2 | C3 | C4, so C2 starts + // at offset 32. + const OFFSET: usize = 32 + 3; + const MASK: u8 = 0b0010_0000; + input[OFFSET] ^= MASK; + + let out = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + assert_eq!(out.len(), 64); + + assert_eq!(&out[0..16], &plaintext[0..16], "P1 depends only on the IV, so it is unaffected"); + + let mut expected_p2 = plaintext[16..32].to_vec(); + expected_p2[3] ^= MASK; + assert_eq!(&out[16..32], &expected_p2[..], "P2 should show exactly the flipped bit"); + + assert_ne!(&out[32..48], &plaintext[32..48], "P3 is randomised: C2 feeds the next cipher call"); + assert_eq!( + &out[48..64], + &plaintext[48..64], + "P4 is unaffected: with s = b, damage stops at P3" + ); +} + +// ---- cross-variant and cross-mode behaviour --------------------------------------------- + +/// Decrypting with a different key length than was used to encrypt cannot succeed silently. +#[test] +fn the_three_variants_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + + // Right length, wrong key: decryption "succeeds" but must not recover the plaintext. CFB is + // unauthenticated, so garbage out is the expected behaviour, not an error -- which is exactly + // why the crate docs insist on authenticating separately. + let wrong_key = "ff".repeat(16); + let out = run_ok(&["aes128-cfb", "decrypt", "--key", &wrong_key], &ciphertext); + assert_ne!(out, plaintext, "a wrong key must not recover the plaintext"); + assert_eq!(out.len(), plaintext.len(), "but the length is unchanged: CFB is unauthenticated"); +} + +/// CFB and CBC ciphertexts are not interchangeable, in either direction. +/// +/// The two commands take the same arguments and produce the same-shaped output, so nothing but this +/// stops a caller pairing them up by mistake. Both spec ciphertexts are for the same key, IV and +/// plaintext, so this is a clean comparison: each mode must reproduce the plaintext only from its +/// own ciphertext. +#[test] +fn cfb_and_cbc_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let cfb_input = unhex(&format!("{IV}{CT_128}")); + let cbc_input = unhex(&format!("{IV}{CBC_CT_128}")); + + // Each mode with its own ciphertext: correct. + assert_eq!(run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &cfb_input), plaintext); + assert_eq!(run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &cbc_input), plaintext); + + // Each mode with the other's ciphertext: wrong, but silently so -- neither mode is + // authenticated, so there is nothing to detect the mismatch. + let cfb_reads_cbc = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &cbc_input); + assert_ne!(cfb_reads_cbc, plaintext, "CFB must not decrypt a CBC ciphertext"); + + let cbc_reads_cfb = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &cfb_input); + assert_ne!(cbc_reads_cfb, plaintext, "CBC must not decrypt a CFB ciphertext"); +} + +// ---- discoverability -------------------------------------------------------------------- + +/// The subcommands appear in `--help`, so they are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aes128-cfb", "aes192-cfb", "aes256-cfb"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} + +/// Each subcommand's own help names the two actions, the IV convention, and -- because `CFB8` and +/// `CFB1` are different, non-interoperable modes -- the segment size. +#[test] +fn per_command_help_documents_the_iv_convention_and_the_segment_size() { + let out = run_ok(&["aes128-cfb", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.contains("encrypt"), "help should list the encrypt action"); + assert!(help.contains("decrypt"), "help should list the decrypt action"); + assert!( + help.contains("FIRST 16 BYTES") || help.contains("first 16 bytes"), + "help should explain where the IV goes: {help}" + ); + assert!(help.contains("CFB128"), "help should say which CFB variant this is: {help}"); +} diff --git a/crypto/aes-lowmemory/src/cfb.rs b/crypto/aes-lowmemory/src/cfb.rs new file mode 100644 index 00000000..aa2c0075 --- /dev/null +++ b/crypto/aes-lowmemory/src/cfb.rs @@ -0,0 +1,84 @@ +//! Type aliases for AES in CFB mode (NIST SP 800-38A Sec 6.3). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. These aliases pin the AES values so +//! callers never spell them out. They add nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! The segment size is the full block, so these are **CFB128**. SP 800-38A's `s = 8` and `s = 1` +//! variants are not block-aligned and are not implemented; see the `bouncycastle_modes::Cfb` docs. + +use crate::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_modes::Cfb; + +/// AES-128 in CFB128 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// The IV is generated by encryption and returned alongside the ciphertext; it is never supplied. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// // 48 bytes: three whole blocks. The length is checked at compile time. +/// let message = [0u8; 48]; +/// let (iv, ciphertext) = AES_CFB_128::::encrypt(&key, &message).unwrap(); +/// assert_eq!(AES_CFB_128::::decrypt(&key, &iv, &ciphertext).unwrap(), message); +/// +/// // Streaming, a few blocks at a time: +/// let (mut enc, iv) = AES_CFB_128::::do_encrypt_init(&key).unwrap(); +/// let first = enc.do_encrypt(&[0u8; 16]).unwrap(); +/// let rest = enc.do_encrypt(&[1u8; 32]).unwrap(); +/// let mut dec = AES_CFB_128::::do_decrypt_init(&key, &iv).unwrap(); +/// assert_eq!(dec.do_decrypt(&first).unwrap(), [0u8; 16]); +/// assert_eq!(dec.do_decrypt(&rest).unwrap(), [1u8; 32]); +/// ``` +/// +/// A length that is not a whole number of blocks is a **compile** error, not a runtime one: +/// +/// ```compile_fail +/// use bouncycastle_aes_lowmemory::AES_CFB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. +/// let _ = AES_CFB_128::::encrypt(&key, &[0u8; 47]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_128 = Cfb; + +/// AES-192 in CFB128 mode. See [`AES_CFB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let (iv, ct) = AES_CFB_192::::encrypt(&key, &[0u8; 32]).unwrap(); +/// assert_eq!(AES_CFB_192::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_192 = Cfb; + +/// AES-256 in CFB128 mode. See [`AES_CFB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let (iv, ct) = AES_CFB_256::::encrypt(&key, &[0u8; 32]).unwrap(); +/// assert_eq!(AES_CFB_256::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_256 = Cfb; diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs index c7ede6c5..8a1f6786 100644 --- a/crypto/aes-lowmemory/src/lib.rs +++ b/crypto/aes-lowmemory/src/lib.rs @@ -56,11 +56,14 @@ //! assert_eq!(blocks, [[0u8; 16], [1u8; 16]]); //! ``` //! -//! ## CBC mode +//! ## Modes of operation //! //! To encrypt more than one block, use a mode of operation from `bouncycastle-modes`. This crate -//! provides [`AES_CBC_128`], [`AES_CBC_192`] and [`AES_CBC_256`] as aliases that fill in the const -//! parameters, with the direction left as the type parameter: +//! provides aliases that fill in the const parameters, with the direction left as the type +//! parameter: [`AES_CBC_128`], [`AES_CBC_192`] and [`AES_CBC_256`] for CBC (SP 800-38A Sec 6.2), +//! and [`AES_CFB_128`], [`AES_CFB_192`] and [`AES_CFB_256`] for CFB128 (Sec 6.3). The two are +//! interchangeable at the call site -- swap `AES_CBC_256` for `AES_CFB_256` in the example below +//! and nothing else changes: //! //! ``` //! use bouncycastle_aes_lowmemory::AES_CBC_256; @@ -193,6 +196,7 @@ mod aes; mod bitslice; mod cbc; +mod cfb; mod round; mod sbox; mod schedule; @@ -200,4 +204,5 @@ mod schedule; pub use aes::{Aes, Aes128, Aes192, Aes256, BLOCK_LEN}; pub use bitslice::Block; pub use cbc::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +pub use cfb::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; pub use schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams}; diff --git a/crypto/modes/Cargo.toml b/crypto/modes/Cargo.toml index 1aca516f..bc020c7f 100644 --- a/crypto/modes/Cargo.toml +++ b/crypto/modes/Cargo.toml @@ -12,6 +12,8 @@ bouncycastle-rng.workspace = true bouncycastle-aes-lowmemory.workspace = true bouncycastle-core-test-framework.workspace = true bouncycastle-hex.workspace = true +# Only to prove the modes compose with the padding layer for arbitrary-length data; no runtime dep. +bouncycastle-padding.workspace = true criterion.workspace = true serde_json = "1.0" diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index e7ea635e..1df62105 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -1,18 +1,25 @@ //! Criterion benchmarks for the modes. //! -//! The number to watch is the **decrypt/encrypt throughput ratio at N >= 2**. CBC encryption is -//! serial by construction (SP 800-38A Sec 6.2: each forward cipher input depends on the previous -//! output), so it can only ever use the single-block path. CBC *decryption* is parallel, and this -//! implementation hands blocks to `decrypt_blocks2` in pairs. With the bit-sliced AES, whose -//! two-block path costs barely more than one block, decryption should therefore run at roughly -//! twice the throughput of encryption. That gap is the entire justification for the pair methods -//! on `BlockPermutation`, so if it disappears, something has stopped taking the pair path. +//! The number to watch is the **decrypt/encrypt throughput ratio at N >= 2**. Encryption in both +//! CBC and CFB is serial by construction (SP 800-38A Sec 6.2 and Sec 6.3: each forward cipher input +//! depends on the previous output), so it can only ever use the single-block path. *Decryption* in +//! both is parallel, and this implementation hands blocks to the permutation's pair method -- for +//! CBC that is `decrypt_blocks2`, for CFB it is `encrypt_blocks2`, since CFB uses the forward +//! function in both directions. With the bit-sliced AES, whose two-block path costs barely more +//! than one block, decryption should therefore run at roughly twice the throughput of encryption. +//! That gap is the entire justification for the pair methods on `BlockPermutation`, so if it +//! disappears, something has stopped taking the pair path. //! //! `N = 1` is included to show the effect vanishing: with one block there is no pair to form, so //! decryption falls back to the single-block path and the ratio should be about 1. //! //! The cipher works in place, so each measurement runs on a fresh copy of the data made in //! criterion's untimed setup (`iter_batched`); the copy is not part of the timing. +//! +//! The `modes::cbc::Aes128` and `modes::cfb::Aes128` groups are directly comparable -- same cipher, +//! same data, same call granularity -- so the difference between them is the cost of the mode. CFB +//! never calls the inverse cipher, so on an engine whose inverse is slower than its forward +//! direction, CFB decryption is expected to come out ahead of CBC decryption. use bouncycastle_aes_lowmemory::{Aes128, Aes256}; use bouncycastle_core::errors::SymmetricCipherError; @@ -20,7 +27,7 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{ Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, }; -use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use bouncycastle_modes::{Cbc, Cfb, Decrypting, Encrypting}; use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -31,6 +38,8 @@ const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; type Aes128Cbc = Cbc; type Aes256Cbc = Cbc; +type Aes128Cfb = Cfb; +type Aes256Cfb = Cfb; /// AES-128 with the pair methods **not** overridden, so they fall back to the trait defaults of /// two single-block calls. @@ -63,6 +72,7 @@ impl BlockPermutation<16, BLOCK_LEN> for UnpairedAes128 { } type UnpairedAes128Cbc = Cbc; +type UnpairedAes128Cfb = Cfb; fn key() -> KeyMaterial { let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); @@ -273,6 +283,154 @@ fn bench_aes256(c: &mut Criterion) { group.finish(); } +fn bench_cfb_aes128(c: &mut Criterion) { + let k = key::<16>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cfb::Aes128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- + group.bench_function("16KiB encrypt -- N=1", |b| { + b.iter(|| { + let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + for block in blocks.iter() { + black_box(enc.do_encrypt(block).unwrap()); + } + }) + }); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter(|| { + let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + for chunk in blocks.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(enc.do_encrypt(arr).unwrap()); + } + }) + }); + + // ---- decryption: parallel, and uses `encrypt_blocks2` -- the FORWARD pair method ---- + let (mut enc, iv) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks + .chunks_exact(8) + .flat_map(|chunk| { + let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + let mut out = [[0u8; BLOCK_LEN]; 8]; + enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); + out + }) + .collect(); + + // N=1 never forms a pair, so this is the single-block path: the ratio against encrypt should + // be about 1. + group.bench_function("16KiB decrypt -- N=1 (no pairing)", |b| { + b.iter(|| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for block in ciphertext.iter() { + black_box(dec.do_decrypt(block).unwrap()); + } + }) + }); + + // N=2 and N=8 are all pairs, so every block goes through encrypt_blocks2. + group.bench_function("16KiB decrypt -- N=2 (all pairs)", |b| { + b.iter(|| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(2) { + let arr: &[u8; 2 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { + b.iter(|| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + // N=9 is four pairs plus a one-block remainder, so it exercises the tail path too. + group.bench_function("16KiB decrypt -- N=9 (pairs + remainder)", |b| { + b.iter(|| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(9) { + let arr: &[u8; 9 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + // The controlled comparison: identical N, identical cipher, pair methods overridden vs not. + // This pair of numbers -- and only this pair -- measures what `encrypt_blocks2` buys CFB. + group.bench_function("16KiB decrypt -- N=8, pair path (blocks2 overridden)", |b| { + b.iter(|| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + group.bench_function("16KiB decrypt -- N=8, no pair path (trait default)", |b| { + b.iter(|| { + let mut dec = UnpairedAes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + group.finish(); +} + +fn bench_cfb_aes256(c: &mut Criterion) { + let k = key::<32>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cfb::Aes256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter(|| { + let (mut enc, _) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); + for chunk in blocks.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(enc.do_encrypt(arr).unwrap()); + } + }) + }); + + let (mut enc, iv) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); + let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks + .chunks_exact(8) + .flat_map(|chunk| { + let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + let mut out = [[0u8; BLOCK_LEN]; 8]; + enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); + out + }) + .collect(); + + group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { + b.iter(|| { + let mut dec = Aes256Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in ciphertext.chunks_exact(8) { + let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); + black_box(dec.do_decrypt(arr).unwrap()); + } + }) + }); + + group.finish(); +} + /// `do_*_init` includes a key expansion, and for encryption also an IV draw from the OS-backed /// DRBG. Worth its own measurement, because for short messages it dominates. fn bench_init(c: &mut Criterion) { @@ -280,7 +438,7 @@ fn bench_init(c: &mut Criterion) { let k256 = key::<32>(); let iv = [0u8; BLOCK_LEN]; - let mut group = c.benchmark_group("modes::cbc::init"); + let mut group = c.benchmark_group("modes::init"); group.bench_function("Aes128 do_encrypt_init (key schedule + IV)", |b| { b.iter(|| black_box(Aes128Cbc::::do_encrypt_init(black_box(&k128)).unwrap().1)) @@ -296,8 +454,22 @@ fn bench_init(c: &mut Criterion) { }) }); + // CFB does exactly the same work here -- one key expansion, plus an IV draw when encrypting -- + // so these should match the CBC numbers. A divergence would mean one mode is doing something + // extra at construction time. + group.bench_function("Aes128 do_encrypt_init, CFB (key schedule + IV)", |b| { + b.iter(|| black_box(Aes128Cfb::::do_encrypt_init(black_box(&k128)).unwrap().1)) + }); + group.bench_function("Aes128 do_decrypt_init, CFB (key schedule only)", |b| { + b.iter(|| { + black_box(Aes128Cfb::::do_decrypt_init(black_box(&k128), &iv).unwrap()) + }) + }); + group.finish(); } -criterion_group!(benches, bench_aes128, bench_aes256, bench_init); +criterion_group!( + benches, bench_aes128, bench_aes256, bench_cfb_aes128, bench_cfb_aes256, bench_init +); criterion_main!(benches); diff --git a/crypto/modes/src/cfb.rs b/crypto/modes/src/cfb.rs new file mode 100644 index 00000000..7c7f24a4 --- /dev/null +++ b/crypto/modes/src/cfb.rs @@ -0,0 +1,279 @@ +//! The Cipher Feedback mode of operation (NIST SP 800-38A Sec 6.3), full-block segment only. +//! +//! # The specification +//! +//! Sec 6.3 defines CFB against a segment size `s` with `1 <= s <= b`, where `b` is the block size. +//! Quoting the equations verbatim: +//! +//! ```text +//! CFB Encryption: I1 = IV; +//! Ij = LSB_{b-s}(I_{j-1}) | C#_{j-1} for j = 2 ... n; +//! Oj = CIPH_K(Ij) for j = 1, 2 ... n; +//! C#_j = P#_j XOR MSB_s(Oj) for j = 1, 2 ... n. +//! +//! CFB Decryption: I1 = IV; +//! Ij = LSB_{b-s}(I_{j-1}) | C#_{j-1} for j = 2 ... n; +//! Oj = CIPH_K(Ij) for j = 1, 2 ... n; +//! P#_j = C#_j XOR MSB_s(Oj) for j = 1, 2 ... n. +//! ``` +//! +//! # This type is the `s = b` specialisation +//! +//! [`Cfb`] implements **only** `s = b`, the variant Sec 6.3 says is "sometimes incorporated into +//! the name of the mode", i.e. CFB128 for a 128-bit block. That is the only segment size which is +//! block-aligned, and so the only one that fits [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`]. Substituting `s = b` collapses the equations exactly: +//! +//! * `LSB_{b-s}(I_{j-1})` becomes `LSB_0(I_{j-1})`, the empty bit string, so the concatenation +//! leaves `Ij = C_{j-1}`. Sec 6.3's alternative description agrees: the previous input block +//! "circularly shift[s] s positions to the left, and then the ciphertext segment replaces the s +//! least significant bits of the result" -- shifting a whole block by its own width and replacing +//! every bit of it is just assignment. +//! * `MSB_s(Oj)` becomes `MSB_b(Oj)`, which is `Oj`. No part of the output block is discarded, so +//! there are no wasted cipher calls: one forward cipher per block, the same as CBC. +//! +//! leaving +//! +//! ```text +//! I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj / Pj = Cj XOR Oj +//! ``` +//! +//! As in `Cbc`, the `j = 1` and `j >= 2` cases differ only in what gets fed to the cipher, so a +//! single `chain` field holds `Ij` -- the IV to start with, then each ciphertext block as it is +//! produced or consumed. That is why no code below special-cases the first block. +//! +//! CFB1 and CFB8 (the `s = 1` and `s = 8` variants, which SP 800-38A Appendix F.3 also gives +//! vectors for) are deliberately **not** here: they are not block-aligned, so they belong to a +//! `StreamCipher`-shaped API rather than this one. +//! +//! # Decryption uses the *forward* cipher function +//! +//! This is the thing about CFB that surprises a reader used to CBC: both directions apply +//! `CIPH_K`. Sec 6.3 is explicit -- "In CFB decryption, the IV is the first input block, and each +//! successive input block is formed as in CFB encryption [...] The *forward cipher* function is +//! applied to each input block to produce the output blocks." +//! +//! So [`Cfb`](Cfb) never calls [`BlockPermutation::decrypt_block`] or +//! [`BlockPermutation::decrypt_blocks2`]. A permutation could implement only the forward direction +//! and still work here; `cfb_tests.rs` pins that with a toy whose inverse panics. The mode XORs a +//! keystream in both directions, and the two directions differ only in which of the two buffers +//! becomes the next chaining value. +//! +//! # Parallel decryption +//! +//! Sec 6.3: "In CFB encryption, like CBC encryption, the input block to each forward cipher +//! function (except the first) depends on the result of the previous forward cipher function; +//! therefore, multiple forward cipher operations cannot be performed in parallel. In CFB +//! decryption, the required forward cipher operations can be performed in parallel if the input +//! blocks are first constructed (in series) from the IV and the ciphertext." +//! +//! Constructing them "in series" is trivial here: with `s = b` the input blocks *are* the IV +//! followed by the ciphertext blocks, already in hand. Decryption therefore walks the ciphertext in +//! pairs through [`BlockPermutation::encrypt_blocks2`], which a bit-sliced engine computes for +//! barely more than the cost of one block. Encryption cannot, and does not. + +use crate::iv::random_iv; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, + SecurityStrength, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use core::marker::PhantomData; + +/// CFB mode over any [`BlockPermutation`], with the direction encoded in the type. +/// +/// The segment size is the full block (`s = b`, i.e. CFB128 for AES); see the module docs for why +/// the other segment sizes are out of scope. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the +/// former and [`BlockCipherDecryptor`] only for the latter, so a `Cfb<_, Encrypting, _, _>` has no +/// decryption methods at all -- using one in the wrong direction is a compile error rather than a +/// runtime check. +/// +/// The initialization data is one block, so `INIT_DATA_LEN == BLOCK_LEN`. +/// +/// # State +/// +/// The same two fields as `Cbc`, and the same size: the permutation (which owns the key schedule, +/// and is responsible for keeping it in a zeroize-on-drop wrapper) and one block holding `Ij`. `Ij` +/// is an IV or a ciphertext block, both of which are public, so it is deliberately not wrapped in a +/// `Secret`. +/// +/// Note what is *not* stored: the output block `Oj`. It is recomputed from `chain` on each call and +/// lives only in a local, so no keystream outlives the call that used it. +pub struct Cfb +where + P: BlockPermutation, +{ + perm: P, + /// `Ij`: the IV, then `C_{j-1}`. See the module docs on why there is only one field for both. + chain: [u8; BLOCK_LEN], + _dir: PhantomData, +} + +impl Cfb +where + P: BlockPermutation, +{ + /// `Oj = CIPH_K(Ij)`, the keystream block for the current position. + /// + /// The forward cipher function, in both directions -- see the module docs. + #[inline] + fn keystream(&self) -> [u8; BLOCK_LEN] { + let mut o = self.chain; + self.perm.encrypt_block(&mut o); + o + } + + /// `Cj = Pj XOR Oj`, then `Cj` becomes the next input block. + #[inline] + fn encrypt_one(&mut self, plaintext: &[u8; BLOCK_LEN], ciphertext: &mut [u8; BLOCK_LEN]) { + let o = self.keystream(); + for (out, (p, o)) in ciphertext.iter_mut().zip(plaintext.iter().zip(o.iter())) { + *out = *p ^ *o; + } + // I_{j+1} = Cj. Serial: this is the input to the next cipher call. + self.chain = *ciphertext; + } + + /// `Pj = Cj XOR Oj`, then `Cj` -- the *ciphertext*, not the recovered plaintext -- becomes the + /// next input block. + #[inline] + fn decrypt_one(&mut self, ciphertext: &[u8; BLOCK_LEN], plaintext: &mut [u8; BLOCK_LEN]) { + let o = self.keystream(); + for (out, (c, o)) in plaintext.iter_mut().zip(ciphertext.iter().zip(o.iter())) { + *out = *c ^ *o; + } + // `I_{j+1} = C#_j` of the spec equations: the ciphertext segment is what is fed back. + // Feeding back the plaintext instead would still decrypt the first block correctly and + // nothing after it, which is why `cfb_tests.rs` checks exactly that. + self.chain = *ciphertext; + } + + /// Decrypts two consecutive blocks with one [`BlockPermutation::encrypt_blocks2`] call. + /// + /// Writing the pair as `Cj, Cj+1` with `Ij` the incoming chaining value, the `s = b` equations + /// give + /// + /// ```text + /// Ij = chain Oj = CIPH_K(Ij) Pj = Cj XOR Oj + /// Ij+1 = Cj Oj+1 = CIPH_K(Ij+1) Pj+1 = Cj+1 XOR Oj+1 + /// ``` + /// + /// Both input blocks are known before either cipher call -- `Ij` is already held and `Ij+1` is + /// just `Cj`, which the caller supplied -- so the two forward ciphers are independent and + /// computing them together changes nothing. This is precisely the parallelism Sec 6.3 describes, + /// with the input blocks "first constructed (in series) from the IV and the ciphertext". + #[inline] + fn decrypt_pair( + &mut self, + ciphertext: &[[u8; BLOCK_LEN]; 2], + plaintext: &mut [[u8; BLOCK_LEN]; 2], + ) { + // The two input blocks, constructed in series: Ij (already held) and Ij+1 (= Cj). + let mut o = [self.chain, ciphertext[0]]; + self.perm.encrypt_blocks2(&mut o); + + for ((out, c), o) in plaintext.iter_mut().zip(ciphertext.iter()).zip(o.iter()) { + for ((out, c), o) in out.iter_mut().zip(c.iter()).zip(o.iter()) { + *out = *c ^ *o; + } + } + + // I_{j+2} = Cj+1. + self.chain = ciphertext[1]; + } +} + +impl BlockCipher + for Cfb +where + P: BlockPermutation, +{ + /// A mode does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength =

::MAX_SECURITY_STRENGTH; +} + +impl + BlockCipherEncryptor for Cfb +where + P: BlockPermutation, +{ + /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + /// As [`BlockCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let perm = P::new(key)?; + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) + } + + /// The implementor hook (the flat `do_encrypt[_out]` are provided over it). + /// + /// Strictly serial: `Oj+1 = CIPH_K(Cj)` and `Cj` is the *output* of the previous cipher call, so + /// there is no pair path here. See the module docs. + fn do_encrypt_blocks_out( + &mut self, + plaintext: &[[u8; BLOCK_LEN]; N], + ciphertext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result { + for (p, c) in plaintext.iter().zip(ciphertext.iter_mut()) { + self.encrypt_one(p, c); + } + Ok(N * BLOCK_LEN) + } +} + +impl + BlockCipherDecryptor for Cfb +where + P: BlockPermutation, +{ + /// Begins a decryption flow from the IV returned by + /// [`BlockCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; BLOCK_LEN], + ) -> Result { + let perm = P::new(key)?; + // `I1 = IV`, exactly as on the encrypt side. + Ok(Self { perm, chain: *init_data, _dir: PhantomData }) + } + + /// The implementor hook (the flat `do_decrypt[_out]` are provided over it). + /// + /// Walks the input in pairs so the permutation's two-block *forward* path is used, with an + /// at-most-one block remainder for odd `N`. `as_chunks` splits into exactly that shape with no + /// runtime length check and no indexing arithmetic; `N` is a compile-time constant, so for even + /// `N` the tail loop is empty and for `N = 1` the pair loop is. + fn do_decrypt_blocks_out( + &mut self, + ciphertext: &[[u8; BLOCK_LEN]; N], + plaintext: &mut [[u8; BLOCK_LEN]; N], + ) -> Result { + let (ct_pairs, ct_tail) = ciphertext.as_chunks::<2>(); + let (pt_pairs, pt_tail) = plaintext.as_chunks_mut::<2>(); + + for (ct_pair, pt_pair) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { + self.decrypt_pair(ct_pair, pt_pair); + } + for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { + self.decrypt_one(c, p); + } + + Ok(N * BLOCK_LEN) + } +} diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 0680ee6d..133d08d0 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -2,26 +2,41 @@ //! //! A mode turns a keyed block permutation -- `bouncycastle-aes-lowmemory`'s `Aes128` and friends, //! or anything else implementing [`BlockPermutation`] -- into something that can encrypt more than -//! one block. This crate currently provides **CBC** ([`Cbc`], SP 800-38A Sec 6.2). +//! one block. This crate provides: +//! +//! | Mode | Type | Spec | Notes | +//! |---|---|---|---| +//! | CBC | [`Cbc`] | SP 800-38A Sec 6.2 | Cipher Block Chaining | +//! | CFB | [`Cfb`] | SP 800-38A Sec 6.3 | Cipher Feedback, full-block segment (`s = b`) only | +//! +//! Both are strictly block-aligned and both generate their own IV; they differ only in how the +//! block permutation is wired up, and the two types have identical APIs and identical size. See +//! [Choosing between CBC and CFB](#choosing-between-cbc-and-cfb). //! //! The crate is deliberately cipher-agnostic: it depends on no concrete block cipher, only on the -//! trait. Define a one-line alias for the combination you use: +//! trait. Define a one-line alias for the combination you use -- or use the ready-made +//! `AES_CBC_128` / `AES_CFB_128` and friends from `bouncycastle-aes-lowmemory`: //! //! ``` //! use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; -//! use bouncycastle_modes::Cbc; +//! use bouncycastle_modes::{Cbc, Cfb}; //! //! type Aes128Cbc

= Cbc; //! type Aes192Cbc = Cbc; //! type Aes256Cbc = Cbc; +//! +//! type Aes128Cfb = Cfb; +//! type Aes192Cfb = Cfb; +//! type Aes256Cfb = Cfb; //! ``` //! //! # Usage Examples //! //! The direction is part of the type: [`Cbc`](Cbc) implements //! [`BlockCipherEncryptor`] and nothing else, and [`Cbc`](Cbc) implements -//! [`BlockCipherDecryptor`] and nothing else. The IV is generated for you and returned; there is no -//! API for supplying your own (see [Security Considerations](#security-considerations)). +//! [`BlockCipherDecryptor`] and nothing else. [`Cfb`] is the same. The IV is generated for you and +//! returned; there is no API for supplying your own (see +//! [Security Considerations](#security-considerations)). //! //! ``` //! use bouncycastle_aes_lowmemory::Aes128; @@ -74,6 +89,32 @@ //! assert_eq!(rest, [0xBBu8; 32]); //! ``` //! +//! CFB is a drop-in swap for CBC -- same methods, same IV convention, same block alignment. The +//! only visible difference is the ciphertext: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Cbc, Cfb, Decrypting, Encrypting}; +//! +//! type Aes128Cbc = Cbc; +//! type Aes128Cfb = Cfb; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let plaintext = [0x5Au8; 32]; +//! +//! let (iv, ciphertext) = Aes128Cfb::::encrypt(&key, &plaintext).expect("encryption"); +//! let recovered = Aes128Cfb::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! +//! // The modes are not interchangeable: a ciphertext must be decrypted with the mode that +//! // produced it, and nothing at the type level stops you getting that wrong. +//! let as_if_cbc = Aes128Cbc::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! assert_ne!(as_if_cbc, plaintext); +//! ``` +//! //! Using the wrong direction does not compile: //! //! ```compile_fail @@ -89,16 +130,61 @@ //! let _ = Aes128Cbc::::do_decrypt_init(&key, &[0u8; 16]); //! ``` //! +//! # Choosing between CBC and CFB +//! +//! Neither is authenticated, so the honest answer for new designs is "neither -- use an AEAD". +//! Between the two: +//! +//! * **Error propagation differs**, and it is the sharpest practical difference. SP 800-38A +//! Appendix D, Table D.2: a bit error in `Cj` gives CBC a *randomised* `Pj` plus the **same bit** +//! flipped in `Pj+1`, and gives CFB the **same bit** flipped in `Pj` plus a randomised `Pj+1`. +//! So under CFB an attacker who can flip a ciphertext bit flips the corresponding plaintext bit +//! directly, in the block they targeted. Both are malleable; authenticate the ciphertext. +//! * **CFB needs only the forward cipher function**, in both directions (Sec 6.3). That halves what +//! a permutation has to provide, and where the inverse costs more than the forward direction it +//! makes CFB decryption faster: with `bouncycastle-aes-lowmemory` this crate's benches measure CFB +//! decryption at about 1.37x CBC decryption (AES-128, 16 KiB, `N = 8`). Encryption is the same +//! speed in both, since both are serial and both use only the forward function. +//! * **"CFB" alone is ambiguous.** SP 800-38A's `s = 8` and `s = 1` variants are also called CFB and +//! are *not* interoperable with [`Cfb`], which is `s = b`. If you are matching an existing system, +//! check which segment size it means before assuming this one. CBC has no such ambiguity. +//! * Both encrypt serially and decrypt in parallel, so their scaling with `N` matches. +//! //! # Block alignment //! //! These types are **strictly block-aligned**: whole blocks in, whole blocks out, no finalization //! step. SP 800-38A Sec 5.2 requires exactly that of CBC ("the total number of bits in the -//! plaintext must be a multiple of the block size"), and Appendix A puts the formatting of -//! non-aligned data outside the scope of the recommendation. +//! plaintext must be a multiple of the block size"); for CFB it requires the total to be a multiple +//! of the segment size `s`, and this crate fixes `s = b`, so the requirement is the same. Appendix +//! A puts the formatting of non-aligned data outside the scope of the recommendation. //! -//! Arbitrary-length data therefore needs a padding layer on top. That layer is *not* in this -//! crate, and at the time of writing is not in the workspace at all -- see -//! [Not yet implemented](#not-yet-implemented). +//! Arbitrary-length data therefore needs a padding layer on top. That layer is *not* in this crate: +//! it is `bouncycastle-padding`, whose `PaddedEncryptor` / `PaddedDecryptor` wrap any +//! [`BlockCipherEncryptor`] / [`BlockCipherDecryptor`] pair, so both modes get arbitrary-length +//! support by being wrapped rather than by growing padding logic of their own. +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; +//! use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +//! +//! type Enc = PaddedEncryptor, PKCS7, 16, 16, 16>; +//! type Dec = PaddedDecryptor, PKCS7, 16, 16, 16>; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // 5 bytes: not a whole block, which the bare mode would refuse to compile. +//! let message = b"hello"; +//! let mut ciphertext = [0u8; 16]; +//! let (iv, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); +//! assert_eq!(written, 16); +//! +//! let mut plaintext = [0u8; 16]; +//! let n = Dec::decrypt_out(&key, &iv, &ciphertext, &mut plaintext).expect("decryption"); +//! assert_eq!(&plaintext[..n], message); +//! ``` //! //! # Memory Usage //! @@ -107,31 +193,44 @@ //! //! ```text //! size_of::>() == size_of::

() + BLOCK_LEN +//! size_of::>() == size_of::

() + BLOCK_LEN //! ``` //! //! | Combination | Permutation | Chain | Total | //! |---|---|---|---| -//! | AES-128 CBC | 176 B | 16 B | 192 B | -//! | AES-192 CBC | 208 B | 16 B | 224 B | -//! | AES-256 CBC | 240 B | 16 B | 256 B | +//! | AES-128 CBC or CFB | 176 B | 16 B | 192 B | +//! | AES-192 CBC or CFB | 208 B | 16 B | 224 B | +//! | AES-256 CBC or CFB | 240 B | 16 B | 256 B | +//! +//! CFB is the same size as CBC because it stores the same thing: one block of input to the next +//! cipher call. Its keystream block `Oj` is recomputed per call and lives only in a local, so it +//! costs `BLOCK_LEN` of transient stack and nothing persistent. //! -//! The data methods work in place and add nothing beyond the copy of the two ciphertext blocks -//! `decrypt_pair` keeps for the chaining value. [`Encrypting`] and [`Decrypting`] are zero-sized and held in a +//! The data methods work in place. The pair path in either mode's decryptor adds one +//! `[[u8; BLOCK_LEN]; 2]` copy of the ciphertext it needs for the chaining value. [`Encrypting`] and [`Decrypting`] are zero-sized and held in a //! `PhantomData`, so encoding the direction in the type is free. The table is pinned by -//! `sizes_match_the_documented_memory_table` in `tests/cbc_tests.rs`. +//! `sizes_match_the_documented_memory_table` in `tests/cbc_tests.rs` and `tests/cfb_tests.rs`. //! //! # Security Considerations //! -//! ## CBC is not authenticated +//! ## Neither mode is authenticated +//! +//! Both provide confidentiality only. Neither detects tampering, and both are malleable in +//! specific, exploitable ways -- SP 800-38A Appendix D, Table D.2: +//! +//! * **CBC:** flipping a bit of `Cj` flips the same bit of the decryption of `Cj+1`, and randomises +//! the decryption of `Cj` itself. +//! * **CFB:** flipping a bit of `Cj` flips the same bit of the decryption of `Cj` -- the block the +//! attacker aimed at -- and randomises the decryption of `Cj+1`. So the controlled flip lands in +//! the targeted block rather than the next one. //! -//! CBC provides confidentiality only. It does not detect tampering, and it is malleable in -//! specific, exploitable ways -- SP 800-38A Appendix D: flipping a bit of `Cj` flips the same bit -//! of the decryption of `Cj+1`, and randomises the decryption of `Cj` itself. **Authenticate the -//! ciphertext.** Prefer an AEAD; if you must use CBC, MAC the ciphertext *and* the IV, and verify -//! before decrypting. +//! **Authenticate the ciphertext.** Prefer an AEAD; if you must use either of these, MAC the +//! ciphertext *and* the IV, and verify before decrypting. //! -//! Combining CBC decryption with a padding check is the classic padding-oracle setup. Do not -//! report padding failures distinguishably, and do not decrypt unauthenticated ciphertext. +//! Combining decryption with a padding check is the classic padding-oracle setup, for either mode. +//! Do not report padding failures distinguishably, and do not decrypt unauthenticated ciphertext. +//! `bouncycastle-padding`'s `unpad` is constant-time for exactly this reason, but constant-time +//! unpadding is not a substitute for authentication. //! //! ## The IV must be unpredictable, and this crate generates it //! @@ -149,56 +248,78 @@ //! //! Appendix D: "for the CBC mode, the decryption of the first ciphertext block is vulnerable to the //! (deliberate) introduction of bit errors in specific bit positions of the IV if the integrity of -//! the IV is not protected". A flipped IV bit flips exactly that bit of `P1`. The IV need not be -//! secret, but it must be authenticated along with the ciphertext. +//! the IV is not protected". Under CBC a flipped IV bit flips exactly that bit of `P1`. +//! +//! CFB damages `P1` too, but unpredictably rather than controllably: the IV is the first thing fed +//! to the cipher, so Table D.2 gives *random* bit errors in the decryption of `C1` -- and, because +//! this crate fixes `s = b`, in `C1` only (Appendix D's "the first `i/s` (rounding up) ciphertext +//! segments" is one segment when `s = b`). Later blocks are unaffected in both modes. +//! +//! Either way the IV need not be secret, but it must be authenticated along with the ciphertext. //! //! ## Key and IV reuse //! -//! Nothing here stops one key being used for many messages, which is fine for CBC provided each -//! gets a fresh unpredictable IV. It is the IV, not the key, that must not repeat. +//! Nothing here stops one key being used for many messages, which is fine for either mode provided +//! each gets a fresh unpredictable IV. It is the IV, not the key, that must not repeat. +//! +//! Repeating one matters more for CFB. CFB XORs a keystream, so two messages encrypted under the +//! same key *and* IV satisfy `C1 XOR C1' == P1 XOR P1'` -- the plaintext XOR leaks directly, the +//! classic two-time-pad failure, and it continues into later blocks for as long as the two +//! ciphertexts agree. CBC under a repeated IV leaks only whether the blocks were equal, not their +//! XOR. Since [`BlockCipherEncryptor::do_encrypt_init`] draws every IV from the DRBG, neither case +//! arises through this API; it is a reason not to add an IV-accepting one. //! //! # Not yet implemented //! -//! * **Padding.** There is no `Padding` trait, `PKCS7`, `PaddedEncryptor` or `PaddedDecryptor` in -//! this workspace yet, so arbitrary-length CBC is not available. When that layer lands, CBC gets -//! it for free by being wrapped -- no padding logic belongs in this crate. -//! * **CFB** (SP 800-38A Sec 6.3), and the other three modes of the recommendation (ECB, OFB, CTR). +//! * **The CFB segment sizes below the block size** (`s = 1` and `s = 8`, for which SP 800-38A +//! Appendix F.3 also gives vectors). They are not block-aligned, so they need a +//! `StreamCipher`-shaped API rather than [`BlockCipherEncryptor`]. +//! * **ECB, OFB and CTR**, the other three modes of the recommendation. ECB is a raw permutation +//! applied per block and is not confidential; OFB and CTR are keystream modes and, like CFB1/8, +//! do not require block alignment. //! //! # Command line //! -//! The `bc-rust` CLI exposes CBC as `aes128-cbc`, `aes192-cbc` and `aes256-cbc`, each taking -//! `encrypt` or `decrypt` and streaming stdin to stdout. Because there is no API for a -//! caller-supplied IV, `encrypt` writes the generated IV as the first block of its output and -//! `decrypt` reads it back from the first block of its input, so the two compose: +//! The `bc-rust` CLI exposes both modes for all three AES key lengths: `aes128-cbc`, `aes192-cbc`, +//! `aes256-cbc`, `aes128-cfb`, `aes192-cfb` and `aes256-cfb`, each taking `encrypt` or `decrypt` +//! and streaming stdin to stdout. Because there is no API for a caller-supplied IV, `encrypt` +//! writes the generated IV as the first block of its output and `decrypt` reads it back from the +//! first block of its input, so the two compose: //! //! ```text //! bc-rust aes256-cbc encrypt --key-file k.bin < plain.bin > cipher.bin //! bc-rust aes256-cbc decrypt --key-file k.bin < cipher.bin | cmp - plain.bin +//! +//! bc-rust aes256-cfb encrypt --key-file k.bin < plain.bin > cipher.bin +//! bc-rust aes256-cfb decrypt --key-file k.bin < cipher.bin | cmp - plain.bin //! ``` //! -//! Input must be block-aligned there too, for the reason given above. +//! The `-cfb` commands are CFB128, matching [`Cfb`]. Input must be block-aligned there too, for the +//! reason given above. #![no_std] #![forbid(unsafe_code)] #![forbid(missing_docs)] mod cbc; +mod cfb; mod iv; pub use cbc::Cbc; +pub use cfb::Cfb; // Imports needed for docs #[allow(unused_imports)] use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation}; // end of imports needed for docs -/// Direction marker for a mode that encrypts. See [`Cbc`]. +/// Direction marker for a mode that encrypts. See [`Cbc`] and [`Cfb`]. /// /// Zero-sized: encoding the direction in the type costs no memory. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Encrypting; -/// Direction marker for a mode that decrypts. See [`Cbc`]. +/// Direction marker for a mode that decrypts. See [`Cbc`] and [`Cfb`]. /// /// Zero-sized: encoding the direction in the type costs no memory. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crypto/modes/tests/acvp_cfb_tests.rs b/crypto/modes/tests/acvp_cfb_tests.rs new file mode 100644 index 00000000..39ec1bc9 --- /dev/null +++ b/crypto/modes/tests/acvp_cfb_tests.rs @@ -0,0 +1,305 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CFB128` vectors from the `bc-test-data` repo. +//! +//! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` +//! relative to the root of this git project. If it is absent the test prints a warning and passes, +//! matching the convention used by the ML-KEM, ML-DSA, `aes-lowmemory` and AES-CBC suites -- +//! `cargo test` must stay green for someone who has only cloned this repository. +//! +//! This is the CFB counterpart to `acvp_tests.rs` (AES-CBC) and to +//! `crypto/aes-lowmemory/tests/acvp_tests.rs` (AES-ECB, the raw permutation). The `CFB128` file is +//! the one that matches [`Cfb`]: `ACVP-AES-CFB8` and `ACVP-AES-CFB1` are the sub-block segment +//! sizes this crate does not implement, and are deliberately not read. +//! +//! # Joining the request and response files +//! +//! As with CBC, the response file carries **only the answer** (`ct` for an encrypt group, `pt` for a +//! decrypt group) against a `tcId`. The key, IV and input live in the request file, and the group +//! metadata that says which direction a case is -- `direction` and `keyLen` -- lives only there too. +//! So both files are read and joined on `tcId`. +//! +//! # Coverage +//! +//! 2138 AFT (Algorithm Functional Test) cases across all three key lengths and both directions, +//! including 54 whose payload spans 2 to 10 blocks. Every case is run **twice**: once block by +//! block, and once in pairs with a one-block remainder for odd lengths. The second pass is what puts +//! the multi-block cases through the pair path -- which for CFB is +//! [`BlockPermutation::encrypt_blocks2`], the *forward* function, even on the decrypt side -- so it +//! is exercised against real vectors and not only against the toy in `cfb_tests.rs`. +//! +//! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a +//! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather +//! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports +//! how many it skipped so the gap stays visible. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, SecurityStrength, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +const BLOCK_LEN: usize = 16; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const REQUEST_FILE: &str = "ACVP-AES-CFB128.4014530.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CFB128.4014530.rsp.json"; + +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(REQUEST_FILE).exists() && path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-CFB128 tests will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP set deliberately includes an all-zero key. `KeyMaterial` tags an all-zero buffer as +/// `KeyType::Zeroized` and will not promote it outside a `do_hazardous_operations` closure, which +/// is the right default -- so this opts in explicitly rather than the engine weakening its guard. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the parameter set"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("ACVP key bytes fit the buffer"); + + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a NIST all-zero test key"); + } + key +} + +/// How to walk the blocks of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// One block per call. Never forms a pair. + Single, + /// Two blocks per call, with a one-block remainder for odd lengths. Uses the pair path. + Pairs, +} + +/// Runs one CFB128 case in one direction, for a given permutation, under the given grouping. +/// +/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's +/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- +/// so a change that ignored the RNG could not pass silently. +fn run_case( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> +where + P: BlockPermutation, +{ + let key = cipher_key::(key_bytes); + let mut out: Vec<[u8; BLOCK_LEN]> = Vec::with_capacity(input.len()); + + if encrypt { + let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); + + match grouping { + Grouping::Single => { + for block in input { + out.push(enc.do_encrypt(block).unwrap()); + } + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut c = [[0u8; BLOCK_LEN]; 2]; + enc.do_encrypt_blocks_out(pair, &mut c).unwrap(); + out.extend_from_slice(&c); + } + for block in tail { + out.push(enc.do_encrypt(block).unwrap()); + } + } + } + } else { + let mut dec = + Cfb::::do_decrypt_init(&key, &iv).expect("dec init"); + + match grouping { + Grouping::Single => { + for block in input { + out.push(dec.do_decrypt(block).unwrap()); + } + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut p = [[0u8; BLOCK_LEN]; 2]; + dec.do_decrypt_blocks_out(pair, &mut p).unwrap(); + out.extend_from_slice(&p); + } + for block in tail { + out.push(dec.do_decrypt(block).unwrap()); + } + } + } + } + + out +} + +/// Dispatches on key length, which is what selects the AES parameter set. +fn run_case_for_key_len( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> { + match key_bytes.len() { + 16 => run_case::(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } +} + +fn to_blocks(bytes: &[u8]) -> Vec<[u8; BLOCK_LEN]> { + assert_eq!(bytes.len() % BLOCK_LEN, 0, "ACVP CFB128 payloads are block-aligned"); + bytes.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect() +} + +fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +#[test] +fn acvp_aes_cfb128_known_answer_tests() { + let Some(dir) = test_data_dir() else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + // The response file carries only the answer, against a tcId. Index it. + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("response testGroups") + { + for test in group.get("tests").and_then(Value::as_array).expect("response tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("request testGroups"); + + let mut checked = 0usize; + let mut multi_block = 0usize; + let mut skipped_mct = 0usize; + let mut per_kind: BTreeMap = BTreeMap::new(); + + for group in groups { + let test_type = group.get("testType").and_then(Value::as_str).expect("testType"); + let direction = group.get("direction").and_then(Value::as_str).expect("direction"); + let encrypt = match direction { + "encrypt" => true, + "decrypt" => false, + other => panic!("unexpected direction {other}"), + }; + + for test in group.get("tests").and_then(Value::as_array).expect("tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + + if test_type == "MCT" { + skipped_mct += 1; + continue; + } + + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + if answer.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let key_bytes = decode(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = decode(test, "iv", tc_id).try_into().expect("a 16-byte IV"); + + // Input comes from the request, expected output from the response. + let (input_field, output_field) = if encrypt { ("pt", "ct") } else { ("ct", "pt") }; + let input = to_blocks(&decode(test, input_field, tc_id)); + let expected = to_blocks(&decode(answer, output_field, tc_id)); + + assert_eq!(input.len(), expected.len(), "tcId {tc_id}: length mismatch"); + if input.len() > 1 { + multi_block += 1; + } + + for grouping in [Grouping::Single, Grouping::Pairs] { + let got = run_case_for_key_len(&key_bytes, iv, &input, encrypt, grouping); + assert_eq!( + got, + expected, + "tcId {tc_id}: AES-{} CFB128 {direction}, {} blocks, {grouping:?} grouping", + key_bytes.len() * 8, + input.len() + ); + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-CFB128 {kind}: {n} cases"); + } + println!( + "ACVP AES-CFB128: {checked} AFT cases checked in two groupings each \ + ({multi_block} of them multi-block); {skipped_mct} MCT cases skipped" + ); + + // Guard against a silently-empty or partial run. + assert!(checked > 2000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(multi_block >= 50, "expected the multi-block cases, found {multi_block}"); + assert_eq!(per_kind.len(), 6, "expected all three key lengths in both directions"); +} diff --git a/crypto/modes/tests/cfb_tests.rs b/crypto/modes/tests/cfb_tests.rs new file mode 100644 index 00000000..04b05ba7 --- /dev/null +++ b/crypto/modes/tests/cfb_tests.rs @@ -0,0 +1,620 @@ +//! Structural tests for CFB, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- the keystream construction, chaining, call +//! sequencing, the pair/remainder split, direction typing, SP 800-38A Appendix D error propagation, +//! and the "forward cipher function only" rule of Sec 6.3 -- independently of any real cipher. The +//! known-answer tests against SP 800-38A Appendix F.3.13-F.3.18 are in `sp800_38a_cfb_tests.rs`, +//! and the ACVP CFB128 set is in `acvp_cfb_tests.rs`. +//! +//! The toy's own conformance to [`BlockPermutation`] is pinned once, by +//! `the_toy_permutation_conforms_to_the_trait` in `cbc_tests.rs`; it is the same `Toy` here, so it +//! is not re-run. + +mod common; + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; +use bouncycastle_modes::{Cbc, Cfb, Decrypting, Encrypting}; +use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +use common::{ForwardOnlyToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCfb

= Cfb; +type SwappedCfb = Cfb; +type ForwardOnlyCfb = Cfb; + +/// The implementor hook `do_encrypt_blocks_out`, by value, for tests whose data is block-shaped. +fn enc_blocks( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut ct = [[0u8; TOY_LEN]; N]; + enc.do_encrypt_blocks_out(plaintext, &mut ct).unwrap(); + ct +} + +/// The implementor hook `do_decrypt_blocks_out`, by value. +fn dec_blocks( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut pt = [[0u8; TOY_LEN]; N]; + dec.do_decrypt_blocks_out(ciphertext, &mut pt).unwrap(); + pt +} + +/// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the +/// fixed-output RNG that `do_encrypt_init_rng` takes. +fn pinned_iv() -> [u8; TOY_LEN] { + core::array::from_fn(|i| 0xF0 ^ (i as u8)) +} + +fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { + FixedSeedRNG::::new(iv) +} + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn cfb_conforms_to_the_block_cipher_framework() { + TestFrameworkBlockCipher::new() + .test::, ToyCfb>(); +} + +// ---- the spec equations ------------------------------------------------------------------- + +/// CFB with `s = b` from SP 800-38A Sec 6.3, written out longhand against the raw permutation: +/// +/// ```text +/// I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj +/// ``` +/// +/// This is the independent reference the mode is checked against below. It uses only +/// [`BlockPermutation::encrypt_block`], because that is all the spec calls for. +fn reference_cfb( + perm: &Toy, + iv: [u8; TOY_LEN], + input: &[[u8; TOY_LEN]], + encrypt: bool, +) -> Vec<[u8; TOY_LEN]> { + let mut chain = iv; // I1 = IV + let mut out = Vec::with_capacity(input.len()); + for block in input { + let mut o = chain; + perm.encrypt_block(&mut o); // Oj = CIPH_K(Ij) + let result: [u8; TOY_LEN] = core::array::from_fn(|k| block[k] ^ o[k]); + // I_{j+1} is always the *ciphertext* block, whichever direction we are going. + chain = if encrypt { result } else { *block }; + out.push(result); + } + out +} + +/// The mode must reproduce the Sec 6.3 equations exactly, in both directions. +/// +/// A reference implementation is a weak test on its own -- both could be wrong the same way -- so +/// this also pins the two anchors that follow directly from the equations and that no plausible +/// mistake preserves: `C1 = P1 XOR CIPH_K(IV)`, and encrypting an all-zero block reveals the +/// keystream block itself. +#[test] +fn the_mode_matches_the_spec_equations() { + let key = toy_key(); + let iv = pinned_iv(); + let perm = >::new(&key).unwrap(); + let plaintext: [[u8; TOY_LEN]; 5] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * 31 + j * 7 + 1) as u8)); + + let (mut enc, got_iv) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); + let ct = enc_blocks(&mut enc, &plaintext); + + assert_eq!( + ct.to_vec(), + reference_cfb(&perm, iv, &plaintext, true), + "encryption must match the Sec 6.3 equations" + ); + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let recovered = dec_blocks(&mut dec, &ct); + assert_eq!(recovered, plaintext, "round trip"); + assert_eq!( + recovered.to_vec(), + reference_cfb(&perm, iv, &ct, false), + "decryption must match the Sec 6.3 equations" + ); + + // Anchor 1: `O1 = CIPH_K(IV)` and `C1 = P1 XOR O1`. + let mut o1 = iv; + perm.encrypt_block(&mut o1); + let expected_c1: [u8; TOY_LEN] = core::array::from_fn(|k| plaintext[0][k] ^ o1[k]); + assert_eq!(ct[0], expected_c1, "C1 = P1 XOR CIPH_K(IV)"); + + // Anchor 2: with `P1 = 0`, `C1 = O1`. CFB is a keystream mode, and this is what that means. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!( + enc.do_encrypt(&[0u8; TOY_LEN]).unwrap(), + o1, + "encrypting zero yields the keystream" + ); + + // ...and CFB is not CBC: CBC computes `CIPH_K(P1 XOR IV)`, CFB computes `P1 XOR CIPH_K(IV)`. + let (mut cbc, _) = + Cbc::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) + .unwrap(); + assert_ne!(cbc.do_encrypt(&plaintext[0]).unwrap(), ct[0], "CFB must not agree with CBC"); +} + +// ---- the forward-cipher-only rule --------------------------------------------------------- + +/// SP 800-38A Sec 6.3: "The *forward cipher* function is applied to each input block to produce the +/// output blocks" -- in CFB *decryption* as well as encryption. +/// +/// [`ForwardOnlyToy`] panics from both `decrypt_block` and `decrypt_blocks2`, so this test fails +/// loudly if either direction of the mode ever reaches the inverse cipher. Both the pair path (even +/// `N`) and the single-block path are exercised, and the result is required to agree with the plain +/// [`Toy`] -- otherwise the test could pass by not really encrypting anything. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext: [[u8; TOY_LEN]; 4] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * 17 + j) as u8)); + + let (mut enc, _) = + ForwardOnlyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // The pair path: N = 4 is two pairs, so `encrypt_blocks2` is used and `decrypt_blocks2` is not. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext, "pair path, forward cipher only"); + + // The single-block path. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + for (c, p) in ct.iter().zip(plaintext.iter()) { + assert_eq!(&dec.do_decrypt(c).unwrap(), p, "single-block path, forward cipher only"); + } + + // N = 3 leaves a remainder after the pair loop, so both paths run in one call. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let three = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2]]); + assert_eq!(three, [plaintext[0], plaintext[1], plaintext[2]], "pairs + remainder"); + + // The forward-only toy must agree with the real one, or the above proves nothing. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc_blocks(&mut enc, &plaintext), ct, "the two toys must agree going forward"); +} + +/// The decryptor must feed the **ciphertext** block back, not the plaintext it just recovered. +/// +/// Getting this wrong is invisible in the first block -- `O1 = CIPH_K(IV)` either way -- and wrong +/// from the second onwards. An encryptor run over ciphertext is exactly that mistake: it XORs the +/// right keystream into block 1 and then chains on its own output. So block 1 agreeing while +/// block 2 disagrees is the signature of the bug, and is what this asserts. +#[test] +fn the_decryptor_chains_on_ciphertext_not_plaintext() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + assert_ne!(ct[0], plaintext[0], "the two feedback choices must actually differ here"); + + let (mut wrong, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let out = enc_blocks(&mut wrong, &ct); + + assert_eq!(out[0], plaintext[0], "block 1 cannot tell the two apart"); + assert_ne!(out[1], plaintext[1], "block 2 must, so the feedback source is pinned"); +} + +// ---- chaining and call sequencing -------------------------------------------------------- + +/// Encrypting `n` blocks must not depend on how the calls are grouped, and likewise for +/// decryption. This is the "a sequence of calls is equivalent to one call over the concatenation" +/// contract of the trait, and for CFB it is entirely about `Ij` surviving across calls. +/// +/// The odd groupings matter for decryption specifically: `N = 3` and `N = 5` leave a one-block +/// remainder after the pair loop, and `N = 1` skips the pair loop altogether. +#[test] +fn call_grouping_does_not_change_the_result() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext: [[u8; TOY_LEN]; 8] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * TOY_LEN + j) as u8)); + + // Reference: all eight blocks in one call. + let (mut enc, got_iv) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); + let reference = enc_blocks(&mut enc, &plaintext); + + // The same eight blocks, grouped every way that exercises a different code path. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let mut got = [[0u8; TOY_LEN]; 8]; + let a = enc.do_encrypt(&plaintext[0]).unwrap(); // one block, flat + let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 + let c = enc_blocks(&mut enc, &[plaintext[3], plaintext[4], plaintext[5]]); // N = 3 + let d = enc_blocks(&mut enc, &[plaintext[6], plaintext[7]]); // N = 2 + got[0] = a; + got[1..3].copy_from_slice(&b); + got[3..6].copy_from_slice(&c); + got[6..8].copy_from_slice(&d); + + assert_eq!(got, reference, "grouping must not change the ciphertext"); + + // Now the decrypt side: one call vs several groupings, all from the same ciphertext. + let ct = reference; + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + for grouping in [1usize, 2, 4] { + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut out = [[0u8; TOY_LEN]; 8]; + let mut at = 0; + while at < 8 { + match grouping { + 1 => { + out[at] = dec.do_decrypt(&ct[at]).unwrap(); + } + 2 => { + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1]]); + out[at..at + 2].copy_from_slice(&p); + } + _ => { + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1], ct[at + 2], ct[at + 3]]); + out[at..at + 4].copy_from_slice(&p); + } + } + at += grouping; + } + assert_eq!(out, plaintext, "decrypting in groups of {grouping}"); + } + + // N = 3 and N = 5 both leave a one-block remainder after the pair loop. + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let three = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2]]); + let five = dec_blocks(&mut dec, &[ct[3], ct[4], ct[5], ct[6], ct[7]]); + assert_eq!(three, [plaintext[0], plaintext[1], plaintext[2]]); + assert_eq!(five, [plaintext[3], plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); +} + +/// The pair path in `do_decrypt_blocks_out` must actually be taken. +/// +/// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block methods +/// are correct. CFB decryption pairs through `encrypt_blocks2`, so with this permutation a pair +/// comes out wrong and a lone block comes out right. If both came out right, the pair path would be +/// dead code and every claim about it would be untested. +#[test] +fn the_pair_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; + + // The correct toy round-trips. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + // The swapped-pair toy encrypts identically -- CFB encryption is serial and never pairs, so its + // `encrypt_blocks2` override is not reached from the encryptor at all. + let (mut enc, _) = + SwappedCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let swapped_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(swapped_ct, ct, "CFB encryption must not use the pair path"); + + // ...but decrypting the pair together must now be wrong, because the pair path is used. + let mut dec = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!( + dec_blocks(&mut dec, &swapped_ct), + plaintext, + "decrypting a pair must go through encrypt_blocks2" + ); + + // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. + let mut dec = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let p0 = dec.do_decrypt(&swapped_ct[0]).unwrap(); + let p1 = dec.do_decrypt(&swapped_ct[1]).unwrap(); + assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); +} + +/// The flat streaming method must agree with the block-shaped implementor hook and report the +/// byte count. +#[test] +fn flat_streaming_agrees_with_the_block_hook() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + let flat_plaintext: [u8; 3 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let by_value = enc.do_encrypt(&flat_plaintext).unwrap(); + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let mut out = [[0u8; TOY_LEN]; 3]; + let n = enc.do_encrypt_blocks_out(&plaintext, &mut out).unwrap(); + assert_eq!(n, 3 * TOY_LEN); + assert_eq!(*out.as_flattened(), by_value, "flat streaming must equal the block hook"); + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut back = [[0u8; TOY_LEN]; 3]; + let n = dec.do_decrypt_blocks_out(&out, &mut back).unwrap(); + assert_eq!(n, 3 * TOY_LEN); + assert_eq!(back, plaintext); +} + +/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`) must produce exactly what the streaming +/// API produces over the same blocks, for an odd block count (pairs plus a one-block tail) and an +/// even one (pairs only), in both directions and through the `_out` variants. +#[test] +fn one_shots_agree_with_the_streaming_api() { + let key = toy_key(); + let iv = pinned_iv(); + + // 3 blocks = 48 bytes: one pair and a tail. + let flat3: [u8; 3 * TOY_LEN] = core::array::from_fn(|i| (i * 7) as u8); + let blocks3: [[u8; TOY_LEN]; 3] = + core::array::from_fn(|b| flat3[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let (iv_a, ct_blocks) = { + let (mut enc, got) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + (got, enc_blocks(&mut enc, &blocks3)) + }; + let (iv_b, ct_flat) = + ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &flat3).unwrap(); + assert_eq!(iv_a, iv_b); + assert_eq!(ct_flat, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + assert_eq!(ToyCfb::::decrypt(&key, &iv, &ct_flat).unwrap(), flat3); + + let mut ct_out = [0u8; 3 * TOY_LEN]; + let (_, n) = + ToyCfb::::encrypt_out_rng(&key, &mut pinned_rng(iv), &flat3, &mut ct_out) + .unwrap(); + assert_eq!((n, ct_out), (3 * TOY_LEN, ct_flat)); + let mut pt_out = [0u8; 3 * TOY_LEN]; + assert_eq!( + ToyCfb::::decrypt_out(&key, &iv, &ct_out, &mut pt_out).unwrap(), + 3 * TOY_LEN + ); + assert_eq!(pt_out, flat3); + + // 4 blocks = 64 bytes: pairs only, no tail. + let flat4: [u8; 4 * TOY_LEN] = core::array::from_fn(|i| (i * 13 + 1) as u8); + let blocks4: [[u8; TOY_LEN]; 4] = + core::array::from_fn(|b| flat4[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let ct_blocks = { + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + enc_blocks(&mut enc, &blocks4) + }; + let (_, ct_flat) = + ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &flat4).unwrap(); + assert_eq!(ct_flat, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + assert_eq!(ToyCfb::::decrypt(&key, &iv, &ct_flat).unwrap(), flat4); + + // The OS-RNG variant round-trips too. + let (iv_fresh, ct) = ToyCfb::::encrypt(&key, &flat3).unwrap(); + assert_eq!(ToyCfb::::decrypt(&key, &iv_fresh, &ct).unwrap(), flat3); +} + +// ---- SP 800-38A Appendix D error propagation --------------------------------------------- + +/// The parts of Appendix D that follow from the equations and hold for *any* permutation. +/// +/// Table D.2 for CFB: a bit error in `Cj` gives "SBE in the decryption of `Cj`" -- specific bit +/// errors, i.e. the same bit positions -- because `Pj = Cj XOR Oj` and `Oj = CIPH_K(C_{j-1})` does +/// not depend on `Cj` at all. Earlier blocks are untouched, and with `s = b` the damage reaches +/// exactly one block further (`Cj+1`, since `b/s = 1`). +#[test] +fn a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_block() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // Every bit of C2, so the SBE claim is checked exhaustively rather than at one position. + for byte in 0..TOY_LEN { + for bit in 0..8 { + let mut corrupt = ct; + corrupt[1][byte] ^= 1 << bit; + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let got = dec_blocks(&mut dec, &corrupt); + + assert_eq!(got[0], plaintext[0], "P1 depends only on the IV and C1"); + + let mut expected_p2 = plaintext[1]; + expected_p2[byte] ^= 1 << bit; + assert_eq!( + got[1], expected_p2, + "C2 byte {byte} bit {bit}: exactly that bit of P2 should change" + ); + + assert_ne!(got[2], plaintext[2], "P3 comes from CIPH_K of the corrupted C2"); + assert_eq!(got[3], plaintext[3], "P4 is unaffected: b/s = 1, so damage stops at P3"); + } + } +} + +/// The parts of Appendix D that need a real cipher's diffusion, checked with AES-128. +/// +/// Table D.2 for CFB says the *other* affected block gets "RBE" -- random bit errors, "bit errors +/// occur independently in any bit position with an expected probability of 1/2". That is a property +/// of the block cipher, not of the mode, so the toy (whose rounds are byte-local) cannot show it. +/// +/// The point worth pinning is that CFB and CBC differ here, and in which direction: under CBC a +/// corrupted IV flips *exactly* the corresponding bit of `P1` (Appendix D, and +/// `an_iv_bit_error_flips_exactly_that_bit_of_the_first_block` in `cbc_tests.rs`), whereas under CFB +/// the IV goes through the cipher first, so `P1` is randomised instead. Confusing the two would be a +/// real bug and this is what catches it. +#[test] +fn an_iv_bit_error_randomises_only_the_first_block() { + type Aes128Cfb = Cfb; + const LEN: usize = 16; + + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) + .expect("a valid AES-128 key"); + let iv: [u8; LEN] = core::array::from_fn(|i| 0x0F ^ (i as u8)); + let plaintext = [[0x00u8; LEN], [0x11u8; LEN], [0x22u8; LEN]]; + + let (mut enc, got_iv) = + Aes128Cfb::::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(iv)) + .unwrap(); + assert_eq!(got_iv, iv); + let mut ct = [[0u8; LEN]; 3]; + enc.do_encrypt_blocks_out(&plaintext, &mut ct).unwrap(); + + let mut first_blocks = std::collections::BTreeSet::new(); + + for byte in 0..LEN { + for bit in 0..8 { + let mut corrupt_iv = iv; + corrupt_iv[byte] ^= 1 << bit; + + let mut dec = Aes128Cfb::::do_decrypt_init(&key, &corrupt_iv).unwrap(); + let mut got = [[0u8; LEN]; 3]; + dec.do_decrypt_blocks_out(&ct, &mut got).unwrap(); + + // Only P1 is affected: with s = b, Appendix D's "first i/s (rounding up) ciphertext + // segments" is one segment for every bit position i. + assert_eq!(got[1], plaintext[1], "IV byte {byte} bit {bit}: P2 must be unaffected"); + assert_eq!(got[2], plaintext[2], "IV byte {byte} bit {bit}: P3 must be unaffected"); + + // ...and it is randomised, not flipped in place. The CBC behaviour would be a + // single-bit difference in exactly the position that was corrupted. + let differing_bits: u32 = + got[0].iter().zip(plaintext[0].iter()).map(|(a, b)| (a ^ b).count_ones()).sum(); + assert!( + differing_bits > 1, + "IV byte {byte} bit {bit}: P1 should be randomised, not flipped in place \ + ({differing_bits} bit(s) differ)" + ); + + let mut cbc_style = plaintext[0]; + cbc_style[byte] ^= 1 << bit; + assert_ne!(got[0], cbc_style, "CFB must not behave like CBC for a corrupted IV"); + + assert!(first_blocks.insert(got[0]), "distinct IVs should give distinct P1"); + } + } + + assert_eq!(first_blocks.len(), LEN * 8, "every corrupted IV should have been tried"); +} + +// ---- IV handling ------------------------------------------------------------------------- + +/// Two encryption flows under the same key must not reuse an IV. The framework checks this too; +/// repeated here because a repeated IV is worse for CFB than for CBC -- it leaks the XOR of the two +/// plaintexts, not merely their equality (see the crate docs, "Key and IV reuse"). +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = toy_key(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..64 { + let (_, iv) = ToyCfb::::do_encrypt_init(&key).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); + } +} + +/// Identical plaintext under the same key must give different ciphertext, because the IV differs. +#[test] +fn identical_plaintext_gives_different_ciphertext() { + let key = toy_key(); + let plaintext = [0x77u8; 2 * TOY_LEN]; + + let (_, first) = ToyCfb::::encrypt(&key, &plaintext).unwrap(); + let (_, second) = ToyCfb::::encrypt(&key, &plaintext).unwrap(); + assert_ne!(first, second); + + // ...and, within one message, two identical plaintext blocks must not give identical ciphertext + // blocks either, because the keystream block differs. + assert_ne!( + first[..TOY_LEN], + first[TOY_LEN..], + "feedback should break the ECB pattern within a message" + ); +} + +// ---- key handling ------------------------------------------------------------------------ + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyCfb::::do_encrypt_init(&seed).is_err()); + assert!(ToyCfb::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +} + +// ---- composition with the padding layer -------------------------------------------------- + +/// CFB is block-aligned by contract, so arbitrary-length data goes through `bouncycastle-padding`. +/// Nothing in either crate knows about the other, so this is the test that they actually compose -- +/// across every length from empty to just past three blocks, which covers an exact multiple of the +/// block size (where PKCS7 appends a whole extra block) and every partial block. +#[test] +fn the_padding_layer_round_trips_every_length() { + type Enc = PaddedEncryptor, PKCS7, TOY_LEN, TOY_LEN, TOY_LEN>; + type Dec = PaddedDecryptor, PKCS7, TOY_LEN, TOY_LEN, TOY_LEN>; + + for len in 0..=(3 * TOY_LEN + 1) { + let plaintext: Vec = (0..len).map(|i| (i * 5 + 3) as u8).collect(); + + let mut ciphertext = vec![0u8; Enc::encrypt_out_len(len)]; + let (iv, written) = + Enc::encrypt_out(&toy_key(), &plaintext, &mut ciphertext).expect("padded encryption"); + assert_eq!(written, ciphertext.len(), "len {len}: one whole number of blocks out"); + assert!(written > len, "len {len}: PKCS7 always adds at least one byte"); + + let mut recovered = vec![0u8; Dec::decrypt_out_max_len(written)]; + let n = Dec::decrypt_out(&toy_key(), &iv, &ciphertext, &mut recovered) + .expect("padded decryption"); + assert_eq!(&recovered[..n], &plaintext[..], "len {len}: round trip through PKCS7"); + } +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the "Memory Usage" table in the crate docs, and the claim that CFB costs exactly what CBC +/// costs. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + assert_eq!(size_of::>(), 176 + 16); + assert_eq!(size_of::>(), 208 + 16); + assert_eq!(size_of::>(), 240 + 16); + + // The direction marker is free, and does not change the layout. + assert_eq!( + size_of::>(), + size_of::>() + ); + + // ...and the general rule the docs state. + assert_eq!(size_of::>(), size_of::() + 16); + + // The docs say CFB is the same size as CBC, because it stores the same thing. + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); +} diff --git a/crypto/modes/tests/common/mod.rs b/crypto/modes/tests/common/mod.rs index 6bd5dcd4..7bd9b033 100644 --- a/crypto/modes/tests/common/mod.rs +++ b/crypto/modes/tests/common/mod.rs @@ -13,6 +13,11 @@ //! round-trip. [`Toy`] is therefore asymmetric: it rotates before XOR-ing, so the two directions are //! genuinely different functions. +// Each test binary that includes this module uses a subset of it -- `cfb_tests.rs` needs +// `ForwardOnlyToy`, `cbc_tests.rs` does not -- and an unused item in an integration test's private +// module is otherwise a dead-code warning. +#![allow(dead_code)] + use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{Algorithm, BlockPermutation, SecurityStrength}; @@ -115,6 +120,48 @@ impl BlockPermutation for SwappedPairToy { } } +/// A toy whose **inverse cipher function panics**. +/// +/// SP 800-38A Sec 6.3 applies the forward cipher function in both directions of CFB, so a correct +/// `Cfb` never touches `decrypt_block` or `decrypt_blocks2`. Running a full CFB round trip over this +/// permutation turns that claim into a test: if either decryption entry point is ever reached, the +/// test panics with the message below rather than quietly producing a right answer for the wrong +/// reason. +/// +/// This is deliberately not a valid [`BlockPermutation`] -- it cannot pass +/// `TestFrameworkBlockPermutation`, which exercises both directions -- so it is only ever used with +/// `Cfb`. Its forward methods delegate to [`Toy`], including the pair method, so a CFB round trip +/// over it must agree with one over `Toy`. +pub struct ForwardOnlyToy { + inner: Toy, +} + +impl BlockCipher for ForwardOnlyToy { + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl BlockPermutation for ForwardOnlyToy { + fn new(key: &KeyMaterial) -> Result { + Ok(Self { inner: Toy::new(key)? }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.encrypt_block(block); + } + + fn decrypt_block(&self, _block: &mut [u8; TOY_LEN]) { + panic!("CFB must never call the inverse cipher function (SP 800-38A Sec 6.3)"); + } + + fn encrypt_blocks2(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.encrypt_blocks2(blocks); + } + + fn decrypt_blocks2(&self, _blocks: &mut [[u8; TOY_LEN]; 2]) { + panic!("CFB must never call the inverse cipher pair function (SP 800-38A Sec 6.3)"); + } +} + /// Builds a `KeyMaterial` for the toys from a fixed non-zero pattern. pub fn toy_key() -> KeyMaterial { let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); diff --git a/crypto/modes/tests/sp800_38a_cfb_tests.rs b/crypto/modes/tests/sp800_38a_cfb_tests.rs new file mode 100644 index 00000000..ff04c1b6 --- /dev/null +++ b/crypto/modes/tests/sp800_38a_cfb_tests.rs @@ -0,0 +1,377 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.3, "CFB Example Vectors". +//! +//! Sections **F.3.13 through F.3.18**: CFB128-AES128, CFB128-AES192 and CFB128-AES256, Encrypt and +//! Decrypt. These are the `s = b` subsections, the ones [`Cfb`] implements. The rest of Appendix F.3 +//! -- F.3.1-F.3.6 (CFB1) and F.3.7-F.3.12 (CFB8) -- covers segment sizes this crate does not +//! provide, and is deliberately not transcribed; see the [`Cfb`] module docs. +//! +//! All six share the same IV and the same four plaintext blocks (Appendix F preamble: the plaintext +//! is the same for every subsection except the CFB1 and CFB8 ones, which truncate it); only the key +//! and the resulting ciphertext differ. The three keys are the same three used by SP 800-38A F.1 +//! (ECB) and F.2 (CBC), so these vectors also re-check each AES key expansion through a third +//! construction. +//! +//! Transcribed from the published SP 800-38A PDF (2001 edition). +//! +//! # The intermediate values are checked too +//! +//! Unlike Appendix F.2, whose "Input Block" is just `Pj XOR Cj-1`, the F.3 subsections tabulate the +//! CFB **output blocks** -- the keystream `Oj` -- alongside the input blocks. Those are the mode's +//! internals, so `the_tabulated_output_blocks_are_the_keystream` checks them against the raw +//! permutation rather than only comparing final ciphertext. A mode that produced the right +//! ciphertext by a different route would still have to match them. +//! +//! # Driving the IV +//! +//! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven +//! through [`BlockCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is +//! the vector's IV, and the test asserts the returned init data really is that IV before comparing +//! any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; + +const BLOCK_LEN: usize = 16; + +/// The IV shared by every Appendix F.3 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four plaintext blocks shared by every Appendix F subsection (Appendix F preamble). +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// F.3.13 / F.3.14 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.3.13 CFB128-AES128.Encrypt ciphertext segments. +const CIPHERTEXTS_128: [&str; 4] = [ + "3b3fd92eb72dad20333449f8e83cfb4a", + "c8a64537a0b3a93fcde3cdad9f1ce58b", + "26751f67a3cbb140b1808cf187a4f4df", + "c04b05357c5d1c0eeac4c66f9ff7f2e6", +]; +/// F.3.13 CFB128-AES128.Encrypt output blocks, i.e. the keystream `Oj`. +const OUTPUT_BLOCKS_128: [&str; 4] = [ + "50fe67cc996d32b6da0937e99bafec60", + "668bcf60beb005a35354a201dab36bda", + "16bd032100975551547b4de89daea630", + "36d42170a312871947ef8714799bc5f6", +]; + +/// F.3.15 / F.3.16 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.3.15 CFB128-AES192.Encrypt ciphertext segments. +const CIPHERTEXTS_192: [&str; 4] = [ + "cdc80d6fddf18cab34c25909c99a4174", + "67ce7f7f81173621961a2b70171d3d7a", + "2e1e8a1dd59b88b1c8e60fed1efac4c9", + "c05f9f9ca9834fa042ae8fba584b09ff", +]; +/// F.3.15 CFB128-AES192.Encrypt output blocks. +const OUTPUT_BLOCKS_192: [&str; 4] = [ + "a609b38df3b1133dddff2718ba09565e", + "c9e3f5289f149abd08ad44dc52b2b32b", + "1ed6965b76c76ca02d1dcef404f09626", + "36c0bbd976ccd4b7ef85cec1be273eef", +]; + +/// F.3.17 / F.3.18 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.3.17 CFB128-AES256.Encrypt ciphertext segments. +const CIPHERTEXTS_256: [&str; 4] = [ + "dc7e84bfda79164b7ecd8486985d3860", + "39ffed143b28b1c832113c6331e5407b", + "df10132415e54b92a13ed0a8267ae2f9", + "75a385741ab9cef82031623d55b1e471", +]; +/// F.3.17 CFB128-AES256.Encrypt output blocks. +const OUTPUT_BLOCKS_256: [&str; 4] = [ + "b7bf3a5df43989dd97f0fa97ebce2f4a", + "97d26743252b1d54aca653cf744ace2a", + "efd80f62b6b9af8344c511b13c70b016", + "833ca131c5f655ef8d1a2346b3ddd361", +]; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn blocks(hex_strs: &[&str; 4]) -> [[u8; BLOCK_LEN]; 4] { + core::array::from_fn(|i| block(hex_strs[i])) +} + +/// The same four blocks as 64 contiguous bytes, for the flat streaming and one-shot methods. +fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { + blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let bytes = hex::decode(hex_str).expect("valid hex"); + assert_eq!(bytes.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Runs one Appendix F.3 encrypt subsection. +/// +/// Checks the whole message in one call, then again one segment at a time, then again through the +/// `_out` variant -- the vector should not care how the calls are grouped. +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +where + P: BlockPermutation, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(expected); + + // All four segments in one call. + let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + assert_eq!( + enc.do_encrypt(&flat(&PLAINTEXTS)).unwrap(), + flat(expected), + "{section}: four segments in one call" + ); + + // One segment at a time. + let (mut enc, _) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let got = enc.do_encrypt(p).unwrap(); + assert_eq!(&got, c, "{section}: segment #{}", i + 1); + } + + // Through the implementor hook, `do_*_blocks_out`. + let (mut enc, _) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + let mut out = [[0u8; BLOCK_LEN]; 4]; + let n = enc.do_encrypt_blocks_out(&pt, &mut out).unwrap(); + assert_eq!(n, 4 * BLOCK_LEN); + assert_eq!(out, ct, "{section}: _out variant"); +} + +/// Runs one Appendix F.3 decrypt subsection. +/// +/// Checks one call, one segment at a time, and the odd grouping `3 + 1` -- which is the grouping +/// that leaves a one-block remainder after the pair loop in `do_decrypt_blocks_out`. +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +where + P: BlockPermutation, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertext); + + type Dec = Cfb; + + // All four segments in one call (two pairs, no remainder). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!( + dec.do_decrypt(&flat(ciphertext)).unwrap(), + flat(&PLAINTEXTS), + "{section}: four segments in one call" + ); + + // One segment at a time (never takes the pair path). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { + let got = dec.do_decrypt(c).unwrap(); + assert_eq!(&got, p, "{section}: segment #{}", i + 1); + } + + // 3 + 1: one pair plus a remainder, then a lone block. + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let first_three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + let three = dec.do_decrypt(&first_three).unwrap(); + let one = dec.do_decrypt(&ct[3]).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: segments 1-3"); + assert_eq!(one, pt[3], "{section}: segment 4"); + + // Through the implementor hook, `do_*_blocks_out`. + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut out = [[0u8; BLOCK_LEN]; 4]; + let n = dec.do_decrypt_blocks_out(&ct, &mut out).unwrap(); + assert_eq!(n, 4 * BLOCK_LEN); + assert_eq!(out, pt, "{section}: _out variant"); +} + +#[test] +fn f_3_13_cfb128_aes128_encrypt() { + check_encrypt::("F.3.13", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_3_14_cfb128_aes128_decrypt() { + check_decrypt::("F.3.14", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_3_15_cfb128_aes192_encrypt() { + check_encrypt::("F.3.15", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_3_16_cfb128_aes192_decrypt() { + check_decrypt::("F.3.16", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_3_17_cfb128_aes256_encrypt() { + check_encrypt::("F.3.17", KEY_256, &CIPHERTEXTS_256); +} + +#[test] +fn f_3_18_cfb128_aes256_decrypt() { + check_decrypt::("F.3.18", KEY_256, &CIPHERTEXTS_256); +} + +/// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. +/// The one-shots take flat arrays, so the four segments are presented as 64 contiguous bytes. +#[test] +fn the_one_shot_api_matches_the_vectors() { + let iv = block(IV); + let pt = flat(&PLAINTEXTS); + + assert_eq!( + Cfb::::decrypt( + &key_material::<16>(KEY_128), + &iv, + &flat(&CIPHERTEXTS_128) + ) + .unwrap(), + pt + ); + assert_eq!( + Cfb::::decrypt( + &key_material::<24>(KEY_192), + &iv, + &flat(&CIPHERTEXTS_192) + ) + .unwrap(), + pt + ); + assert_eq!( + Cfb::::decrypt( + &key_material::<32>(KEY_256), + &iv, + &flat(&CIPHERTEXTS_256) + ) + .unwrap(), + pt + ); +} + +/// The spec's tabulated **Output Blocks** are the CFB keystream, and its **Input Blocks** are the +/// IV followed by the ciphertext segments. Both fall straight out of Sec 6.3 with `s = b`: +/// +/// ```text +/// I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj +/// ``` +/// +/// So each `Oj` in the table must equal the raw permutation applied to the previous ciphertext +/// segment (or to the IV, for `j = 1`), and XOR-ing it with the plaintext must give the ciphertext. +/// Checking this pins the mode's internals against the spec, not just its final output -- and in +/// particular it is what distinguishes CFB from a mode that happens to agree on the ciphertext. +/// +/// It also confirms the transcription: the ciphertext and output-block columns above are related by +/// an XOR that would not survive a typo in either. +fn check_output_blocks( + section: &str, + key_hex: &str, + ciphertexts: &[&str; 4], + output_blocks: &[&str; 4], +) where + P: BlockPermutation, +{ + let key = key_material::(key_hex); + let perm = P::new(&key).expect("a valid key"); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertexts); + let o = blocks(output_blocks); + + for j in 0..4 { + // Ij: the IV for j = 1, otherwise the previous ciphertext segment. + let input_block = if j == 0 { block(IV) } else { ct[j - 1] }; + + // Oj = CIPH_K(Ij) -- the *forward* cipher function, which is all CFB ever uses. + let mut computed = input_block; + perm.encrypt_block(&mut computed); + assert_eq!( + computed, + o[j], + "{section}: tabulated output block #{} should be CIPH_K of input block #{}", + j + 1, + j + 1 + ); + + // Cj = Pj XOR Oj. + let xored: [u8; BLOCK_LEN] = core::array::from_fn(|k| pt[j][k] ^ o[j][k]); + assert_eq!(xored, ct[j], "{section}: Cj = Pj XOR Oj for segment #{}", j + 1); + } +} + +#[test] +fn the_tabulated_output_blocks_are_the_keystream() { + check_output_blocks::("F.3.13", KEY_128, &CIPHERTEXTS_128, &OUTPUT_BLOCKS_128); + check_output_blocks::("F.3.15", KEY_192, &CIPHERTEXTS_192, &OUTPUT_BLOCKS_192); + check_output_blocks::("F.3.17", KEY_256, &CIPHERTEXTS_256, &OUTPUT_BLOCKS_256); +} + +/// CFB128 and OFB must agree on the **first** block and on nothing after it. +/// +/// Both modes set `I1 = IV` and `O1 = CIPH_K(IV)`, and both then XOR that into the plaintext, so +/// `C1` is necessarily the same. They diverge from the second block, because OFB feeds back the +/// output block `Oj` (Sec 6.4) while CFB feeds back the ciphertext `Cj` (Sec 6.3). +/// +/// Appendix F bears this out, and the values below are quoted from **F.4.1 (OFB-AES128.Encrypt)**, +/// a different subsection from the ones this file is testing. Agreement on block 1 is therefore an +/// independent check that the F.3.13 transcription is right; disagreement on block 2 is a check +/// that [`Cfb`] is CFB and not OFB. +#[test] +fn cfb128_agrees_with_ofb_on_the_first_block_only() { + /// F.4.1 OFB-AES128.Encrypt, Block #1 Output Block. Same key and IV, so the same `O1`. + const OFB_OUTPUT_BLOCK_1: &str = "50fe67cc996d32b6da0937e99bafec60"; + /// F.4.1 OFB-AES128.Encrypt, Block #1 and Block #2 Ciphertext. + const OFB_CIPHERTEXT_1: &str = "3b3fd92eb72dad20333449f8e83cfb4a"; + const OFB_CIPHERTEXT_2: &str = "7789508d16918f03f53c52dac54ed825"; + + assert_eq!( + OUTPUT_BLOCKS_128[0], OFB_OUTPUT_BLOCK_1, + "F.3.13 and F.4.1 must tabulate the same O1 = CIPH_K(IV)" + ); + + let key = key_material::<16>(KEY_128); + let iv = block(IV); + let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<16>::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv); + + let c1 = enc.do_encrypt(&block(PLAINTEXTS[0])).unwrap(); + assert_eq!(c1, block(OFB_CIPHERTEXT_1), "block 1 must match OFB, and F.3.13"); + + let c2 = enc.do_encrypt(&block(PLAINTEXTS[1])).unwrap(); + assert_eq!(c2, block(CIPHERTEXTS_128[1]), "block 2 must match F.3.13"); + assert_ne!(c2, block(OFB_CIPHERTEXT_2), "block 2 must NOT match OFB"); +} From c15aef93ddcd1ef3ef3f089046ac938b7203f66a Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Wed, 2 Sep 2026 22:59:10 +0700 Subject: [PATCH 2/4] Fixed EPIPE race and BrokenPipe bug (#103) --- cli/tests/aes_cbc_cli_tests.rs | 18 +++++++++++------- cli/tests/aes_cfb_cli_tests.rs | 18 +++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/cli/tests/aes_cbc_cli_tests.rs b/cli/tests/aes_cbc_cli_tests.rs index 9dcea30c..32a0d040 100644 --- a/cli/tests/aes_cbc_cli_tests.rs +++ b/cli/tests/aes_cbc_cli_tests.rs @@ -7,7 +7,7 @@ //! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the //! current profile, so there is nothing to build or locate by hand. -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::process::{Command, Output, Stdio}; /// The path to the binary under test, resolved by cargo. @@ -60,12 +60,16 @@ fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { .spawn() .expect("failed to spawn bc-rust"); - child - .stdin - .as_mut() - .expect("stdin piped") - .write_all(stdin_bytes) - .expect("failed to write to stdin"); + // `BrokenPipe` here is an expected outcome, not a harness failure. The error-path tests hand a + // rejected key or a misaligned length to a command that `exit`s before it reads stdin, so the + // write races the child's exit and loses on a slow or loaded runner. What those tests assert is + // the exit status and stderr, both of which `wait_with_output` still returns. Any *other* write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } child.wait_with_output().expect("failed to wait for bc-rust") } diff --git a/cli/tests/aes_cfb_cli_tests.rs b/cli/tests/aes_cfb_cli_tests.rs index 4637c0b9..ef44ead5 100644 --- a/cli/tests/aes_cfb_cli_tests.rs +++ b/cli/tests/aes_cfb_cli_tests.rs @@ -13,7 +13,7 @@ //! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the //! current profile, so there is nothing to build or locate by hand. -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::process::{Command, Output, Stdio}; /// The path to the binary under test, resolved by cargo. @@ -74,12 +74,16 @@ fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { .spawn() .expect("failed to spawn bc-rust"); - child - .stdin - .as_mut() - .expect("stdin piped") - .write_all(stdin_bytes) - .expect("failed to write to stdin"); + // `BrokenPipe` here is an expected outcome, not a harness failure. The error-path tests hand a + // rejected key or a misaligned length to a command that `exit`s before it reads stdin, so the + // write races the child's exit and loses on a slow or loaded runner. What those tests assert is + // the exit status and stderr, both of which `wait_with_output` still returns. Any *other* write + // error is a real problem and still panics. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } child.wait_with_output().expect("failed to wait for bc-rust") } From 8c55b5099baafd24445704291553a6a8fe0fe543 Mon Sep 17 00:00:00 2001 From: officialfrancismendoza Date: Wed, 2 Sep 2026 22:59:38 +0700 Subject: [PATCH 3/4] Added a regression test and fixed other detected race condition (#103) --- cli/tests/aes_cbc_cli_tests.rs | 89 +++++++++++++++++++++++++++++----- cli/tests/aes_cfb_cli_tests.rs | 89 +++++++++++++++++++++++++++++----- 2 files changed, 154 insertions(+), 24 deletions(-) diff --git a/cli/tests/aes_cbc_cli_tests.rs b/cli/tests/aes_cbc_cli_tests.rs index 32a0d040..d659c0cd 100644 --- a/cli/tests/aes_cbc_cli_tests.rs +++ b/cli/tests/aes_cbc_cli_tests.rs @@ -9,6 +9,7 @@ use std::io::{ErrorKind, Write}; use std::process::{Command, Output, Stdio}; +use std::thread; /// The path to the binary under test, resolved by cargo. const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); @@ -51,6 +52,27 @@ const CT_256: &str = concat!( ); /// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// # Why stdin is written from a thread +/// +/// stdin, stdout and stderr are all pipes with a bounded buffer (typically 64 KiB). Writing all of +/// stdin from *this* thread before reading any output deadlocks as soon as the payload is large +/// enough: the child fills its stdout buffer and blocks, so it stops draining stdin, so our write +/// blocks too, and neither side can move. That is a hang rather than a failure, so it would surface +/// as a CI timeout. Writing on a separate thread leaves this one free to drain stdout and stderr +/// via `wait_with_output`, which breaks the cycle. `a_payload_larger_than_the_pipe_buffer_round_trips` +/// pins it. +/// +/// Dropping the pipe when the write finishes is what signals EOF to the child, so the writer thread +/// owns the handle (`take`, not `as_mut`) and must run to completion. +/// +/// # Why `BrokenPipe` is ignored +/// +/// The error-path tests hand a rejected key or a misaligned length to a command that `exit`s before +/// it reads stdin, so the write races the child's exit and loses. That is an expected outcome, not a +/// harness failure: those tests assert the exit status and stderr, both of which `wait_with_output` +/// still returns. Any *other* write error is a real problem and still panics. +/// `a_large_payload_on_an_error_path_does_not_break_the_harness` pins it. fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { let mut child = Command::new(BC_RUST) .args(args) @@ -60,18 +82,22 @@ fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { .spawn() .expect("failed to spawn bc-rust"); - // `BrokenPipe` here is an expected outcome, not a harness failure. The error-path tests hand a - // rejected key or a misaligned length to a command that `exit`s before it reads stdin, so the - // write races the child's exit and loses on a slow or loaded runner. What those tests assert is - // the exit status and stderr, both of which `wait_with_output` still returns. Any *other* write - // error is a real problem and still panics. - match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { - Ok(()) => {} - Err(e) if e.kind() == ErrorKind::BrokenPipe => {} - Err(e) => panic!("failed to write to stdin: {e}"), - } - - child.wait_with_output().expect("failed to wait for bc-rust") + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + // Drain stdout and stderr first: the writer may still be blocked on a full stdin buffer, and it + // cannot finish until the child consumes more, which it cannot do while its output is backed up. + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output } /// Runs a command that is expected to succeed, returning stdout. @@ -122,6 +148,45 @@ fn pseudo_random(len: usize, seed: u32) -> Vec { .collect() } +// ---- the harness itself ------------------------------------------------------------------ +// +// These two pin `run`'s pipe handling. Both bugs they cover are timing-dependent: they pass on a +// fast machine with a small payload and fail on a slow or loaded runner, which is exactly how the +// first one reached CI. Forcing the condition with an oversized payload makes them deterministic +// instead of waiting for a bad day. The same pair exists in `aes_cfb_cli_tests.rs`, because each +// file has its own copy of `run`. + +/// Far beyond any pipe buffer, so a write cannot complete before the child has drained it. +const OVERSIZED: usize = 4 * 1024 * 1024; + +/// An error path must not take the harness down with it. +/// +/// `encrypt` with no `--key` prints its complaint and exits without reading stdin, so the write +/// loses the race and the pipe breaks. Before `run` tolerated `ErrorKind::BrokenPipe` this panicked +/// with "failed to write to stdin" (os error 109 on Windows, EPIPE elsewhere) instead of reporting +/// the CLI's actual error, which is what the other error-path tests assert on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let stderr = run_err(&["aes128-cbc", "encrypt"], &vec![0u8; OVERSIZED]); + assert!(stderr.contains("--key"), "the CLI's own error must still be reported: {stderr}"); +} + +/// A payload larger than the pipe buffer must round-trip rather than deadlock. +/// +/// This is the reason `run` writes stdin from a separate thread. Writing it inline wedges once both +/// pipes fill: the child blocks writing stdout, so it stops reading stdin, so the harness blocks +/// writing stdin. Nothing times out on its own -- the test just hangs until CI kills the job -- so +/// this is the check that would have caught it. +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + let plaintext = pseudo_random(OVERSIZED, 0xC0FFEE); + let ciphertext = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len() + 16, "IV plus the ciphertext"); + + let recovered = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{OVERSIZED} bytes should round trip"); +} + // ---- the SP 800-38A F.2 vectors, through the CLI ----------------------------------------- /// `decrypt` reproduces the spec plaintext when handed the spec's IV followed by the spec's diff --git a/cli/tests/aes_cfb_cli_tests.rs b/cli/tests/aes_cfb_cli_tests.rs index ef44ead5..571cfebe 100644 --- a/cli/tests/aes_cfb_cli_tests.rs +++ b/cli/tests/aes_cfb_cli_tests.rs @@ -15,6 +15,7 @@ use std::io::{ErrorKind, Write}; use std::process::{Command, Output, Stdio}; +use std::thread; /// The path to the binary under test, resolved by cargo. const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); @@ -65,6 +66,27 @@ const CBC_CT_128: &str = concat!( ); /// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// # Why stdin is written from a thread +/// +/// stdin, stdout and stderr are all pipes with a bounded buffer (typically 64 KiB). Writing all of +/// stdin from *this* thread before reading any output deadlocks as soon as the payload is large +/// enough: the child fills its stdout buffer and blocks, so it stops draining stdin, so our write +/// blocks too, and neither side can move. That is a hang rather than a failure, so it would surface +/// as a CI timeout. Writing on a separate thread leaves this one free to drain stdout and stderr +/// via `wait_with_output`, which breaks the cycle. `a_payload_larger_than_the_pipe_buffer_round_trips` +/// pins it. +/// +/// Dropping the pipe when the write finishes is what signals EOF to the child, so the writer thread +/// owns the handle (`take`, not `as_mut`) and must run to completion. +/// +/// # Why `BrokenPipe` is ignored +/// +/// The error-path tests hand a rejected key or a misaligned length to a command that `exit`s before +/// it reads stdin, so the write races the child's exit and loses. That is an expected outcome, not a +/// harness failure: those tests assert the exit status and stderr, both of which `wait_with_output` +/// still returns. Any *other* write error is a real problem and still panics. +/// `a_large_payload_on_an_error_path_does_not_break_the_harness` pins it. fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { let mut child = Command::new(BC_RUST) .args(args) @@ -74,18 +96,22 @@ fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { .spawn() .expect("failed to spawn bc-rust"); - // `BrokenPipe` here is an expected outcome, not a harness failure. The error-path tests hand a - // rejected key or a misaligned length to a command that `exit`s before it reads stdin, so the - // write races the child's exit and loses on a slow or loaded runner. What those tests assert is - // the exit status and stderr, both of which `wait_with_output` still returns. Any *other* write - // error is a real problem and still panics. - match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { - Ok(()) => {} - Err(e) if e.kind() == ErrorKind::BrokenPipe => {} - Err(e) => panic!("failed to write to stdin: {e}"), - } - - child.wait_with_output().expect("failed to wait for bc-rust") + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + // Drain stdout and stderr first: the writer may still be blocked on a full stdin buffer, and it + // cannot finish until the child consumes more, which it cannot do while its output is backed up. + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output } /// Runs a command that is expected to succeed, returning stdout. @@ -136,6 +162,45 @@ fn pseudo_random(len: usize, seed: u32) -> Vec { .collect() } +// ---- the harness itself ------------------------------------------------------------------ +// +// These two pin `run`'s pipe handling. Both bugs they cover are timing-dependent: they pass on a +// fast machine with a small payload and fail on a slow or loaded runner, which is exactly how the +// first one reached CI. Forcing the condition with an oversized payload makes them deterministic +// instead of waiting for a bad day. The same pair exists in `aes_cbc_cli_tests.rs`, because each +// file has its own copy of `run`. + +/// Far beyond any pipe buffer, so a write cannot complete before the child has drained it. +const OVERSIZED: usize = 4 * 1024 * 1024; + +/// An error path must not take the harness down with it. +/// +/// `encrypt` with no `--key` prints its complaint and exits without reading stdin, so the write +/// loses the race and the pipe breaks. Before `run` tolerated `ErrorKind::BrokenPipe` this panicked +/// with "failed to write to stdin" (os error 109 on Windows, EPIPE elsewhere) instead of reporting +/// the CLI's actual error, which is what the other error-path tests assert on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let stderr = run_err(&["aes128-cfb", "encrypt"], &vec![0u8; OVERSIZED]); + assert!(stderr.contains("--key"), "the CLI's own error must still be reported: {stderr}"); +} + +/// A payload larger than the pipe buffer must round-trip rather than deadlock. +/// +/// This is the reason `run` writes stdin from a separate thread. Writing it inline wedges once both +/// pipes fill: the child blocks writing stdout, so it stops reading stdin, so the harness blocks +/// writing stdin. Nothing times out on its own -- the test just hangs until CI kills the job -- so +/// this is the check that would have caught it. +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + let plaintext = pseudo_random(OVERSIZED, 0xC0FFEE); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len() + 16, "IV plus the ciphertext"); + + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{OVERSIZED} bytes should round trip"); +} + // ---- the SP 800-38A F.3 vectors, through the CLI ----------------------------------------- /// `decrypt` reproduces the spec plaintext when handed the spec's IV followed by the spec's From f07e73edd857c9407c0444867beec227d1bcdec2 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 3 Sep 2026 17:33:38 +1000 Subject: [PATCH 4/4] modes: port Cfb, its tests, benches, docs and the CLI to the in-place API Follows ede3b9e (core: block cipher data methods work in place) and 60e0cc0 (core: fold BlockCipher into Algorithm) on feature/aes-tweaks: * Cfb implements Algorithm (its permutation's name and strength) and the in-place hooks do_encrypt_blocks / do_decrypt_blocks. decrypt_pair builds the two cipher inputs in its keystream buffer, so the only extra copy is C_{j+1} for the chaining value. * block_mode_cmd streams through a mutable buffer and writes it back out after transforming it, with no separate output buffer. * The CFB tests and benches use the same by-value helpers and iter_batched setup as the CBC ones; the AES_CFB_* doctests and crate docs show the in-place calls. Co-Authored-By: Claude Fable 5.1 --- cli/src/block_mode_cmd.rs | 57 +++--- crypto/aes-lowmemory/src/cfb.rs | 36 ++-- crypto/modes/benches/modes_benches.rs | 222 +++++++++++++--------- crypto/modes/src/cfb.rs | 108 ++++++----- crypto/modes/src/lib.rs | 9 +- crypto/modes/tests/acvp_cfb_tests.rs | 24 ++- crypto/modes/tests/cfb_tests.rs | 131 +++++++------ crypto/modes/tests/common/mod.rs | 3 +- crypto/modes/tests/sp800_38a_cfb_tests.rs | 101 +++++----- 9 files changed, 376 insertions(+), 315 deletions(-) diff --git a/cli/src/block_mode_cmd.rs b/cli/src/block_mode_cmd.rs index bb21795c..e52ef826 100644 --- a/cli/src/block_mode_cmd.rs +++ b/cli/src/block_mode_cmd.rs @@ -53,9 +53,9 @@ pub(crate) const BLOCK_LEN: usize = 16; /// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. /// -/// A full chunk goes through `do_*_out::` in one call, which for decryption means 32 -/// pairs down the mode's two-block path. The at-most-63-block tail at end of input goes one block -/// at a time; it is bounded, so its cost does not scale with the input. +/// A full chunk goes through `do_*::` in one call, in place, which for decryption means +/// 32 pairs down the mode's two-block path. The at-most-63-block tail at end of input goes one +/// block at a time; it is bounded, so its cost does not scale with the input. pub(crate) const CHUNK_LEN: usize = 64 * BLOCK_LEN; /// Which direction to run. Shared by every mode subcommand. @@ -151,20 +151,18 @@ pub(crate) fn encrypt_stream( // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. write_bytes_or_hex(&iv, output_hex); - let mut out = [0u8; CHUNK_LEN]; - - stream_aligned(mode, |data| match <&[u8; CHUNK_LEN]>::try_from(data) { - Ok(chunk) => { - // Cannot fail: the mode's block methods are infallible for a constructed value. - enc.do_encrypt_out(chunk, &mut out).unwrap(); - write_bytes_or_hex(&out, output_hex); - } - Err(_) => { + // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + stream_aligned(mode, |data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { + // Cannot fail: neither mode has a per-IV data limit. + enc.do_encrypt(chunk).unwrap(); + } else { // The bounded tail at end of input: whole blocks, fewer than a chunk. - for block in data.as_chunks::().0 { - write_bytes_or_hex(&enc.do_encrypt(block).unwrap(), output_hex); + for block in data.as_chunks_mut::().0 { + enc.do_encrypt(block).unwrap(); } } + write_bytes_or_hex(data, output_hex); }); finish(output_hex); @@ -193,32 +191,29 @@ pub(crate) fn decrypt_stream( exit(-1); }); - let mut out = [0u8; CHUNK_LEN]; - - stream_aligned(mode, |data| match <&[u8; CHUNK_LEN]>::try_from(data) { - Ok(chunk) => { + stream_aligned(mode, |data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { // A full chunk is 32 pairs, so this is the mode's two-block path. - dec.do_decrypt_out(chunk, &mut out).unwrap(); - write_bytes_or_hex(&out, output_hex); - } - Err(_) => { - for block in data.as_chunks::().0 { - write_bytes_or_hex(&dec.do_decrypt(block).unwrap(), output_hex); + dec.do_decrypt(chunk).unwrap(); + } else { + for block in data.as_chunks_mut::().0 { + dec.do_decrypt(block).unwrap(); } } + write_bytes_or_hex(data, output_hex); }); finish(output_hex); } -/// Reads stdin and hands it to `process` in block-aligned pieces: a full `CHUNK_LEN` bytes each time -/// one has accumulated, then once more at end of input with whatever whole blocks remain (fewer -/// than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the +/// Reads stdin and hands it to `process` in block-aligned pieces, mutably so it can be transformed +/// in place: a full `CHUNK_LEN` bytes each time one has accumulated, then once more at end of input +/// with whatever whole blocks remain (fewer than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the /// buffer until it is full -- so a block split across two reads needs no special handling. /// /// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because neither mode is /// defined on a partial block and these commands do not pad. -fn stream_aligned(mode: &str, mut process: impl FnMut(&[u8])) { +fn stream_aligned(mode: &str, mut process: impl FnMut(&mut [u8])) { let mut buf = [0u8; CHUNK_LEN]; let mut filled = 0usize; @@ -232,12 +227,12 @@ fn stream_aligned(mode: &str, mut process: impl FnMut(&[u8])) { } filled += n; if filled == CHUNK_LEN { - process(&buf); + process(&mut buf); filled = 0; } } - if filled % BLOCK_LEN != 0 { + if !filled.is_multiple_of(BLOCK_LEN) { eprintln!( "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ {mode} is defined only on whole blocks (SP 800-38A Sec 5.2), and these commands apply \ @@ -247,7 +242,7 @@ fn stream_aligned(mode: &str, mut process: impl FnMut(&[u8])) { exit(-1); } if filled != 0 { - process(&buf[..filled]); + process(&mut buf[..filled]); } } diff --git a/crypto/aes-lowmemory/src/cfb.rs b/crypto/aes-lowmemory/src/cfb.rs index aa2c0075..5549188c 100644 --- a/crypto/aes-lowmemory/src/cfb.rs +++ b/crypto/aes-lowmemory/src/cfb.rs @@ -14,7 +14,8 @@ use bouncycastle_modes::Cfb; /// AES-128 in CFB128 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or /// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. /// -/// The IV is generated by encryption and returned alongside the ciphertext; it is never supplied. +/// The IV is generated by encryption and returned; it is never supplied. Encryption and decryption +/// work in place. /// /// ``` /// use bouncycastle_aes_lowmemory::AES_CFB_128; @@ -26,16 +27,23 @@ use bouncycastle_modes::Cfb; /// .expect("a 16-byte symmetric cipher key"); /// // 48 bytes: three whole blocks. The length is checked at compile time. /// let message = [0u8; 48]; -/// let (iv, ciphertext) = AES_CFB_128::::encrypt(&key, &message).unwrap(); -/// assert_eq!(AES_CFB_128::::decrypt(&key, &iv, &ciphertext).unwrap(), message); +/// let mut data = message; +/// let iv = AES_CFB_128::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// AES_CFB_128::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); /// /// // Streaming, a few blocks at a time: /// let (mut enc, iv) = AES_CFB_128::::do_encrypt_init(&key).unwrap(); -/// let first = enc.do_encrypt(&[0u8; 16]).unwrap(); -/// let rest = enc.do_encrypt(&[1u8; 32]).unwrap(); +/// let mut first = [0u8; 16]; +/// let mut rest = [1u8; 32]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); /// let mut dec = AES_CFB_128::::do_decrypt_init(&key, &iv).unwrap(); -/// assert_eq!(dec.do_decrypt(&first).unwrap(), [0u8; 16]); -/// assert_eq!(dec.do_decrypt(&rest).unwrap(), [1u8; 32]); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 16]); +/// assert_eq!(rest, [1u8; 32]); /// ``` /// /// A length that is not a whole number of blocks is a **compile** error, not a runtime one: @@ -48,7 +56,7 @@ use bouncycastle_modes::Cfb; /// /// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); /// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. -/// let _ = AES_CFB_128::::encrypt(&key, &[0u8; 47]); +/// let _ = AES_CFB_128::::encrypt(&key, &mut [0u8; 47]); /// ``` #[allow(non_camel_case_types)] pub type AES_CFB_128 = Cfb; @@ -62,8 +70,10 @@ pub type AES_CFB_128 = Cfb; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CFB_192::::encrypt(&key, &[0u8; 32]).unwrap(); -/// assert_eq!(AES_CFB_192::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// let mut data = [0u8; 32]; +/// let iv = AES_CFB_192::::encrypt(&key, &mut data).unwrap(); +/// AES_CFB_192::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CFB_192 = Cfb; @@ -77,8 +87,10 @@ pub type AES_CFB_192 = Cfb; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); -/// let (iv, ct) = AES_CFB_256::::encrypt(&key, &[0u8; 32]).unwrap(); -/// assert_eq!(AES_CFB_256::::decrypt(&key, &iv, &ct).unwrap(), [0u8; 32]); +/// let mut data = [0u8; 32]; +/// let iv = AES_CFB_256::::encrypt(&key, &mut data).unwrap(); +/// AES_CFB_256::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); /// ``` #[allow(non_camel_case_types)] pub type AES_CFB_256 = Cfb; diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index 1df62105..da8d0440 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -292,99 +292,141 @@ fn bench_cfb_aes128(c: &mut Criterion) { // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- group.bench_function("16KiB encrypt -- N=1", |b| { - b.iter(|| { - let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); - for block in blocks.iter() { - black_box(enc.do_encrypt(block).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + for block in scratch.iter_mut() { + enc.do_encrypt(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB encrypt -- N=8", |b| { - b.iter(|| { - let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); - for chunk in blocks.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(enc.do_encrypt(arr).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // ---- decryption: parallel, and uses `encrypt_blocks2` -- the FORWARD pair method ---- let (mut enc, iv) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); - let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks - .chunks_exact(8) - .flat_map(|chunk| { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 8]; - enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); - out - }) - .collect(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).unwrap(); + } // N=1 never forms a pair, so this is the single-block path: the ratio against encrypt should // be about 1. group.bench_function("16KiB decrypt -- N=1 (no pairing)", |b| { - b.iter(|| { - let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for block in ciphertext.iter() { - black_box(dec.do_decrypt(block).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for block in scratch.iter_mut() { + dec.do_decrypt(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // N=2 and N=8 are all pairs, so every block goes through encrypt_blocks2. group.bench_function("16KiB decrypt -- N=2 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(2) { - let arr: &[u8; 2 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(2) { + let arr: &mut [u8; 2 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // N=9 is four pairs plus a one-block remainder, so it exercises the tail path too. group.bench_function("16KiB decrypt -- N=9 (pairs + remainder)", |b| { - b.iter(|| { - let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(9) { - let arr: &[u8; 9 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(9) { + let arr: &mut [u8; 9 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); // The controlled comparison: identical N, identical cipher, pair methods overridden vs not. // This pair of numbers -- and only this pair -- measures what `encrypt_blocks2` buys CFB. group.bench_function("16KiB decrypt -- N=8, pair path (blocks2 overridden)", |b| { - b.iter(|| { - let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.bench_function("16KiB decrypt -- N=8, no pair path (trait default)", |b| { - b.iter(|| { - let mut dec = UnpairedAes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = UnpairedAes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.finish(); @@ -398,34 +440,42 @@ fn bench_cfb_aes256(c: &mut Criterion) { group.throughput(Throughput::Bytes(DATA_LEN as u64)); group.bench_function("16KiB encrypt -- N=8", |b| { - b.iter(|| { - let (mut enc, _) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); - for chunk in blocks.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(enc.do_encrypt(arr).unwrap()); - } - }) + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); let (mut enc, iv) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); - let ciphertext: Vec<[u8; BLOCK_LEN]> = blocks - .chunks_exact(8) - .flat_map(|chunk| { - let arr: &[[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 8]; - enc.do_encrypt_blocks_out(arr, &mut out).unwrap(); - out - }) - .collect(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).unwrap(); + } group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { - b.iter(|| { - let mut dec = Aes256Cfb::::do_decrypt_init(&k, &iv).unwrap(); - for chunk in ciphertext.chunks_exact(8) { - let arr: &[u8; 8 * BLOCK_LEN] = chunk.as_flattened().try_into().unwrap(); - black_box(dec.do_decrypt(arr).unwrap()); - } - }) + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes256Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) }); group.finish(); diff --git a/crypto/modes/src/cfb.rs b/crypto/modes/src/cfb.rs index 7c7f24a4..e8e82508 100644 --- a/crypto/modes/src/cfb.rs +++ b/crypto/modes/src/cfb.rs @@ -77,8 +77,7 @@ use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{ - BlockCipher, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, - SecurityStrength, + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockPermutation, RNG, SecurityStrength, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -128,29 +127,30 @@ where o } - /// `Cj = Pj XOR Oj`, then `Cj` becomes the next input block. + /// `Cj = Pj XOR Oj` in place, then `Cj` becomes the next input block. #[inline] - fn encrypt_one(&mut self, plaintext: &[u8; BLOCK_LEN], ciphertext: &mut [u8; BLOCK_LEN]) { + fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { let o = self.keystream(); - for (out, (p, o)) in ciphertext.iter_mut().zip(plaintext.iter().zip(o.iter())) { - *out = *p ^ *o; + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; } // I_{j+1} = Cj. Serial: this is the input to the next cipher call. - self.chain = *ciphertext; + self.chain = *block; } - /// `Pj = Cj XOR Oj`, then `Cj` -- the *ciphertext*, not the recovered plaintext -- becomes the - /// next input block. + /// `Pj = Cj XOR Oj` in place, then `Cj` -- the *ciphertext*, not the recovered plaintext -- + /// becomes the next input block. `Cj` is overwritten by `Pj`, so it is copied first. #[inline] - fn decrypt_one(&mut self, ciphertext: &[u8; BLOCK_LEN], plaintext: &mut [u8; BLOCK_LEN]) { - let o = self.keystream(); - for (out, (c, o)) in plaintext.iter_mut().zip(ciphertext.iter().zip(o.iter())) { - *out = *c ^ *o; - } + fn decrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { // `I_{j+1} = C#_j` of the spec equations: the ciphertext segment is what is fed back. // Feeding back the plaintext instead would still decrypt the first block correctly and // nothing after it, which is why `cfb_tests.rs` checks exactly that. - self.chain = *ciphertext; + let cj = *block; + let o = self.keystream(); + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + self.chain = cj; } /// Decrypts two consecutive blocks with one [`BlockPermutation::encrypt_blocks2`] call. @@ -167,34 +167,36 @@ where /// just `Cj`, which the caller supplied -- so the two forward ciphers are independent and /// computing them together changes nothing. This is precisely the parallelism Sec 6.3 describes, /// with the input blocks "first constructed (in series) from the IV and the ciphertext". + /// + /// In place: the two input blocks are the keystream buffer, so the ciphertext is never + /// overwritten before it has been read, and only `Cj+1` needs copying for the chaining value. #[inline] - fn decrypt_pair( - &mut self, - ciphertext: &[[u8; BLOCK_LEN]; 2], - plaintext: &mut [[u8; BLOCK_LEN]; 2], - ) { + fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { // The two input blocks, constructed in series: Ij (already held) and Ij+1 (= Cj). - let mut o = [self.chain, ciphertext[0]]; + let mut o = [self.chain, blocks[0]]; self.perm.encrypt_blocks2(&mut o); - for ((out, c), o) in plaintext.iter_mut().zip(ciphertext.iter()).zip(o.iter()) { - for ((out, c), o) in out.iter_mut().zip(c.iter()).zip(o.iter()) { - *out = *c ^ *o; + // I_{j+2} = Cj+1, read before the XOR below turns it into Pj+1. + self.chain = blocks[1]; + + for (block, o) in blocks.iter_mut().zip(o.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; } } - - // I_{j+2} = Cj+1. - self.chain = ciphertext[1]; } } -impl BlockCipher +impl Algorithm for Cfb where P: BlockPermutation, { + /// The underlying permutation's name. The mode is not appended: `&'static str`s cannot be + /// concatenated in a `const`, and the mode is already in the type. + const ALG_NAME: &'static str = P::ALG_NAME; /// A mode does not change the strength of the underlying cipher. - const MAX_SECURITY_STRENGTH: SecurityStrength =

::MAX_SECURITY_STRENGTH; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } impl @@ -221,19 +223,18 @@ where Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) } - /// The implementor hook (the flat `do_encrypt[_out]` are provided over it). + /// The implementor hook (the flat `do_encrypt` is provided over it). /// /// Strictly serial: `Oj+1 = CIPH_K(Cj)` and `Cj` is the *output* of the previous cipher call, so - /// there is no pair path here. See the module docs. - fn do_encrypt_blocks_out( + /// there is no pair path here. See the module docs. Never fails: CFB has no per-IV data limit. + fn do_encrypt_blocks( &mut self, - plaintext: &[[u8; BLOCK_LEN]; N], - ciphertext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result { - for (p, c) in plaintext.iter().zip(ciphertext.iter_mut()) { - self.encrypt_one(p, c); + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + self.encrypt_one(block); } - Ok(N * BLOCK_LEN) + Ok(()) } } @@ -253,27 +254,24 @@ where Ok(Self { perm, chain: *init_data, _dir: PhantomData }) } - /// The implementor hook (the flat `do_decrypt[_out]` are provided over it). + /// The implementor hook (the flat `do_decrypt` is provided over it). /// /// Walks the input in pairs so the permutation's two-block *forward* path is used, with an - /// at-most-one block remainder for odd `N`. `as_chunks` splits into exactly that shape with no - /// runtime length check and no indexing arithmetic; `N` is a compile-time constant, so for even - /// `N` the tail loop is empty and for `N = 1` the pair loop is. - fn do_decrypt_blocks_out( + /// at-most-one block remainder for odd `N`. `as_chunks_mut` splits into exactly that shape with + /// no runtime length check and no indexing arithmetic; `N` is a compile-time constant, so for + /// even `N` the tail loop is empty and for `N = 1` the pair loop is. Never fails: CFB has no + /// per-IV data limit. + fn do_decrypt_blocks( &mut self, - ciphertext: &[[u8; BLOCK_LEN]; N], - plaintext: &mut [[u8; BLOCK_LEN]; N], - ) -> Result { - let (ct_pairs, ct_tail) = ciphertext.as_chunks::<2>(); - let (pt_pairs, pt_tail) = plaintext.as_chunks_mut::<2>(); - - for (ct_pair, pt_pair) in ct_pairs.iter().zip(pt_pairs.iter_mut()) { - self.decrypt_pair(ct_pair, pt_pair); + blocks: &mut [[u8; BLOCK_LEN]; N], + ) -> Result<(), SymmetricCipherError> { + let (pairs, tail) = blocks.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.decrypt_pair(pair); } - for (c, p) in ct_tail.iter().zip(pt_tail.iter_mut()) { - self.decrypt_one(c, p); + for block in tail.iter_mut() { + self.decrypt_one(block); } - - Ok(N * BLOCK_LEN) + Ok(()) } } diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 133d08d0..9c1a9a9e 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -105,13 +105,16 @@ //! .expect("a 16-byte symmetric cipher key"); //! let plaintext = [0x5Au8; 32]; //! -//! let (iv, ciphertext) = Aes128Cfb::::encrypt(&key, &plaintext).expect("encryption"); -//! let recovered = Aes128Cfb::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! let mut ciphertext = plaintext; +//! let iv = Aes128Cfb::::encrypt(&key, &mut ciphertext).expect("encryption"); +//! let mut recovered = ciphertext; +//! Aes128Cfb::::decrypt(&key, &iv, &mut recovered).expect("decryption"); //! assert_eq!(recovered, plaintext); //! //! // The modes are not interchangeable: a ciphertext must be decrypted with the mode that //! // produced it, and nothing at the type level stops you getting that wrong. -//! let as_if_cbc = Aes128Cbc::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! let mut as_if_cbc = ciphertext; +//! Aes128Cbc::::decrypt(&key, &iv, &mut as_if_cbc).expect("decryption"); //! assert_ne!(as_if_cbc, plaintext); //! ``` //! diff --git a/crypto/modes/tests/acvp_cfb_tests.rs b/crypto/modes/tests/acvp_cfb_tests.rs index 39ec1bc9..8d965fb7 100644 --- a/crypto/modes/tests/acvp_cfb_tests.rs +++ b/crypto/modes/tests/acvp_cfb_tests.rs @@ -129,18 +129,22 @@ where match grouping { Grouping::Single => { for block in input { - out.push(enc.do_encrypt(block).unwrap()); + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - let mut c = [[0u8; BLOCK_LEN]; 2]; - enc.do_encrypt_blocks_out(pair, &mut c).unwrap(); + let mut c = *pair; + enc.do_encrypt_blocks(&mut c).unwrap(); out.extend_from_slice(&c); } for block in tail { - out.push(enc.do_encrypt(block).unwrap()); + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); } } } @@ -151,18 +155,22 @@ where match grouping { Grouping::Single => { for block in input { - out.push(dec.do_decrypt(block).unwrap()); + let mut p = *block; + dec.do_decrypt(&mut p).unwrap(); + out.push(p); } } Grouping::Pairs => { let (pairs, tail) = input.as_chunks::<2>(); for pair in pairs { - let mut p = [[0u8; BLOCK_LEN]; 2]; - dec.do_decrypt_blocks_out(pair, &mut p).unwrap(); + let mut p = *pair; + dec.do_decrypt_blocks(&mut p).unwrap(); out.extend_from_slice(&p); } for block in tail { - out.push(dec.do_decrypt(block).unwrap()); + let mut p = *block; + dec.do_decrypt(&mut p).unwrap(); + out.push(p); } } } diff --git a/crypto/modes/tests/cfb_tests.rs b/crypto/modes/tests/cfb_tests.rs index 04b05ba7..9a6cf1d0 100644 --- a/crypto/modes/tests/cfb_tests.rs +++ b/crypto/modes/tests/cfb_tests.rs @@ -25,24 +25,44 @@ type ToyCfb

= Cfb; type SwappedCfb = Cfb; type ForwardOnlyCfb = Cfb; -/// The implementor hook `do_encrypt_blocks_out`, by value, for tests whose data is block-shaped. +/// The implementor hook `do_encrypt_blocks`, by value, for tests whose data is block-shaped. fn enc_blocks( enc: &mut impl BlockCipherEncryptor, plaintext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { - let mut ct = [[0u8; TOY_LEN]; N]; - enc.do_encrypt_blocks_out(plaintext, &mut ct).unwrap(); - ct + let mut blocks = *plaintext; + enc.do_encrypt_blocks(&mut blocks).unwrap(); + blocks } -/// The implementor hook `do_decrypt_blocks_out`, by value. +/// The implementor hook `do_decrypt_blocks`, by value. fn dec_blocks( dec: &mut impl BlockCipherDecryptor, ciphertext: &[[u8; TOY_LEN]; N], ) -> [[u8; TOY_LEN]; N] { - let mut pt = [[0u8; TOY_LEN]; N]; - dec.do_decrypt_blocks_out(ciphertext, &mut pt).unwrap(); - pt + let mut blocks = *ciphertext; + dec.do_decrypt_blocks(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt(&mut data).unwrap(); + data } /// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the @@ -135,17 +155,13 @@ fn the_mode_matches_the_spec_equations() { // Anchor 2: with `P1 = 0`, `C1 = O1`. CFB is a keystream mode, and this is what that means. let (mut enc, _) = ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); - assert_eq!( - enc.do_encrypt(&[0u8; TOY_LEN]).unwrap(), - o1, - "encrypting zero yields the keystream" - ); + assert_eq!(enc_flat(&mut enc, &[0u8; TOY_LEN]), o1, "encrypting zero yields the keystream"); // ...and CFB is not CBC: CBC computes `CIPH_K(P1 XOR IV)`, CFB computes `P1 XOR CIPH_K(IV)`. let (mut cbc, _) = Cbc::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) .unwrap(); - assert_ne!(cbc.do_encrypt(&plaintext[0]).unwrap(), ct[0], "CFB must not agree with CBC"); + assert_ne!(enc_flat(&mut cbc, &plaintext[0]), ct[0], "CFB must not agree with CBC"); } // ---- the forward-cipher-only rule --------------------------------------------------------- @@ -175,7 +191,7 @@ fn neither_direction_uses_the_inverse_cipher() { // The single-block path. let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); for (c, p) in ct.iter().zip(plaintext.iter()) { - assert_eq!(&dec.do_decrypt(c).unwrap(), p, "single-block path, forward cipher only"); + assert_eq!(&dec_flat(&mut dec, c), p, "single-block path, forward cipher only"); } // N = 3 leaves a remainder after the pair loop, so both paths run in one call. @@ -239,7 +255,7 @@ fn call_grouping_does_not_change_the_result() { let (mut enc, _) = ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); let mut got = [[0u8; TOY_LEN]; 8]; - let a = enc.do_encrypt(&plaintext[0]).unwrap(); // one block, flat + let a = enc_flat(&mut enc, &plaintext[0]); // one block, flat let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 let c = enc_blocks(&mut enc, &[plaintext[3], plaintext[4], plaintext[5]]); // N = 3 let d = enc_blocks(&mut enc, &[plaintext[6], plaintext[7]]); // N = 2 @@ -263,7 +279,7 @@ fn call_grouping_does_not_change_the_result() { while at < 8 { match grouping { 1 => { - out[at] = dec.do_decrypt(&ct[at]).unwrap(); + out[at] = dec_flat(&mut dec, &ct[at]); } 2 => { let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1]]); @@ -287,7 +303,7 @@ fn call_grouping_does_not_change_the_result() { assert_eq!(five, [plaintext[3], plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); } -/// The pair path in `do_decrypt_blocks_out` must actually be taken. +/// The pair path in `do_decrypt_blocks` must actually be taken. /// /// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block methods /// are correct. CFB decryption pairs through `encrypt_blocks2`, so with this permutation a pair @@ -323,13 +339,12 @@ fn the_pair_path_is_really_used() { // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. let mut dec = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); - let p0 = dec.do_decrypt(&swapped_ct[0]).unwrap(); - let p1 = dec.do_decrypt(&swapped_ct[1]).unwrap(); + let p0 = dec_flat(&mut dec, &swapped_ct[0]); + let p1 = dec_flat(&mut dec, &swapped_ct[1]); assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); } -/// The flat streaming method must agree with the block-shaped implementor hook and report the -/// byte count. +/// The flat streaming method must agree with the block-shaped implementor hook. #[test] fn flat_streaming_agrees_with_the_block_hook() { let key = toy_key(); @@ -339,25 +354,22 @@ fn flat_streaming_agrees_with_the_block_hook() { let (mut enc, _) = ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); - let by_value = enc.do_encrypt(&flat_plaintext).unwrap(); + let flat_ct = enc_flat(&mut enc, &flat_plaintext); let (mut enc, _) = ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); - let mut out = [[0u8; TOY_LEN]; 3]; - let n = enc.do_encrypt_blocks_out(&plaintext, &mut out).unwrap(); - assert_eq!(n, 3 * TOY_LEN); - assert_eq!(*out.as_flattened(), by_value, "flat streaming must equal the block hook"); + let block_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(*block_ct.as_flattened(), flat_ct, "flat streaming must equal the block hook"); let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); - let mut back = [[0u8; TOY_LEN]; 3]; - let n = dec.do_decrypt_blocks_out(&out, &mut back).unwrap(); - assert_eq!(n, 3 * TOY_LEN); - assert_eq!(back, plaintext); + assert_eq!(dec_blocks(&mut dec, &block_ct), plaintext); + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_flat(&mut dec, &flat_ct), flat_plaintext); } -/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`) must produce exactly what the streaming -/// API produces over the same blocks, for an odd block count (pairs plus a one-block tail) and an -/// even one (pairs only), in both directions and through the `_out` variants. +/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`, in place) must produce exactly what the +/// streaming API produces over the same blocks, for an odd block count (pairs plus a one-block +/// tail) and an even one (pairs only), in both directions. #[test] fn one_shots_agree_with_the_streaming_api() { let key = toy_key(); @@ -372,23 +384,12 @@ fn one_shots_agree_with_the_streaming_api() { ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); (got, enc_blocks(&mut enc, &blocks3)) }; - let (iv_b, ct_flat) = - ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &flat3).unwrap(); + let mut buf = flat3; + let iv_b = ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &mut buf).unwrap(); assert_eq!(iv_a, iv_b); - assert_eq!(ct_flat, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); - assert_eq!(ToyCfb::::decrypt(&key, &iv, &ct_flat).unwrap(), flat3); - - let mut ct_out = [0u8; 3 * TOY_LEN]; - let (_, n) = - ToyCfb::::encrypt_out_rng(&key, &mut pinned_rng(iv), &flat3, &mut ct_out) - .unwrap(); - assert_eq!((n, ct_out), (3 * TOY_LEN, ct_flat)); - let mut pt_out = [0u8; 3 * TOY_LEN]; - assert_eq!( - ToyCfb::::decrypt_out(&key, &iv, &ct_out, &mut pt_out).unwrap(), - 3 * TOY_LEN - ); - assert_eq!(pt_out, flat3); + assert_eq!(buf, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + ToyCfb::::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat3); // 4 blocks = 64 bytes: pairs only, no tail. let flat4: [u8; 4 * TOY_LEN] = core::array::from_fn(|i| (i * 13 + 1) as u8); @@ -399,14 +400,18 @@ fn one_shots_agree_with_the_streaming_api() { ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); enc_blocks(&mut enc, &blocks4) }; - let (_, ct_flat) = - ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &flat4).unwrap(); - assert_eq!(ct_flat, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); - assert_eq!(ToyCfb::::decrypt(&key, &iv, &ct_flat).unwrap(), flat4); + let mut buf = flat4; + ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &mut buf).unwrap(); + assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + ToyCfb::::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat4); // The OS-RNG variant round-trips too. - let (iv_fresh, ct) = ToyCfb::::encrypt(&key, &flat3).unwrap(); - assert_eq!(ToyCfb::::decrypt(&key, &iv_fresh, &ct).unwrap(), flat3); + let mut buf = flat3; + let iv_fresh = ToyCfb::::encrypt(&key, &mut buf).unwrap(); + assert_ne!(buf, flat3); + ToyCfb::::decrypt(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, flat3); } // ---- SP 800-38A Appendix D error propagation --------------------------------------------- @@ -476,8 +481,8 @@ fn an_iv_bit_error_randomises_only_the_first_block() { Aes128Cfb::::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(iv)) .unwrap(); assert_eq!(got_iv, iv); - let mut ct = [[0u8; LEN]; 3]; - enc.do_encrypt_blocks_out(&plaintext, &mut ct).unwrap(); + let mut ct = plaintext; + enc.do_encrypt_blocks(&mut ct).unwrap(); let mut first_blocks = std::collections::BTreeSet::new(); @@ -487,8 +492,8 @@ fn an_iv_bit_error_randomises_only_the_first_block() { corrupt_iv[byte] ^= 1 << bit; let mut dec = Aes128Cfb::::do_decrypt_init(&key, &corrupt_iv).unwrap(); - let mut got = [[0u8; LEN]; 3]; - dec.do_decrypt_blocks_out(&ct, &mut got).unwrap(); + let mut got = ct; + dec.do_decrypt_blocks(&mut got).unwrap(); // Only P1 is affected: with s = b, Appendix D's "first i/s (rounding up) ciphertext // segments" is one segment for every bit position i. @@ -537,8 +542,10 @@ fn identical_plaintext_gives_different_ciphertext() { let key = toy_key(); let plaintext = [0x77u8; 2 * TOY_LEN]; - let (_, first) = ToyCfb::::encrypt(&key, &plaintext).unwrap(); - let (_, second) = ToyCfb::::encrypt(&key, &plaintext).unwrap(); + let mut first = plaintext; + ToyCfb::::encrypt(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCfb::::encrypt(&key, &mut second).unwrap(); assert_ne!(first, second); // ...and, within one message, two identical plaintext blocks must not give identical ciphertext diff --git a/crypto/modes/tests/common/mod.rs b/crypto/modes/tests/common/mod.rs index 7bd9b033..cb526855 100644 --- a/crypto/modes/tests/common/mod.rs +++ b/crypto/modes/tests/common/mod.rs @@ -136,7 +136,8 @@ pub struct ForwardOnlyToy { inner: Toy, } -impl BlockCipher for ForwardOnlyToy { +impl Algorithm for ForwardOnlyToy { + const ALG_NAME: &'static str = "ForwardOnlyToy"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } diff --git a/crypto/modes/tests/sp800_38a_cfb_tests.rs b/crypto/modes/tests/sp800_38a_cfb_tests.rs index ff04c1b6..34baba94 100644 --- a/crypto/modes/tests/sp800_38a_cfb_tests.rs +++ b/crypto/modes/tests/sp800_38a_cfb_tests.rs @@ -122,7 +122,7 @@ fn key_material(hex_str: &str) -> KeyMaterial { /// Runs one Appendix F.3 encrypt subsection. /// /// Checks the whole message in one call, then again one segment at a time, then again through the -/// `_out` variant -- the vector should not care how the calls are grouped. +/// implementor hook -- the vector should not care how the calls are grouped. fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) where P: BlockPermutation, @@ -139,11 +139,9 @@ where ) .unwrap(); assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); - assert_eq!( - enc.do_encrypt(&flat(&PLAINTEXTS)).unwrap(), - flat(expected), - "{section}: four segments in one call" - ); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four segments in one call"); // One segment at a time. let (mut enc, _) = Cfb::::do_encrypt_init_rng( @@ -152,26 +150,26 @@ where ) .unwrap(); for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { - let got = enc.do_encrypt(p).unwrap(); + let mut got = *p; + enc.do_encrypt(&mut got).unwrap(); assert_eq!(&got, c, "{section}: segment #{}", i + 1); } - // Through the implementor hook, `do_*_blocks_out`. + // Through the implementor hook, `do_*_blocks`. let (mut enc, _) = Cfb::::do_encrypt_init_rng( &key, &mut FixedSeedRNG::::new(iv), ) .unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 4]; - let n = enc.do_encrypt_blocks_out(&pt, &mut out).unwrap(); - assert_eq!(n, 4 * BLOCK_LEN); - assert_eq!(out, ct, "{section}: _out variant"); + let mut blocks = pt; + enc.do_encrypt_blocks(&mut blocks).unwrap(); + assert_eq!(blocks, ct, "{section}: implementor hook"); } /// Runs one Appendix F.3 decrypt subsection. /// /// Checks one call, one segment at a time, and the odd grouping `3 + 1` -- which is the grouping -/// that leaves a one-block remainder after the pair loop in `do_decrypt_blocks_out`. +/// that leaves a one-block remainder after the pair loop in `do_decrypt_blocks`. fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) where P: BlockPermutation, @@ -185,33 +183,32 @@ where // All four segments in one call (two pairs, no remainder). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - assert_eq!( - dec.do_decrypt(&flat(ciphertext)).unwrap(), - flat(&PLAINTEXTS), - "{section}: four segments in one call" - ); + let mut data = flat(ciphertext); + dec.do_decrypt(&mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: four segments in one call"); // One segment at a time (never takes the pair path). let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { - let got = dec.do_decrypt(c).unwrap(); + let mut got = *c; + dec.do_decrypt(&mut got).unwrap(); assert_eq!(&got, p, "{section}: segment #{}", i + 1); } // 3 + 1: one pair plus a remainder, then a lone block. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - let first_three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); - let three = dec.do_decrypt(&first_three).unwrap(); - let one = dec.do_decrypt(&ct[3]).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + dec.do_decrypt(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt(&mut one).unwrap(); assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: segments 1-3"); assert_eq!(one, pt[3], "{section}: segment 4"); - // Through the implementor hook, `do_*_blocks_out`. + // Through the implementor hook, `do_*_blocks`. let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); - let mut out = [[0u8; BLOCK_LEN]; 4]; - let n = dec.do_decrypt_blocks_out(&ct, &mut out).unwrap(); - assert_eq!(n, 4 * BLOCK_LEN); - assert_eq!(out, pt, "{section}: _out variant"); + let mut blocks = ct; + dec.do_decrypt_blocks(&mut blocks).unwrap(); + assert_eq!(blocks, pt, "{section}: implementor hook"); } #[test] @@ -245,39 +242,27 @@ fn f_3_18_cfb128_aes256_decrypt() { } /// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. -/// The one-shots take flat arrays, so the four segments are presented as 64 contiguous bytes. +/// The one-shots take flat arrays and work in place, so the four ciphertext segments are presented +/// as 64 contiguous bytes and become the four plaintext blocks. #[test] fn the_one_shot_api_matches_the_vectors() { let iv = block(IV); let pt = flat(&PLAINTEXTS); - assert_eq!( - Cfb::::decrypt( - &key_material::<16>(KEY_128), - &iv, - &flat(&CIPHERTEXTS_128) - ) - .unwrap(), - pt - ); - assert_eq!( - Cfb::::decrypt( - &key_material::<24>(KEY_192), - &iv, - &flat(&CIPHERTEXTS_192) - ) - .unwrap(), - pt - ); - assert_eq!( - Cfb::::decrypt( - &key_material::<32>(KEY_256), - &iv, - &flat(&CIPHERTEXTS_256) - ) - .unwrap(), - pt - ); + let mut data = flat(&CIPHERTEXTS_128); + Cfb::::decrypt(&key_material::<16>(KEY_128), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_192); + Cfb::::decrypt(&key_material::<24>(KEY_192), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_256); + Cfb::::decrypt(&key_material::<32>(KEY_256), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); } /// The spec's tabulated **Output Blocks** are the CFB keystream, and its **Input Blocks** are the @@ -368,10 +353,12 @@ fn cfb128_agrees_with_ofb_on_the_first_block_only() { .unwrap(); assert_eq!(got_iv, iv); - let c1 = enc.do_encrypt(&block(PLAINTEXTS[0])).unwrap(); + let mut c1 = block(PLAINTEXTS[0]); + enc.do_encrypt(&mut c1).unwrap(); assert_eq!(c1, block(OFB_CIPHERTEXT_1), "block 1 must match OFB, and F.3.13"); - let c2 = enc.do_encrypt(&block(PLAINTEXTS[1])).unwrap(); + let mut c2 = block(PLAINTEXTS[1]); + enc.do_encrypt(&mut c2).unwrap(); assert_eq!(c2, block(CIPHERTEXTS_128[1]), "block 2 must match F.3.13"); assert_ne!(c2, block(OFB_CIPHERTEXT_2), "block 2 must NOT match OFB"); }