diff --git a/Cargo.toml b/Cargo.toml index 2aaf82363..b5a5560bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } bouncycastle-sm2 = { path = "./crypto/sm2" } bouncycastle-sm3 = { path = "./crypto/sm3" } +bouncycastle-tdes = { path = "./crypto/tdes" } bouncycastle-utils = { path = "./crypto/utils" } @@ -86,3 +87,4 @@ bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true bouncycastle-sm2.workspace = true bouncycastle-sm3.workspace = true +bouncycastle-tdes.workspace = true diff --git a/alpha_0.1.4_release_notes.md b/alpha_0.1.4_release_notes.md index 97ad4dee3..bdf87fa74 100644 --- a/alpha_0.1.4_release_notes.md +++ b/alpha_0.1.4_release_notes.md @@ -3,6 +3,12 @@ ## Major features * CHACHA20 -- ChaCha20, Poly1305 and ChaCha20-Poly1305 (RFC 8439). +* TDES -- three-key TDEA (NIST SP 800-67 Rev 2) as a constant-time, table-free bit-sliced engine under `hazmat`, + with ECB, CBC, CFB64, CFB8 and CTR (SP 800-38A Appendix E) and TDEA Key Wrap (SP 800-38F TKW) through the key + role traits; two-key TDEA decrypts and unwraps only, which `BlockPermutation::DECRYPT_ONLY` makes a compile + error for every encrypting route; the `tdes-*` and `tdes2-*` CLI commands, whose `encrypt` refuses input past + SP 800-67 Rev 2 Sec 3.4's 2^20 blocks counted as the blocks TDEA processes (8 MiB; 1 MiB for CFB8, 699,048 + bytes for TKW). * AES Key Wrap -- AES_KW and AES_KWP (NIST SP 800-38F; RFC 3394 / RFC 5649) over any 128-bit block permutation, with the `KeyWrapper` / `KeyUnwrapper` core traits and the `aes*-kw` / `aes*-kwp` CLI commands. * ECDSA -- FIPS 186-5 signatures over P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1 and @@ -15,6 +21,10 @@ ## Minor features / bug fixes +* The key-wrap workers `Kw`, `Kwp` and `Tkw` carry their direction in the type, as the cipher modes do: + `KeyWrapper` is implemented for the encrypting worker only, so an unwrapper has no wrapping method and a + permutation that decrypts only is refused a wrapper at compile time by every route. The `AES_KW_*`, `AES_KWP_*` + and `TDES_TKW` aliases gain the `Dir` parameter; `TDES2_TKW` is the decrypting one. * Cipher and MAC workers no longer implement `SuspendableKeyed` (#173): a resumed worker is a second copy of a nonce, counter or keystream position. The AES modes, CMAC and Ascon-AEAD128 lose their suspended-state constants and `hazmat::ModeKey` goes with them; HMAC and HKDF still suspend, and hashes, XOFs and KMAC keep diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs index f5026eb86..07e9425c0 100644 --- a/cli/src/aes_cbc_cmd.rs +++ b/cli/src/aes_cbc_cmd.rs @@ -10,10 +10,9 @@ //! separately. use crate::helpers::block_mode_helpers::{ - BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, - start_encryptor, + CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, start_encryptor, }; -use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::aes::{AES_BLOCK_LEN, AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::CBC; use bouncycastle::cipher::padding::PKCS7; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; @@ -29,7 +28,7 @@ pub(crate) fn aes128_cbc_cmd( run::<_, 16>( action, &load_key_object::>, 16>(key, key_file, "AES-128"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -45,7 +44,7 @@ pub(crate) fn aes192_cbc_cmd( run::<_, 24>( action, &load_key_object::>, 24>(key, key_file, "AES-192"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -61,7 +60,7 @@ pub(crate) fn aes256_cbc_cmd( run::<_, 32>( action, &load_key_object::>, 32>(key, key_file, "AES-256"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -70,19 +69,27 @@ pub(crate) fn aes256_cbc_cmd( fn run( action: &CipherDirection, key: &K, - iv: Option<[u8; BLOCK_LEN]>, + iv: Option<[u8; AES_BLOCK_LEN]>, output_hex: bool, ) where - K: SymmetricEncryptionKey - + SymmetricDecryptionKey, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, { match action { CipherDirection::Encrypt => { let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng)); - encrypt_stream::<_, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None) - } - CipherDirection::Decrypt => { - decrypt_stream::<_, BLOCK_LEN>(iv, |iv| key.create_decryptor(iv), output_hex, None) + encrypt_stream::<_, AES_BLOCK_LEN, AES_BLOCK_LEN>( + enc, + generated_iv.as_ref(), + output_hex, + None, + ) } + CipherDirection::Decrypt => decrypt_stream::<_, AES_BLOCK_LEN, AES_BLOCK_LEN>( + iv, + |iv| key.create_decryptor(iv), + output_hex, + None, + ), } } diff --git a/cli/src/aes_cfb8_cmd.rs b/cli/src/aes_cfb8_cmd.rs index 450b8eb1c..39915594f 100644 --- a/cli/src/aes_cfb8_cmd.rs +++ b/cli/src/aes_cfb8_cmd.rs @@ -25,9 +25,9 @@ //! plaintext byte, corrupts the following 16 bytes, and then decryption resynchronises. Do not //! decrypt data you have not authenticated separately. -use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::aes::{AES_BLOCK_LEN, AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::CFB8; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; @@ -43,7 +43,7 @@ pub(crate) fn aes128_cfb8_cmd( run::<_, 16>( action, &load_key_object::, 16>(key, key_file, "AES-128"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -59,7 +59,7 @@ pub(crate) fn aes192_cfb8_cmd( run::<_, 24>( action, &load_key_object::, 24>(key, key_file, "AES-192"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -75,7 +75,7 @@ pub(crate) fn aes256_cfb8_cmd( run::<_, 32>( action, &load_key_object::, 32>(key, key_file, "AES-256"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -84,13 +84,13 @@ pub(crate) fn aes256_cfb8_cmd( fn run<'k, K, const KEY_LEN: usize>( action: &CipherDirection, key: &'k K, - iv: Option<[u8; BLOCK_LEN]>, + iv: Option<[u8; AES_BLOCK_LEN]>, output_hex: bool, ) where - K: SymmetricEncryptionKey - + SymmetricDecryptionKey, - K::Encryptor<'k>: StreamCipherEncryptor, - K::Decryptor<'k>: StreamCipherDecryptor, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { - run_stream_mode::(action, key, iv, output_hex) + run_stream_mode::(action, key, iv, output_hex) } diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs index d59398684..91fa0b895 100644 --- a/cli/src/aes_cfb_cmd.rs +++ b/cli/src/aes_cfb_cmd.rs @@ -26,9 +26,9 @@ //! 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::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_iv, load_key_object}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::aes::{AES_BLOCK_LEN, AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::CFB; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; @@ -44,7 +44,7 @@ pub(crate) fn aes128_cfb_cmd( run::<_, 16>( action, &load_key_object::, 16>(key, key_file, "AES-128"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -60,7 +60,7 @@ pub(crate) fn aes192_cfb_cmd( run::<_, 24>( action, &load_key_object::, 24>(key, key_file, "AES-192"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -76,7 +76,7 @@ pub(crate) fn aes256_cfb_cmd( run::<_, 32>( action, &load_key_object::, 32>(key, key_file, "AES-256"), - load_iv::(iv, iv_file), + load_iv::(iv, iv_file), output_hex, ); } @@ -85,13 +85,13 @@ pub(crate) fn aes256_cfb_cmd( fn run<'k, K, const KEY_LEN: usize>( action: &CipherDirection, key: &'k K, - iv: Option<[u8; BLOCK_LEN]>, + iv: Option<[u8; AES_BLOCK_LEN]>, output_hex: bool, ) where - K: SymmetricEncryptionKey - + SymmetricDecryptionKey, - K::Encryptor<'k>: StreamCipherEncryptor, - K::Decryptor<'k>: StreamCipherDecryptor, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { - run_stream_mode::(action, key, iv, output_hex) + run_stream_mode::(action, key, iv, output_hex) } diff --git a/cli/src/aes_ecb_cmd.rs b/cli/src/aes_ecb_cmd.rs index 4451abc35..6666bfe96 100644 --- a/cli/src/aes_ecb_cmd.rs +++ b/cli/src/aes_ecb_cmd.rs @@ -16,9 +16,9 @@ //! `aes*-cbc` or `aes*-cfb` under separate authentication, or better an AEAD. use crate::helpers::block_mode_helpers::{ - BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, + CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, }; -use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::aes::{AES_BLOCK_LEN, AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::hazmat::ecb::ECB; use bouncycastle::cipher::padding::NoPadding; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; @@ -69,16 +69,16 @@ pub(crate) fn aes256_ecb_cmd( /// the loops write and read no IV, and `NoPadding` makes them refuse a partial final block. fn run(action: &CipherDirection, key: &K, output_hex: bool) where - K: SymmetricEncryptionKey - + SymmetricDecryptionKey, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, { match action { CipherDirection::Encrypt => { // ECB has no IV: the constructor's RNG draws nothing, and nothing is written. let (enc, _) = start_encryptor(None, |rng| key.create_encryptor(rng)); - encrypt_stream::<_, 0>(enc, None, output_hex, Some(MODE)) + encrypt_stream::<_, 0, AES_BLOCK_LEN>(enc, None, output_hex, Some(MODE)) } - CipherDirection::Decrypt => decrypt_stream::<_, 0>( + CipherDirection::Decrypt => decrypt_stream::<_, 0, AES_BLOCK_LEN>( None, |no_iv| key.create_decryptor(no_iv), output_hex, diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs index 56d5ec7b1..d7b5f4323 100644 --- a/cli/src/aes_kw_cmd.rs +++ b/cli/src/aes_kw_cmd.rs @@ -44,7 +44,7 @@ //! | bc-rust aes128-kw -d encrypt --key 000102030405060708090a0b0c0d0e0f -x //! ``` -use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::block_mode_helpers::{CipherDirection, InputLimit, load_key_object}; use crate::helpers::write_bytes_or_hex; use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::{KW, KWP}; @@ -62,6 +62,7 @@ pub(crate) fn aes128_kw_cmd( run::<_, 16>( direction, &load_key_object::, 16>(key, key_file, "AES-128-KW"), + None, output_hex, "AES-128-KW", ); @@ -76,6 +77,7 @@ pub(crate) fn aes192_kw_cmd( run::<_, 24>( direction, &load_key_object::, 24>(key, key_file, "AES-192-KW"), + None, output_hex, "AES-192-KW", ); @@ -90,6 +92,7 @@ pub(crate) fn aes256_kw_cmd( run::<_, 32>( direction, &load_key_object::, 32>(key, key_file, "AES-256-KW"), + None, output_hex, "AES-256-KW", ); @@ -104,6 +107,7 @@ pub(crate) fn aes128_kwp_cmd( run::<_, 16>( direction, &load_key_object::, 16>(key, key_file, "AES-128-KWP"), + None, output_hex, "AES-128-KWP", ); @@ -118,6 +122,7 @@ pub(crate) fn aes192_kwp_cmd( run::<_, 24>( direction, &load_key_object::, 24>(key, key_file, "AES-192-KWP"), + None, output_hex, "AES-192-KWP", ); @@ -132,29 +137,52 @@ pub(crate) fn aes256_kwp_cmd( run::<_, 32>( direction, &load_key_object::, 32>(key, key_file, "AES-256-KWP"), + None, output_hex, "AES-256-KWP", ); } /// Reads all of stdin, wraps or unwraps it under `kek`, and writes the result. `alg` names the -/// algorithm in error messages. -fn run(direction: &CipherDirection, kek: &K, output_hex: bool, alg: &str) -where +/// algorithm in error messages. Shared with the other block ciphers' key-wrap commands. +pub(crate) fn run( + direction: &CipherDirection, + kek: &K, + limit: Option<&InputLimit>, + output_hex: bool, + alg: &str, +) where K: KeyWrappingKey + KeyUnwrappingKey, { + match direction { + CipherDirection::Encrypt => run_with(|input| kek.wrap(input), limit, output_hex, alg), + CipherDirection::Decrypt => run_with(|input| kek.unwrap(input), None, output_hex, alg), + } +} + +/// Reads all of stdin, applies `op` to it and writes the result; the unwrap-only commands pass +/// their key's `unwrap` alone, so that no wrapper is ever instantiated for their key type. +/// `limit`, for a wrapping command whose standard bounds what one key may protect, is checked +/// against the whole input before `op` runs. +pub(crate) fn run_with( + op: impl FnOnce(&[u8]) -> Result, SymmetricCipherError>, + limit: Option<&InputLimit>, + output_hex: bool, + alg: &str, +) { let mut input = Vec::new(); if let Err(e) = std::io::stdin().read_to_end(&mut input) { eprintln!("Error: couldn't read stdin: {e}"); exit(-1); } + if let Some(limit) = limit + && input.len() as u64 > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); + } - let result = match direction { - CipherDirection::Encrypt => kek.wrap(&input), - CipherDirection::Decrypt => kek.unwrap(&input), - }; - - match result { + match op(&input) { Ok(output) => write_bytes_or_hex(&output, output_hex), Err(SymmetricCipherError::DecryptionFailed) => { eprintln!( diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index 0c40ba5e9..3315cf269 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -70,11 +70,8 @@ use std::io; use std::io::{Read, Write}; use std::process::exit; -/// The AES block length in bytes. -pub(crate) const BLOCK_LEN: usize = 16; - /// Bytes read from stdin per call: 1 KiB = 64 blocks, shared with the stream-mode commands. -pub(crate) const CHUNK_LEN: usize = 64 * BLOCK_LEN; +pub(crate) const CHUNK_LEN: usize = 1024; /// Which direction to run. Shared by cipher subcommands, -- see the specific subcommand's /// own `--help` (`bc-rust aes128-ccm --help` and friends) for what `encrypt`/`decrypt` actually do @@ -87,6 +84,13 @@ pub(crate) enum CipherDirection { Decrypt, } +/// The one action a decryption-only command offers. A separate enum rather than a flag, so that +/// `tdes2-* encrypt` is rejected by the parser with the list of valid values, not by the command. +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum DecryptOnlyAction { + Decrypt, +} + /// Loads the key from `--key-file` (raw bytes; hex with `--hex-input`) or `--key` (hex), and /// checks its length. /// @@ -278,11 +282,70 @@ pub(crate) fn start_encryptor( /// the ciphertext; see [`start_encryptor`]. `aligned_mode` names a mode whose input must be a /// whole number of blocks, for the error that reports a partial one (ECB under `NoPadding`); it is /// `None` for a padded mode, which takes any length. -pub(crate) fn encrypt_stream( +pub(crate) fn encrypt_stream( + enc: E, + generated_iv: Option<&[u8; INIT_DATA_LEN]>, + output_hex: bool, + aligned_mode: Option<&str>, +) where + E: SymmetricCipherEncryptor, +{ + encrypt_stream_bounded::( + enc, generated_iv, output_hex, aligned_mode, None, + ) +} + +/// A bound on how much one invocation may encrypt under its key, for an algorithm whose +/// standard limits the data a key may protect: the command refuses input past `max_bytes` with +/// `message` on stderr and an error exit, before any byte past the bound is processed. +pub(crate) struct InputLimit { + pub(crate) max_bytes: u64, + pub(crate) message: &'static str, +} + +/// SP 800-67 Rev 2 Sec 3.4: "One key bundle shall not be used to apply cryptographic protection +/// (e.g., encrypt) more than 2^20 64-bit data blocks", 8 MiB for a mode that passes each 8 bytes +/// of input through TDEA once (ECB, CBC, CFB64, CTR). The bound counts blocks the cipher +/// processes, so a mode that invokes TDEA more often per byte has a lower figure below. The +/// command is the one place that sees a bundle's whole use within an invocation, so it holds the +/// input to that; it cannot count across invocations. +pub(crate) const TDEA_LIMIT: InputLimit = InputLimit { + max_bytes: 1 << 23, + message: "TDEA: one key bundle may protect at most 2^20 blocks (8 MiB; SP 800-67 Rev 2 Sec \ + 3.4), and the input exceeds that", +}; + +/// The same bound for a padded mode, whose padding adds up to one block: one byte less, so the +/// padded message still fits. +pub(crate) const TDEA_LIMIT_PADDED: InputLimit = + InputLimit { max_bytes: (1 << 23) - 1, message: TDEA_LIMIT.message }; + +/// The bound for CFB8, which enciphers one block per byte (SP 800-38A Sec 6.3, `s = 8`): 2^20 +/// blocks is 2^20 bytes, 1 MiB. +pub(crate) const TDEA_LIMIT_CFB8: InputLimit = InputLimit { + max_bytes: 1 << 20, + message: "TDEA: one key bundle may protect at most 2^20 blocks (SP 800-67 Rev 2 Sec 3.4), and \ + CFB8 enciphers one block per byte, so the input exceeds that at 1 MiB", +}; + +/// The bound for TKW, whose wrapping function makes six TDEA calls per 4-byte semiblock of +/// plaintext (SP 800-38F Algorithm 7, `s = 6(n - 1)`): 2^20 blocks is `2^20 / 6` semiblocks, +/// 699,048 bytes. +pub(crate) const TDEA_LIMIT_TKW: InputLimit = InputLimit { + max_bytes: (1 << 20) / 6 * 4, + message: "TDEA: one key bundle may protect at most 2^20 blocks (SP 800-67 Rev 2 Sec 3.4), and \ + TKW makes six TDEA calls per 4-byte block of input, so the input exceeds that at \ + 699,048 bytes", +}; + +/// [`encrypt_stream`] under an [`InputLimit`]: the running input total is checked against the +/// bound before each chunk is encrypted, so nothing past it is ever written. +pub(crate) fn encrypt_stream_bounded( mut enc: E, generated_iv: Option<&[u8; INIT_DATA_LEN]>, output_hex: bool, aligned_mode: Option<&str>, + limit: Option<&InputLimit>, ) where E: SymmetricCipherEncryptor, { @@ -292,8 +355,17 @@ pub(crate) fn encrypt_stream( write_bytes_or_hex(iv, output_hex); } + const { assert!(CHUNK_LEN.is_multiple_of(BLOCK_LEN), "the chunk must be whole blocks") }; let mut out = [0u8; CHUNK_LEN]; + let mut seen: u64 = 0; let total = stream_chunks(|data| { + seen += data.len() as u64; + if let Some(limit) = limit + && seen > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); + } // Cannot fail: `out` has room for every whole block of `data` plus the fewer than // `BLOCK_LEN` bytes held over from the last call, and none of these modes has a per-IV // data limit. @@ -302,7 +374,7 @@ pub(crate) fn encrypt_stream( }); if let Some(mode) = aligned_mode { - check_aligned(mode, total); + check_aligned::(mode, total); } // The padded final block; under `NoPadding` an aligned message has none, and this reports 0. let (last, last_len) = enc.do_encrypt_final().unwrap_or_else(|e| { @@ -317,7 +389,7 @@ pub(crate) fn encrypt_stream( /// Decrypts stdin to stdout through the worker `make_decryptor` gives for the init data (the IV): /// `iv` if it was supplied, else the first `INIT_DATA_LEN` bytes of input -- one block for CBC, /// nothing for ECB. `aligned_mode` is as for [`encrypt_stream`]. -pub(crate) fn decrypt_stream( +pub(crate) fn decrypt_stream( iv: Option<[u8; INIT_DATA_LEN]>, make_decryptor: impl FnOnce(&[u8; INIT_DATA_LEN]) -> D, output_hex: bool, @@ -352,7 +424,7 @@ pub(crate) fn decrypt_stream( }); if let Some(mode) = aligned_mode { - check_aligned(mode, total); + check_aligned::(mode, total); } // Releases the block held back above, unpadded; under `NoPadding` all of it is data. A // padded ciphertext that is unaligned or has malformed padding fails here. @@ -389,7 +461,7 @@ fn stream_chunks(mut process: impl FnMut(&[u8])) -> usize { /// Exits with an error if `total` is not a whole number of blocks, for a mode that is defined only /// on whole blocks and pads nothing. `NoPadding` would also refuse it at `do_*_final`, but without /// saying how many bytes were left over. -fn check_aligned(mode: &str, total: usize) { +fn check_aligned(mode: &str, total: usize) { if !total.is_multiple_of(BLOCK_LEN) { // Everything before the misaligned tail has already been written (bar the one block a // decryptor holds back until `do_decrypt_final`), and in hex mode is diff --git a/cli/src/helpers/stream_mode_helpers.rs b/cli/src/helpers/stream_mode_helpers.rs index d26a272a7..f0f699fbe 100644 --- a/cli/src/helpers/stream_mode_helpers.rs +++ b/cli/src/helpers/stream_mode_helpers.rs @@ -28,7 +28,7 @@ //! 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. -use crate::helpers::block_mode_helpers::{CHUNK_LEN, CipherDirection, start_encryptor}; +use crate::helpers::block_mode_helpers::{CHUNK_LEN, CipherDirection, InputLimit, start_encryptor}; use crate::helpers::{flush_stdout, write_bytes_or_hex, write_stdout}; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; @@ -37,13 +37,16 @@ use std::io::Read; use std::process::exit; /// Encrypts stdin to stdout under the key's stream mode. With no `iv`, the IV is generated and -/// written first; with one, it is used and the output is ciphertext only. +/// written first; with one, it is used and the output is ciphertext only. Under an +/// [`InputLimit`], the running input total is checked against the bound before each chunk is +/// encrypted, so nothing past it is ever written. /// /// `INIT_DATA_LEN` is the mode's: one block for CFB and CFB8, 12 bytes for CTR. pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( key: &'k K, iv: Option<[u8; INIT_DATA_LEN]>, output_hex: bool, + limit: Option<&InputLimit>, ) where K: SymmetricEncryptionKey, K::Encryptor<'k>: StreamCipherEncryptor, @@ -57,7 +60,15 @@ pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: u } // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + let mut seen: u64 = 0; stream(|data| { + seen += data.len() as u64; + if let Some(limit) = limit + && seen > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); + } // Cannot fail: neither CFB nor CFB8 has a per-IV data limit. // CFB and CFB8 cannot fail here; CTR can, once its counter is exhausted, which is a real // limit a long enough stream reaches rather than a bug. @@ -151,10 +162,26 @@ pub(crate) fn run_stream_mode<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: + SymmetricDecryptionKey, K::Encryptor<'k>: StreamCipherEncryptor, K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode_bounded::(action, key, iv, output_hex, None) +} + +/// [`run_stream_mode`] with an [`InputLimit`] on the encrypting direction. +pub(crate) fn run_stream_mode_bounded<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( + action: &CipherDirection, + key: &'k K, + iv: Option<[u8; INIT_DATA_LEN]>, + output_hex: bool, + limit: Option<&InputLimit>, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { match action { CipherDirection::Encrypt => { - encrypt_stream::(key, iv, output_hex) + encrypt_stream::(key, iv, output_hex, limit) } CipherDirection::Decrypt => { decrypt_stream::(key, iv, output_hex) diff --git a/cli/src/main.rs b/cli/src/main.rs index 635260d22..a7518ae54 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -23,6 +23,17 @@ mod sha2_cmd; mod sha3_cmd; mod sm2_cmd; mod sm3_cmd; +mod tdes2_cbc_cmd; +mod tdes2_cfb8_cmd; +mod tdes2_cfb_cmd; +mod tdes2_ctr_cmd; +mod tdes2_ecb_cmd; +mod tdes_cbc_cmd; +mod tdes_cfb8_cmd; +mod tdes_cfb_cmd; +mod tdes_ctr_cmd; +mod tdes_ecb_cmd; +mod tdes_tkw_cmd; use crate::ecdsa_cmd::ECDSAAction; use crate::hkdf_cmd::HKDFVariant; @@ -33,7 +44,7 @@ use crate::sha2_cmd::SHA2Variant; use crate::sha3_cmd::{SHA3Variant, Strength}; use crate::sm2_cmd::SM2Action; use clap::{Parser, Subcommand}; -use helpers::block_mode_helpers::CipherDirection; +use helpers::block_mode_helpers::{CipherDirection, DecryptOnlyAction}; #[derive(Parser)] #[command(version, about, long_about=None, arg_required_else_help=true)] @@ -2537,6 +2548,64 @@ enum Subcommands { x: bool, }, + /// Triple DES Key Wrap, TKW (NIST SP 800-38F Sec 7): `-d encrypt` wraps stdin to stdout, + /// `-d decrypt` unwraps it. LEGACY ONLY: SP 800-38F approves TKW to support legacy systems + /// and says it should not be used for new applications. + /// + /// The same command as `aes128-kw` over TDEA at half the width: input to `encrypt` is a whole + /// number of 4-byte blocks, at least 8 bytes, and the output is 4 bytes longer; input to + /// `decrypt` is a whole number of 4-byte blocks, at least 12 bytes. There is NO IV, and the + /// whole input is read before anything is written. See `aes128-kw` for the rest. + /// + /// `encrypt` refuses input past the SP 800-67 Rev 2 Sec 3.4 bound, counted as the blocks + /// TDEA processes: TKW makes six calls per 4-byte block of input, so at most 699,048 bytes. + /// + /// The key is 24 bytes, KEY1 || KEY2 || KEY3, with three distinct component keys none of which + /// is a weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). Two-key TDEA unwraps through + /// `tdes2-tkw`. + /// + /// 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. + TDES_TKW { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key-encryption key bundle 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 TDES key-encryption key bundle (raw bytes; hex with + /// --hex-input). If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES Key Wrap (2TDEA TKW), UNWRAPPING ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-tkw` for the input rules. + TDES2_TKW { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-KEM-512 key encapsulation algorithm. MLKEM512 { action: mlkem_cmd::MLKEMAction, @@ -2881,6 +2950,386 @@ enum Subcommands { x: bool, }, + /// Triple DES (three-key TDEA, NIST SP 800-67 Rev 2) in CBC mode (SP 800-38A Sec 6.2) with + /// PKCS7 padding, streaming stdin to stdout. LEGACY: NIST disallows TDEA for encryption after + /// 2023 (SP 800-131A Rev 2); this exists for data and protocols that still depend on it. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 8 BYTES of the + /// output; on `decrypt` it is read back from the first 8 bytes of the input, so the two compose + /// directly in a pipeline. With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied + /// instead: `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads ONLY + /// ciphertext from stdin, as for `aes128-cbc`. + /// + /// Input may be any length: it is padded with PKCS7 to a whole number of 8-byte blocks, so the + /// ciphertext after the IV is 1 to 8 bytes longer than the plaintext, and `decrypt` rejects a + /// ciphertext whose padding is malformed. + /// + /// The key is 24 bytes, KEY1 || KEY2 || KEY3, with three distinct component keys none of which + /// is a weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). Two-key TDEA decrypts through the + /// `tdes2-*` commands. + /// + /// 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 + /// separately. One key bundle must not encrypt more than 2^20 blocks (8 MiB) in total: + /// `encrypt` refuses longer input, and counting across invocations is the caller's. + /// + /// 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. + TDES_CBC { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key bundle 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 TDES key bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Triple DES (three-key TDEA) in CFB64 mode (SP 800-38A Sec 6.3, segment = block), streaming + /// stdin to stdout. LEGACY: see `tdes-cbc`. + /// + /// The segment size is the full 8-byte block, i.e. CFB64. SP 800-38A's 8-bit CFB is a + /// different, non-interoperable mode; use `tdes-cfb8` for that. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 8 BYTES of the + /// output; on `decrypt` it is read back from the first 8 bytes of the input. With `--iv` (hex) + /// or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead and only ciphertext is written or + /// read. + /// + /// Input may be ANY length: CFB is a stream cipher, so nothing is padded and the ciphertext is + /// exactly as long as the plaintext. + /// + /// See `tdes-cbc` for the key format and the warnings, which apply unchanged. + TDES_CFB { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key bundle 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 TDES key bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Triple DES (three-key TDEA) in CFB8 mode (SP 800-38A Sec 6.3, s = 8), streaming stdin to + /// stdout. LEGACY: see `tdes-cbc`. + /// + /// CFB8 is a DIFFERENT mode from `tdes-cfb`, not a variant: the two ciphertexts agree only on + /// their first byte. It also costs one TDES call per byte, eight times the work of `tdes-cfb`, + /// so prefer that unless a byte-granular mode is specifically required. That is also why + /// `encrypt` here refuses input past 2^20 bytes (1 MiB): the SP 800-67 Rev 2 Sec 3.4 bound + /// counts the blocks TDEA processes, one per byte in this mode. + /// + /// See `tdes-cfb` for the IV convention and input-length rule, and `tdes-cbc` for the key + /// format and the warnings, which apply unchanged. + TDES_CFB8 { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key bundle 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 TDES key bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of generated: `encrypt` then writes no IV and + /// `decrypt` reads none. See the IV paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Triple DES (three-key TDEA) in CTR mode (SP 800-38A Sec 6.5, TDEA per its Appendix E), + /// streaming stdin to stdout. LEGACY: see `tdes-cbc`. + /// On `encrypt`, a fresh 6-BYTE NONCE is generated and written as the first 6 bytes of the + /// output; on `decrypt` it is read back from the first 6 bytes of the input. With `--iv` (hex) + /// or `--iv-file` (raw bytes; hex with --hex-input), the nonce is supplied instead and only + /// ciphertext is written or read; it MUST then be unique per message under one key bundle. The + /// remaining 2 bytes of the 8-byte counter block are the counter, so a single message is + /// limited to 65 536 blocks (512 KiB); longer input fails rather than repeating a counter + /// block. + /// + /// Input may be any length up to that limit: CTR is a stream cipher, so nothing is padded and + /// the ciphertext is exactly as long as the plaintext. + /// + /// WARNING: CTR provides confidentiality only, and is the most malleable of the modes here: a + /// flipped ciphertext bit flips exactly the corresponding plaintext bit. A repeated nonce under + /// one key is fatal, and the nonce is only 48 bits, so the number of messages under one key + /// bundle must stay small. See `tdes-cbc` for the key format and the other warnings. + TDES_CTR { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key bundle 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 TDES key bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 6-byte nonce in hex. Supplied instead of generated: `encrypt` then writes none + /// and `decrypt` reads none. See the nonce paragraph above. + #[arg(long)] + iv: Option, + + /// A file containing the 6-byte nonce (raw bytes; hex with --hex-input). If both iv and + /// iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Triple DES (three-key TDEA) in ECB mode (SP 800-38A Sec 6.1), streaming stdin to stdout. + /// LEGACY: see `tdes-cbc`. + /// + /// WARNING: ECB is NOT a confidentiality mode for data. Under a given key every plaintext + /// block maps to the same ciphertext block, so equal blocks stay visibly equal, the same input + /// always gives the same output, and blocks can be reordered, repeated or removed undetectably. + /// This command exists for interoperability with systems that require ECB and for test + /// vectors. + /// + /// There is NO IV: nothing is prepended on `encrypt` and nothing is consumed on `decrypt`, so + /// the output is exactly as long as the input. + /// + /// Input must be a whole number of 8-byte blocks: this command is block-aligned and applies no + /// padding, so unaligned input is rejected rather than padded. + /// + /// See `tdes-cbc` for the key format. + TDES_ECB { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key bundle 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 TDES key bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES (2TDEA, Key3 = Key1) in CBC mode, DECRYPTION ONLY, streaming stdin to + /// stdout. NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption and allows + /// decryption for legacy use, so `encrypt` does not exist for this command. That is a guard + /// against accidental encryption: `tdes2-cfb` and `tdes2-ctr` decryption are the forward + /// cipher, which a determined caller can turn into encryption, so a deployment that must rule + /// that out should expose only `tdes2-cbc` and `tdes2-ecb`. + /// + /// The first 8 bytes of input are the IV, as the `tdes-cbc decrypt` convention, unless `--iv` + /// or `--iv-file` supplies it; the rest is PKCS7-padded ciphertext, as `tdes-cbc` writes it. + /// + /// The key is 16 bytes, KEY1 || KEY2, with two distinct component keys neither of which is a + /// weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). + /// + /// 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. + TDES2_CBC { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of read from the input, which is then + /// ciphertext only. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES (2TDEA) in CFB64 mode, DECRYPTION ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-cfb` for the IV convention (first 8 bytes of input, or `--iv`) and + /// the any-length input rule. + TDES2_CFB { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of read from the input, which is then + /// ciphertext only. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES (2TDEA) in CFB8 mode, DECRYPTION ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-cfb8` for the IV convention and the difference from CFB64. + TDES2_CFB8 { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 8-byte IV in hex. Supplied instead of read from the input, which is then + /// ciphertext only. + #[arg(long)] + iv: Option, + + /// A file containing the 8-byte IV, (raw bytes; hex with --hex-input). + /// If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES (2TDEA) in CTR mode, DECRYPTION ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-ctr` for the nonce convention (first 6 bytes of input, or `--iv`) + /// and the 512 KiB per-message limit. + TDES2_CTR { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 6-byte nonce in hex. Supplied instead of read from the input, which is then + /// ciphertext only. + #[arg(long)] + iv: Option, + + /// A file containing the 6-byte nonce (raw bytes; hex with --hex-input). If both iv and + /// iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES (2TDEA) in ECB mode, DECRYPTION ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-ecb` for the ECB warning: no IV, input a whole number of 8-byte + /// blocks, no unpadding. + TDES2_ECB { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle 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 two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given /// by repeated --element flags, each in hex; with none, stdin is hashed as a single element. /// The boundaries between elements are part of the hash. @@ -3440,6 +3889,12 @@ fn run() { Some(Subcommands::AES256_KWP { direction, key, key_file, x }) => { aes_kw_cmd::aes256_kwp_cmd(direction, key, key_file, *x); } + Some(Subcommands::TDES_TKW { direction, key, key_file, x }) => { + tdes_tkw_cmd::tdes_tkw_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES2_TKW { action, key, key_file, x }) => { + tdes_tkw_cmd::tdes2_tkw_cmd(action, key, key_file, *x); + } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); } @@ -3517,6 +3972,36 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } + Some(Subcommands::TDES_CBC { direction, key, key_file, iv, iv_file, x }) => { + tdes_cbc_cmd::tdes_cbc_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES_CFB { direction, key, key_file, iv, iv_file, x }) => { + tdes_cfb_cmd::tdes_cfb_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES_CFB8 { direction, key, key_file, iv, iv_file, x }) => { + tdes_cfb8_cmd::tdes_cfb8_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES_CTR { direction, key, key_file, iv, iv_file, x }) => { + tdes_ctr_cmd::tdes_ctr_cmd(direction, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES_ECB { direction, key, key_file, x }) => { + tdes_ecb_cmd::tdes_ecb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES2_CBC { action, key, key_file, iv, iv_file, x }) => { + tdes2_cbc_cmd::tdes2_cbc_cmd(action, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES2_CFB { action, key, key_file, iv, iv_file, x }) => { + tdes2_cfb_cmd::tdes2_cfb_cmd(action, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES2_CFB8 { action, key, key_file, iv, iv_file, x }) => { + tdes2_cfb8_cmd::tdes2_cfb8_cmd(action, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES2_CTR { action, key, key_file, iv, iv_file, x }) => { + tdes2_ctr_cmd::tdes2_ctr_cmd(action, key, key_file, iv, iv_file, *x); + } + Some(Subcommands::TDES2_ECB { action, key, key_file, x }) => { + tdes2_ecb_cmd::tdes2_ecb_cmd(action, key, key_file, *x); + } Some(Subcommands::TUPLEHASH128 { length, elements, diff --git a/cli/src/tdes2_cbc_cmd.rs b/cli/src/tdes2_cbc_cmd.rs new file mode 100644 index 000000000..6f9b03292 --- /dev/null +++ b/cli/src/tdes2_cbc_cmd.rs @@ -0,0 +1,34 @@ +//! Two-key TDES-CBC decryption, streaming stdin to stdout. +//! +//! Decryption only: NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption, +//! `TDES2Key::create_encryptor` does not compile, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cbc decrypt` -- the first 8 +//! bytes of input are the IV unless `--iv` supplies it, the rest PKCS7-padded ciphertext -- see +//! [`crate::tdes_cbc_cmd`] and [`crate::helpers::block_mode_helpers`]. The key is the 16-byte +//! `KEY1 || KEY2`. + +use crate::helpers::block_mode_helpers::{ + DecryptOnlyAction, decrypt_stream, load_iv, load_key_object, +}; +use bouncycastle::cipher::modes::CBC; +use bouncycastle::cipher::padding::PKCS7; +use bouncycastle::core::symmetric_key::SymmetricDecryptionKey; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_cbc_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key_object::>, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( + load_iv::(iv, iv_file), + |iv| key.create_decryptor(iv), + output_hex, + None, + ) +} diff --git a/cli/src/tdes2_cfb8_cmd.rs b/cli/src/tdes2_cfb8_cmd.rs new file mode 100644 index 000000000..ba3828a9e --- /dev/null +++ b/cli/src/tdes2_cfb8_cmd.rs @@ -0,0 +1,26 @@ +//! Two-key TDES-CFB8 decryption, streaming stdin to stdout. +//! +//! Decryption only: NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption, +//! `TDES2Key::create_encryptor` does not compile, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb8 decrypt` -- the first 8 +//! bytes of input are the IV unless `--iv` supplies it, the rest any length -- see +//! [`crate::tdes_cfb8_cmd`] for how CFB8 differs from CFB64, and +//! [`crate::helpers::stream_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::decrypt_stream; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_cfb8_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, KEY_LEN_2KEY, BLOCK_LEN>(&key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/tdes2_cfb_cmd.rs b/cli/src/tdes2_cfb_cmd.rs new file mode 100644 index 000000000..677db994a --- /dev/null +++ b/cli/src/tdes2_cfb_cmd.rs @@ -0,0 +1,26 @@ +//! Two-key TDES-CFB64 decryption, streaming stdin to stdout. +//! +//! Decryption only: NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption, +//! `TDES2Key::create_encryptor` does not compile, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb decrypt` -- the first 8 +//! bytes of input are the IV unless `--iv` supplies it, the rest any length -- see +//! [`crate::tdes_cfb_cmd`] and +//! [`crate::helpers::stream_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::decrypt_stream; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_cfb_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, KEY_LEN_2KEY, BLOCK_LEN>(&key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/tdes2_ctr_cmd.rs b/cli/src/tdes2_ctr_cmd.rs new file mode 100644 index 000000000..0240790ef --- /dev/null +++ b/cli/src/tdes2_ctr_cmd.rs @@ -0,0 +1,28 @@ +//! Two-key TDES-CTR decryption, streaming stdin to stdout. +//! +//! Decryption only: NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption, +//! `TDES2Key::create_encryptor` does not compile, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-ctr decrypt` -- the first 6 +//! bytes of input are the nonce unless `--iv` supplies it, the counter starts at zero, at most +//! 512 KiB of data -- see [`crate::tdes_ctr_cmd`] and [`crate::helpers::stream_mode_helpers`]. +//! The key is the 16-byte `KEY1 || KEY2`. + +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::decrypt_stream; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_ctr_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key_object::>, KEY_LEN_2KEY>( + key, key_file, "two-key TDES", + ); + decrypt_stream::<_, KEY_LEN_2KEY, CTR_NONCE_LEN>(&key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/tdes2_ecb_cmd.rs b/cli/src/tdes2_ecb_cmd.rs new file mode 100644 index 000000000..e722fd803 --- /dev/null +++ b/cli/src/tdes2_ecb_cmd.rs @@ -0,0 +1,31 @@ +//! Two-key TDES-ECB decryption, streaming stdin to stdout. +//! +//! Decryption only: NIST SP 800-131A Rev 2 Table 1 disallows two-key TDEA for encryption, +//! `TDES2Key::create_encryptor` does not compile, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-ecb decrypt` -- no IV, input a +//! whole number of 8-byte blocks, no unpadding -- see [`crate::tdes_ecb_cmd`] for the ECB warning +//! and [`crate::helpers::block_mode_helpers`] for the framing. The key is the 16-byte +//! `KEY1 || KEY2`. + +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, decrypt_stream, load_key_object}; +use bouncycastle::cipher::modes::hazmat::ecb::ECB; +use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::core::symmetric_key::SymmetricDecryptionKey; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_ecb_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = + load_key_object::>, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, 0, BLOCK_LEN>( + None, + |no_iv| key.create_decryptor(no_iv), + output_hex, + Some("ECB"), + ) +} diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs new file mode 100644 index 000000000..1ce4d6f10 --- /dev/null +++ b/cli/src/tdes_cbc_cmd.rs @@ -0,0 +1,55 @@ +//! TDES-CBC encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading and stdin framing are all in +//! [`crate::helpers::block_mode_helpers`], shared with the AES commands and `tdes-ecb`. See that +//! module for the command-line contract. The block, and so the IV, is 8 bytes, and input is +//! padded with PKCS7, so it may be any length. +//! +//! CBC (NIST SP 800-38A Sec 6.2; TCBC in its Appendix E) 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. +//! +//! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see +//! the `bouncycastle-tdes` crate docs. One key bundle must not protect more than 2^20 blocks +//! (8 MiB) in total (SP 800-67 Rev 2 Sec 3.4): `encrypt` refuses input past that in one +//! invocation, and nothing is counted across invocations, which is the caller's to track. + +use crate::helpers::block_mode_helpers::{ + CipherDirection, TDEA_LIMIT_PADDED, decrypt_stream, encrypt_stream_bounded, load_iv, + load_key_object, start_encryptor, +}; +use bouncycastle::cipher::modes::CBC; +use bouncycastle::cipher::padding::PKCS7; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; + +pub(crate) fn tdes_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::>, KEY_LEN>(key, key_file, "TDES"); + let iv = load_iv::(iv, iv_file); + match action { + CipherDirection::Encrypt => { + let (enc, generated_iv) = start_encryptor(iv, |rng| key.create_encryptor(rng)); + encrypt_stream_bounded::<_, BLOCK_LEN, BLOCK_LEN>( + enc, + generated_iv.as_ref(), + output_hex, + None, + Some(&TDEA_LIMIT_PADDED), + ) + } + CipherDirection::Decrypt => decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( + iv, + |iv| key.create_decryptor(iv), + output_hex, + None, + ), + } +} diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs new file mode 100644 index 000000000..b7b8615c7 --- /dev/null +++ b/cli/src/tdes_cfb8_cmd.rs @@ -0,0 +1,46 @@ +//! TDES-CFB8 encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and IV loaders), shared with the +//! AES stream commands and `tdes-cfb` / `tdes-ctr`. See those modules for the command-line +//! contract. The block, and so the IV, is 8 bytes. +//! +//! CFB8 (SP 800-38A Sec 6.3 with `s = 8`; TCFB with `s = 8` in Appendix E) is a **different, +//! non-interoperable mode** from `tdes-cfb`: the two ciphertexts agree only on their first byte. It +//! also costs one TDES call per byte, eight times the work of `tdes-cfb`, so prefer that unless a +//! byte-granular mode is specifically required. The SP 800-67 Rev 2 Sec 3.4 bound counts the +//! blocks TDEA processes, so `encrypt` here refuses input past 2^20 bytes, 1 MiB, where the modes +//! that process one block per 8 bytes allow 8 MiB. +//! +//! # Warning +//! +//! CFB8 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 separately. +//! +//! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see +//! the `bouncycastle-tdes` crate docs. + +use crate::helpers::block_mode_helpers::TDEA_LIMIT_CFB8; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; + +pub(crate) fn tdes_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "TDES"); + run_stream_mode_bounded::<_, KEY_LEN, BLOCK_LEN>( + action, + &key, + load_iv(iv, iv_file), + output_hex, + Some(&TDEA_LIMIT_CFB8), + ) +} diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs new file mode 100644 index 000000000..1f3edfe46 --- /dev/null +++ b/cli/src/tdes_cfb_cmd.rs @@ -0,0 +1,45 @@ +//! TDES-CFB64 encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and IV loaders), shared with the +//! AES stream commands and `tdes-cfb8` / `tdes-ctr`. See those modules for the command-line +//! contract. The block, and so the IV, is 8 bytes; the segment is the full block, so this is CFB64 +//! -- the TCFB mode of SP 800-38A Appendix E with `s = 64`. +//! +//! # Any length +//! +//! CFB is a stream cipher: input of any length is accepted, nothing is padded, and the output is +//! exactly as long as the input. +//! +//! # Warning +//! +//! CFB 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 separately. +//! +//! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see +//! the `bouncycastle-tdes` crate docs. + +use crate::helpers::block_mode_helpers::TDEA_LIMIT; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; + +pub(crate) fn tdes_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = load_key_object::, KEY_LEN>(key, key_file, "TDES"); + run_stream_mode_bounded::<_, KEY_LEN, BLOCK_LEN>( + action, + &key, + load_iv(iv, iv_file), + output_hex, + Some(&TDEA_LIMIT), + ) +} diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs new file mode 100644 index 000000000..2802f903c --- /dev/null +++ b/cli/src/tdes_ctr_cmd.rs @@ -0,0 +1,65 @@ +//! TDES-CTR encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the nonce convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the +//! key and nonce loaders), shared with the +//! AES stream commands and `tdes-cfb` / `tdes-cfb8`. See those modules for the command-line +//! contract. +//! +//! # The nonce is 6 bytes and the counter is 2 +//! +//! NIST SP 800-38A Sec 6.5 builds CTR on a sequence of counter blocks, and Appendix B.2's second +//! approach makes each one a message nonce followed by a counter; Appendix E allows TDEA as the +//! underlying cipher. With an 8-byte block the split is tight. This command uses the `TDES_CTR` +//! alias: a **6-byte** nonce and a 2-byte counter, so a single message is limited to 65 536 blocks +//! (512 KiB), past which the command fails rather than repeat a counter block. The +//! `bouncycastle-tdes` crate docs explain the trade-off against nonce collisions. +//! +//! `encrypt` writes that 6-byte nonce as the first bytes of its output and `decrypt` reads it back, +//! exactly as the other modes do with their IVs; note that it is 6 bytes here, not 8. +//! +//! # Any length (up to the counter limit) +//! +//! CTR is a stream cipher: nothing is padded, and the output is exactly as long as the input. +//! +//! # Warning +//! +//! CTR provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the nonce is authenticated. It is the most malleable of the modes here: flipping any ciphertext +//! bit flips exactly the corresponding plaintext bit and affects nothing else (SP 800-38A +//! Appendix D). Do not decrypt data you have not authenticated separately. +//! +//! A repeated nonce is fatal here rather than merely unwise: the same nonce under the same key +//! gives the same keystream. The nonce is drawn from the OS-backed DRBG unless `--iv` / +//! `--iv-file` supplies one, and then its uniqueness under the key is the caller's to keep; with +//! only 48 bits of nonce the number of messages under one key bundle must stay small either way +//! -- which SP 800-67 Rev 2 Sec 3.4's limit of 2^20 blocks per bundle already demands. +//! +//! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see +//! the `bouncycastle-tdes` crate docs. + +use crate::helpers::block_mode_helpers::TDEA_LIMIT; +use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES3Key}; + +pub(crate) fn tdes_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + output_hex: bool, +) { + let key = + load_key_object::>, KEY_LEN>(key, key_file, "TDES"); + let nonce = load_iv::(iv, iv_file); + run_stream_mode_bounded::<_, KEY_LEN, CTR_NONCE_LEN>( + action, + &key, + nonce, + output_hex, + Some(&TDEA_LIMIT), + ) +} diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs new file mode 100644 index 000000000..b9e66557a --- /dev/null +++ b/cli/src/tdes_ecb_cmd.rs @@ -0,0 +1,60 @@ +//! TDES-ECB encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: key loading, stdin framing and block-alignment enforcement are +//! all in [`crate::helpers::block_mode_helpers`], shared with the AES commands and `tdes-cbc`. See +//! that module for the command-line contract. ECB has no IV (`INIT_DATA_LEN = 0`), so nothing is +//! prepended to +//! the output or consumed from the input: the ciphertext is exactly as long as the plaintext. The +//! block is 8 bytes. +//! +//! # Warning +//! +//! ECB (NIST SP 800-38A Sec 6.1; TECB in its Appendix E) is **not a confidentiality mode for +//! data**. Under a given key every plaintext block maps to the same ciphertext block, so equal +//! blocks stay visibly equal, the structure of the plaintext shows through, and blocks can be +//! reordered, repeated or removed with nothing to detect it. The same plaintext encrypts to the +//! same ciphertext every time. This command exists for interoperability with systems that use ECB +//! and for driving test vectors. +//! +//! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see +//! the `bouncycastle-tdes` crate docs. + +use crate::helpers::block_mode_helpers::{ + CipherDirection, TDEA_LIMIT, decrypt_stream, encrypt_stream_bounded, load_key_object, + start_encryptor, +}; +use bouncycastle::cipher::modes::hazmat::ecb::ECB; +use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; + +/// Names the mode in error messages. +const MODE: &str = "ECB"; + +pub(crate) fn tdes_ecb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key_object::>, KEY_LEN>(key, key_file, "TDES"); + match action { + CipherDirection::Encrypt => { + // ECB has no IV: the constructor's RNG draws nothing, and nothing is written. + let (enc, _) = start_encryptor(None, |rng| key.create_encryptor(rng)); + encrypt_stream_bounded::<_, 0, BLOCK_LEN>( + enc, + None, + output_hex, + Some(MODE), + Some(&TDEA_LIMIT), + ) + } + CipherDirection::Decrypt => decrypt_stream::<_, 0, BLOCK_LEN>( + None, + |no_iv| key.create_decryptor(no_iv), + output_hex, + Some(MODE), + ), + } +} diff --git a/cli/src/tdes_tkw_cmd.rs b/cli/src/tdes_tkw_cmd.rs new file mode 100644 index 000000000..f99d3f93b --- /dev/null +++ b/cli/src/tdes_tkw_cmd.rs @@ -0,0 +1,46 @@ +//! TDEA key wrap: `tdes-tkw -d encrypt|decrypt` (three-key, both directions) and +//! `tdes2-tkw -d decrypt` (two-key, unwrapping only), stdin to stdout. +//! +//! They are the `aes*-kw` command over TDEA (NIST SP 800-38F Sec 7, TKW), so everything in +//! [`crate::aes_kw_cmd`] -- the whole-input reading, the absence of an IV, the fail-closed unwrap +//! -- applies unchanged, at half the width: the input is a whole number of 4-byte semiblocks, at +//! least 8 bytes, and the output is 4 bytes longer. `encrypt` has the SP 800-67 Rev 2 Sec 3.4 +//! bound the other `tdes-*` encrypting commands have, counted as blocks TDEA processes: TW makes +//! six calls per 4-byte semiblock, so the input may be at most 699,048 bytes +//! ([`TDEA_LIMIT_TKW`]). TKW is approved for legacy systems only (SP 800-38F Sec 8), and two-key +//! TDEA may not wrap at all (SP 800-131A Rev 2 Table 6), so `tdes2-tkw` has no `encrypt`, as the +//! other `tdes2-*` commands have none. + +use crate::aes_kw_cmd::{run, run_with}; +use crate::helpers::block_mode_helpers::{ + CipherDirection, DecryptOnlyAction, TDEA_LIMIT_TKW, load_key_object, +}; +use bouncycastle::cipher::modes::TKW; +use bouncycastle::core::symmetric_key::KeyUnwrappingKey; +use bouncycastle::tdes::{KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +pub(crate) fn tdes_tkw_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, KEY_LEN>( + direction, + &load_key_object::, KEY_LEN>(key, key_file, "TDES"), + Some(&TDEA_LIMIT_TKW), + output_hex, + "TDES-TKW", + ); +} + +pub(crate) fn tdes2_tkw_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let kek = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + run_with(|input| kek.unwrap(input), None, output_hex, "two-key TDES-TKW"); +} diff --git a/cli/tests/tdes_cli_tests.rs b/cli/tests/tdes_cli_tests.rs new file mode 100644 index 000000000..76605bad4 --- /dev/null +++ b/cli/tests/tdes_cli_tests.rs @@ -0,0 +1,380 @@ +//! Tests for the `tdes-*` and `tdes2-*` subcommands. +//! +//! They share their key and IV loaders, streaming loops and error paths with the AES commands +//! (`cli/src/helpers/`), and `test_aes_*.sh` cover those exhaustively. This file pins what is +//! specific to TDEA: the 8-byte block and IV, the 6-byte CTR nonce, the 24- and 16-byte bundles, +//! the decrypt-only two-key commands, and the known-answer vectors in both directions. +//! +//! The vectors are `COUNT = 0` of each NIST CAVP Multi-block Message Test file the crate's own +//! suite uses (`TECBMMT3`, `TCBCMMT3`, `TCFB64MMT3`, `TCFB8MMT3` `[ENCRYPT]`, and the two-key +//! `[DECRYPT]` files), read from `../bc-test-data` at test time; without it those tests print a +//! warning and pass, as the crate's suite does. The CBC vectors are unpadded, so under the +//! commands' PKCS7 framing the ciphertext is followed by one block of padding, computed here from +//! the engine. + +use std::io::{ErrorKind, Write}; +use std::path::Path; +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"); + +/// A three-key bundle of distinct, non-weak components, for the tests that need no vector. +const KEY_3: &str = "0123456789abcdef23456789abcdef01456789abcdef0123"; + +/// 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"); + + // The error-path tests hand a rejected key to a command that `exit`s before it ever reads + // stdin, so this write races the child's exit and sometimes loses. That is an expected + // outcome: `wait_with_output` still returns the exit status and stderr, which is all those + // tests assert on. 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") +} + +/// 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. +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 CAVP vectors ------------------------------------------------------------------------ + +/// `COUNT = 0` of one section of a CAVP MMT response file: key bundle, IV (empty for ECB), +/// plaintext and ciphertext, all as hex. +struct Vector { + key: String, + iv: String, + plaintext: String, + ciphertext: String, +} + +/// The text of a TDES CAVP response file, or `None` (with a warning) when `bc-test-data` is not +/// cloned beside this repository. When it is and the file is missing, this panics rather than +/// letting the test pass vacuously, as the framework's `bc_test_data` loader does. +fn cavp_file(file: &str) -> Option { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bc-test-data"); + if !root.is_dir() { + println!( + "WARNING: bc-test-data not found at {}; the CAVP vector tests are skipped", + root.display() + ); + return None; + } + let path = root.join("crypto/aes_tdes_vectors/TDES").join(file); + Some(std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))) +} + +/// `COUNT = 0` of `[section]` in `file`, or `None` without the data. The key is `KEY1 || KEY2` +/// for a two-key file (whose `KEY3 = KEY1`) and `KEY1 || KEY2 || KEY3` otherwise. +fn vector(file: &str, section: &str) -> Option { + let text = cavp_file(file)?; + let body = text.split(&format!("[{section}]")).nth(1).expect("the section"); + let case = body.split("COUNT = 0").nth(1).expect("COUNT = 0"); + let case = case.split("COUNT = 1").next().expect("one case"); + let field = |name: &str| -> String { + case.lines() + .map(str::trim) + .find_map(|l| l.strip_prefix(&format!("{name} = "))) + .unwrap_or_default() + .to_lowercase() + }; + let (k1, k2, k3) = (field("KEY1"), field("KEY2"), field("KEY3")); + let key = if file.ends_with("MMT2.rsp") { + assert_eq!(k1, k3, "a two-key vector has KEY3 = KEY1"); + k1 + &k2 + } else { + k1 + &k2 + &k3 + }; + Some(Vector { + key, + iv: field("IV"), + plaintext: field("PLAINTEXT"), + ciphertext: field("CIPHERTEXT"), + }) +} + +/// The CBC ciphertext block that a full block of PKCS7 padding (eight `0x08` bytes) encrypts to +/// under `key_hex` after the ciphertext block `prev_hex`: `E_K(prev xor 08..08)`, from the engine +/// of the bundle's length. Appended to an unpadded known-answer ciphertext, it gives the PKCS7 +/// ciphertext of the same plaintext, which is how the commands frame it. +fn pkcs7_block(key_hex: &str, prev_hex: &str) -> String { + use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::tdes::hazmat::{TDES2Internal, TDESInternal}; + + fn material(key_hex: &str) -> KeyMaterial { + let key_bytes: [u8; N] = unhex(key_hex).try_into().expect("the right key length"); + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") + } + let mut block: [u8; 8] = unhex(prev_hex).try_into().expect("an 8-byte block"); + block.iter_mut().for_each(|b| *b ^= 0x08); + match key_hex.len() / 2 { + 24 => TDESInternal::new(&material::<24>(key_hex)).unwrap().encrypt_block(&mut block), + 16 => TDES2Internal::new(&material::<16>(key_hex)).unwrap().encrypt_block(&mut block), + _ => panic!("not a TDEA bundle length"), + } + tohex(&block) +} + +/// The last 16 hex characters: the final ciphertext block. +fn last_block(ct: &str) -> &str { + &ct[ct.len() - 16..] +} + +/// `tdes-cbc encrypt --iv` reproduces TCBCMMT3 followed by the PKCS7 block, `decrypt --iv` takes +/// it back, and `decrypt` with the IV in band gives the same answer. +#[test] +fn tdes_cbc_matches_the_cavp_vector() { + let Some(v) = vector("TCBCMMT3.rsp", "ENCRYPT") else { return }; + let want = format!("{}{}", v.ciphertext, pkcs7_block(&v.key, last_block(&v.ciphertext))); + + let out = run_ok( + &["tdes-cbc", "-d", "encrypt", "--key", &v.key, "--iv", &v.iv], + &unhex(&v.plaintext), + ); + assert_eq!(tohex(&out), want, "encrypt --iv"); + + let back = run_ok(&["tdes-cbc", "-d", "decrypt", "--key", &v.key, "--iv", &v.iv], &out); + assert_eq!(tohex(&back), v.plaintext, "decrypt --iv"); + + let back = + run_ok(&["tdes-cbc", "-d", "decrypt", "--key", &v.key], &unhex(&format!("{}{want}", v.iv))); + assert_eq!(tohex(&back), v.plaintext, "decrypt with the IV in band"); +} + +/// `tdes-cfb encrypt --iv` and `tdes-cfb8 encrypt --iv` reproduce TCFB64MMT3 and TCFB8MMT3 +/// exactly, and `decrypt --iv` takes them back. +#[test] +fn tdes_cfb_and_cfb8_match_the_cavp_vectors() { + for (cmd, file) in [("tdes-cfb", "TCFB64MMT3.rsp"), ("tdes-cfb8", "TCFB8MMT3.rsp")] { + let Some(v) = vector(file, "ENCRYPT") else { return }; + let out = + run_ok(&[cmd, "-d", "encrypt", "--key", &v.key, "--iv", &v.iv], &unhex(&v.plaintext)); + assert_eq!(tohex(&out), v.ciphertext, "{cmd} encrypt --iv"); + let back = run_ok(&[cmd, "-d", "decrypt", "--key", &v.key, "--iv", &v.iv], &out); + assert_eq!(tohex(&back), v.plaintext, "{cmd} decrypt --iv"); + } +} + +/// `tdes-ecb` reproduces TECBMMT3 in both directions; there is no IV and no padding. +#[test] +fn tdes_ecb_matches_the_cavp_vector() { + let Some(v) = vector("TECBMMT3.rsp", "ENCRYPT") else { return }; + let out = run_ok(&["tdes-ecb", "-d", "encrypt", "--key", &v.key], &unhex(&v.plaintext)); + assert_eq!(tohex(&out), v.ciphertext, "encrypt"); + let back = run_ok(&["tdes-ecb", "-d", "decrypt", "--key", &v.key], &out); + assert_eq!(tohex(&back), v.plaintext, "decrypt"); +} + +/// The two-key commands decrypt the `[DECRYPT]` vectors of the two-key files: CBC with the PKCS7 +/// block appended, CFB64 and CFB8 exactly, ECB without an IV. +#[test] +fn tdes2_commands_decrypt_the_cavp_vectors() { + let Some(v) = vector("TCBCMMT2.rsp", "DECRYPT") else { return }; + let padded = format!("{}{}", v.ciphertext, pkcs7_block(&v.key, last_block(&v.ciphertext))); + let out = + run_ok(&["tdes2-cbc", "-d", "decrypt", "--key", &v.key, "--iv", &v.iv], &unhex(&padded)); + assert_eq!(tohex(&out), v.plaintext, "tdes2-cbc decrypt --iv"); + let out = run_ok( + &["tdes2-cbc", "-d", "decrypt", "--key", &v.key], + &unhex(&format!("{}{padded}", v.iv)), + ); + assert_eq!(tohex(&out), v.plaintext, "tdes2-cbc decrypt with the IV in band"); + + for (cmd, file) in [("tdes2-cfb", "TCFB64MMT2.rsp"), ("tdes2-cfb8", "TCFB8MMT2.rsp")] { + let Some(v) = vector(file, "DECRYPT") else { return }; + let out = + run_ok(&[cmd, "-d", "decrypt", "--key", &v.key, "--iv", &v.iv], &unhex(&v.ciphertext)); + assert_eq!(tohex(&out), v.plaintext, "{cmd} decrypt --iv"); + } + + let Some(v) = vector("TECBMMT2.rsp", "DECRYPT") else { return }; + let out = run_ok(&["tdes2-ecb", "-d", "decrypt", "--key", &v.key], &unhex(&v.ciphertext)); + assert_eq!(tohex(&out), v.plaintext, "tdes2-ecb decrypt"); +} + +// ---- the contract ---------------------------------------------------------------------------- + +/// `encrypt | decrypt` composes for every three-key mode: the 8-byte IV (6-byte nonce for CTR) +/// rides at the front, CBC pads with PKCS7, the stream modes and ECB do not. +#[test] +fn every_three_key_mode_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + for (cmd, init_len, padded_len) in [ + ("tdes-cbc", 8, 4096 + 8), + ("tdes-cfb", 8, 4096), + ("tdes-cfb8", 8, 4096), + ("tdes-ctr", 6, 4096), + ("tdes-ecb", 0, 4096), + ] { + let ciphertext = run_ok(&[cmd, "-d", "encrypt", "--key", KEY_3], &plaintext); + assert_eq!(ciphertext.len(), init_len + padded_len, "{cmd}: init data plus the body"); + assert_ne!(&ciphertext[init_len..init_len + 64], &plaintext[..64], "{cmd}: encrypted"); + let recovered = run_ok(&[cmd, "-d", "decrypt", "--key", KEY_3], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// CBC accepts any length and pads to the next whole 8-byte block; ECB does not pad and rejects +/// unaligned input. +#[test] +fn cbc_pads_and_ecb_does_not() { + for len in [0usize, 1, 7, 8, 9, 17] { + let plaintext = vec![0x5Au8; len]; + let ciphertext = run_ok(&["tdes-cbc", "-d", "encrypt", "--key", KEY_3], &plaintext); + assert_eq!(ciphertext.len(), 8 + (len / 8 + 1) * 8, "len {len}: IV plus padded body"); + let recovered = run_ok(&["tdes-cbc", "-d", "decrypt", "--key", KEY_3], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } + let err = run_err(&["tdes-ecb", "-d", "encrypt", "--key", KEY_3], &[0u8; 9]); + assert!(err.contains("whole number of 8-byte blocks"), "stderr was: {err}"); +} + +/// With `--iv` the output is the ciphertext alone, and it is still CTR over TDEA under that +/// 6-byte nonce: the library's key object decrypts it. +#[test] +fn tdes_ctr_under_a_supplied_nonce_agrees_with_the_library() { + use bouncycastle::cipher::modes::CTR; + use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + use bouncycastle::core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; + use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES3Key}; + + let nonce = "000102030405"; + let plaintext = pseudo_random(37, 5); + let out = run_ok(&["tdes-ctr", "-d", "encrypt", "--key", KEY_3, "--iv", nonce], &plaintext); + assert_eq!(out.len(), plaintext.len(), "ciphertext only, no nonce in band"); + + let key_bytes: [u8; 24] = unhex(KEY_3).try_into().unwrap(); + let material = KeyMaterial::<24>::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key"); + let key = TDES3Key::>::from_key_material(&material).unwrap(); + let mut recovered = out.clone(); + let nonce_bytes: [u8; CTR_NONCE_LEN] = unhex(nonce).try_into().unwrap(); + key.decrypt_inplace(&nonce_bytes, &mut recovered).expect("decryption"); + assert_eq!(recovered, plaintext, "the CLI should agree with the library under that nonce"); + + let back = run_ok(&["tdes-ctr", "-d", "decrypt", "--key", KEY_3, "--iv", nonce], &out); + assert_eq!(back, plaintext); +} + +/// The same with `-x`: identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let plaintext = pseudo_random(32, 3); + let ciphertext = run_ok(&["tdes-cbc", "-d", "encrypt", "--key", KEY_3], &plaintext); + let binary = run_ok(&["tdes-cbc", "-d", "decrypt", "--key", KEY_3], &ciphertext); + let hex_out = run_ok(&["tdes-cbc", "-d", "decrypt", "--key", KEY_3, "-x"], &ciphertext); + assert_eq!(String::from_utf8_lossy(&hex_out), format!("{}\n", tohex(&binary))); + assert_eq!(binary, plaintext); +} + +/// The two-key commands have no `encrypt`: the parser refuses it, naming the one value. +#[test] +fn two_key_commands_refuse_to_encrypt() { + let key_2 = &KEY_3[..32]; + for cmd in ["tdes2-cbc", "tdes2-cfb", "tdes2-cfb8", "tdes2-ctr", "tdes2-ecb"] { + let err = run_err(&[cmd, "-d", "encrypt", "--key", key_2], &[0u8; 16]); + assert!( + err.contains("decrypt"), + "{cmd}: stderr should list the one valid value, was: {err}" + ); + } +} + +/// Each command wants exactly its bundle length, and a bundle that repeats a component or holds +/// a weak DEA key is rejected by the key object. +#[test] +fn bad_bundles_are_rejected() { + let err = run_err(&["tdes-cbc", "-d", "encrypt", "--key", &KEY_3[..32]], &[0u8; 16]); + assert!(err.contains("TDES needs a 24-byte key, got 16 bytes"), "stderr was: {err}"); + let err = run_err(&["tdes2-cbc", "-d", "decrypt", "--key", KEY_3], &[0u8; 16]); + assert!(err.contains("two-key TDES needs a 16-byte key, got 24 bytes"), "stderr was: {err}"); + + // Key1 = Key2: two of the three transformations cancel. + let repeated = "0123456789abcdef0123456789abcdef456789abcdef0123"; + let err = run_err(&["tdes-cbc", "-d", "encrypt", "--key", repeated], &[0u8; 16]); + assert!(err.contains("WeakKey"), "stderr was: {err}"); + // A weak DEA key as Key1. + let weak = "010101010101010123456789abcdef01456789abcdef0123"; + let err = run_err(&["tdes-cfb", "-d", "encrypt", "--key", weak], &[0u8; 16]); + assert!(err.contains("WeakKey"), "stderr was: {err}"); +} + +/// The harness above must survive a write that loses the race with the child's exit: the key is +/// rejected so the command exits before reading a byte, and the payload cannot fit in a pipe +/// buffer, so the write is certain to get EPIPE. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let err = + run_err(&["tdes-cbc", "-d", "encrypt", "--key", &KEY_3[..32]], &vec![0u8; 4 * 1024 * 1024]); + assert!(err.contains("TDES needs a 24-byte key"), "stderr was: {err}"); +} + +/// The subcommands 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 + ["tdes-cbc", "tdes-cfb", "tdes-cfb8", "tdes-ctr", "tdes-ecb", "tdes2-cbc", "tdes2-ecb"] + { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} diff --git a/cli/tests/tdes_tkw_cli_tests.rs b/cli/tests/tdes_tkw_cli_tests.rs new file mode 100644 index 000000000..7a3944c0b --- /dev/null +++ b/cli/tests/tdes_tkw_cli_tests.rs @@ -0,0 +1,163 @@ +//! Tests for the `tdes-tkw` and `tdes2-tkw` subcommands. +//! +//! They share everything but the key type and the semiblock width with the `aes*-kw` commands, +//! whose `test_aes_kw.sh` covers the command-line contract exhaustively. This file pins what is +//! specific to TKW: the CAVP known answers in both directions, a CAVP forgery, the 4-byte +//! alignment rule, and that the two-key command unwraps only. +//! +//! The known answers are `COUNT = 0` of the 64- and 128-bit sections of the NIST CAVP `TKW_AE.txt` +//! sample file, and the first `FAIL` of `TKW_AD.txt`, read from `../bc-test-data` at test time; +//! without it those tests print a warning and pass, as the crate's suite does. + +use std::io::{ErrorKind, Write}; +use std::path::Path; +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"); + +/// A three-key bundle of distinct, non-weak components, for the tests that need no vector. +const KEY_3: &str = "0123456789abcdef23456789abcdef01456789abcdef0123"; +/// A two-key bundle of distinct, non-weak components. +const KEY_2: &str = "0123456789abcdef23456789abcdef01"; + +/// 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"); + // A command that exits before reading stdin makes this write race its exit; that is the + // expected outcome for the error-path tests, which assert on the status and stderr only. + 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") +} + +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 +} + +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) + ); + assert!(out.stdout.is_empty(), "a rejected command must write nothing: {args:?}"); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// The text of a CAVP sample file, or `None` (with a warning) when `bc-test-data` is not cloned +/// beside this repository. +fn cavp_file(file: &str) -> Option { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bc-test-data"); + let path = root.join("crypto/aes_tdes_vectors/TDES").join(file); + if !root.exists() { + println!( + "WARNING: bc-test-data not found at {}; the CAVP tests are skipped", + root.display() + ); + return None; + } + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + // The CAVS files have CRLF line ends. + Some(text.replace('\r', "")) +} + +/// `(K, P, C)` of `COUNT = 0` in the `[PLAINTEXT LENGTH = bits]` section of `file`; `P` is empty +/// for a `FAIL` case. +fn case(file: &str, bits: u32, count: u32) -> Option<(String, String, String)> { + let text = cavp_file(file)?; + let section = text.split(&format!("[PLAINTEXT LENGTH = {bits}]")).nth(1).expect("the section"); + let body = section.split(&format!("COUNT = {count}\n")).nth(1).expect("the case"); + let body = body.split("COUNT = ").next().unwrap(); + let field = |name: &str| -> String { + body.lines() + .map(str::trim) + .find_map(|l| l.strip_prefix(&format!("{name} = "))) + .unwrap_or_default() + .to_lowercase() + }; + Some((field("K"), field("P"), field("C"))) +} + +#[test] +fn tdes_tkw_matches_the_cavp_vectors_in_both_directions() { + for bits in [64, 128] { + let Some((k, p, c)) = case("TKW_AE.txt", bits, 0) else { return }; + let wrapped = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", &k], &unhex(&p)); + assert_eq!(tohex(&wrapped), c, "{bits}-bit wrap"); + let hex = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", &k, "-x"], &unhex(&p)); + assert_eq!(String::from_utf8_lossy(&hex).trim(), c, "{bits}-bit wrap -x"); + let unwrapped = run_ok(&["tdes-tkw", "-d", "decrypt", "--key", &k], &unhex(&c)); + assert_eq!(tohex(&unwrapped), p, "{bits}-bit unwrap"); + } +} + +#[test] +fn tdes_tkw_rejects_a_cavp_forgery() { + let Some(text) = cavp_file("TKW_AD.txt") else { return }; + // The first FAIL: its K and C are the two `NAME = value` lines just above it. + let before = text.split("\nFAIL").next().expect("a FAIL line"); + let mut lines = before.lines().rev().map(str::trim); + let c = lines.next().unwrap().strip_prefix("C = ").expect("C before FAIL").to_lowercase(); + let k = lines.next().unwrap().strip_prefix("K = ").expect("K before C").to_lowercase(); + let err = run_err(&["tdes-tkw", "-d", "decrypt", "--key", &k], &unhex(&c)); + assert!(err.contains("not an authentic"), "{err}"); +} + +#[test] +fn tkw_takes_whole_4_byte_semiblocks_and_at_least_two() { + let err = run_err(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 4]); + assert!(err.contains("4 bytes rejected"), "{err}"); + let err = run_err(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 10]); + assert!(err.contains("10 bytes rejected"), "{err}"); + let wrapped = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 12]); + assert_eq!(wrapped.len(), 16, "12 bytes wrap to 16"); + let back = run_ok(&["tdes-tkw", "-d", "decrypt", "--key", KEY_3], &wrapped); + assert_eq!(back, [0x5Au8; 12]); +} + +#[test] +fn tdes2_tkw_unwraps_only() { + // `encrypt` is not a value of the two-key direction at all. + let err = run_err(&["tdes2-tkw", "-d", "encrypt", "--key", KEY_2], &[0x5Au8; 8]); + assert!(err.contains("encrypt"), "{err}"); + // A ciphertext this key never made is rejected, not unwrapped to garbage. + let err = run_err(&["tdes2-tkw", "-d", "decrypt", "--key", KEY_2], &[0x5Au8; 12]); + assert!(err.contains("not an authentic"), "{err}"); + // A three-key bundle is not a two-key one. + let err = run_err(&["tdes2-tkw", "-d", "decrypt", "--key", KEY_3], &[0x5Au8; 12]); + assert!(err.contains("16-byte key"), "{err}"); +} + +#[test] +fn the_subcommands_are_listed_in_help() { + let help = String::from_utf8(run_ok(&["--help"], b"")).unwrap(); + for cmd in ["tdes-tkw", "tdes2-tkw"] { + assert!(help.contains(cmd), "--help should list {cmd}"); + } +} diff --git a/cli/tests/test_tdes.sh b/cli/tests/test_tdes.sh new file mode 100755 index 000000000..22a9a1bff --- /dev/null +++ b/cli/tests/test_tdes.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# The tdes-* subcommands' data limit, end to end through the binary. SP 800-67 Rev 2 Sec 3.4 +# allows one key bundle 2^20 64-bit blocks (8 MiB) of protection; `encrypt` refuses input past +# that with the usual `Error:` line, before any byte past the bound is processed. CBC's PKCS7 +# padding adds up to a block, so its bound is one byte lower; CFB64 takes exactly 8 MiB. The +# bound counts the blocks TDEA processes, so CFB8 (one block per byte) stops at 1 MiB and TKW +# (six calls per 4-byte semiblock) at 699,048 bytes. CTR never reaches it: its 6-byte nonce +# leaves a 16-bit counter, so the library's own keystream limit (2^16 blocks, 512 KiB) binds +# first, as `DataLimitExceeded`. Decryption is not limited. This lives here rather than in +# `tdes_cli_tests.rs` because 8 MiB through a debug build of the bit-sliced engine takes a minute +# or two per run. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +KEY=0123456789abcdef23456789abcdef01456789abcdef0123 +KEY2=0123456789abcdef23456789abcdef01 +LIMIT=$((1 << 23)) + +# `zeros N` writes N zero bytes. +zeros() { head -c "$1" /dev/zero; } + +test_cbc_refuses_a_padded_message_past_the_limit() { + expect_fail "8 MiB pads to 2^20 + 1 blocks" \ + "$BC_RUST" tdes-cbc -d encrypt --key "$KEY" < <(zeros "$LIMIT") + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cbc_takes_a_message_that_pads_to_the_limit() { + expect_ok "8 MiB less one byte pads to exactly 2^20 blocks" \ + "$BC_RUST" tdes-cbc -d encrypt --key "$KEY" < <(zeros $((LIMIT - 1))) >/dev/null +} + +test_cfb_refuses_one_byte_past_the_limit() { + expect_fail "tdes-cfb: 2^20 blocks plus a byte" \ + "$BC_RUST" tdes-cfb -d encrypt --key "$KEY" < <(zeros $((LIMIT + 1))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cfb8_counts_a_block_per_byte() { + expect_ok "tdes-cfb8: 2^20 bytes is 2^20 blocks" \ + "$BC_RUST" tdes-cfb8 -d encrypt --key "$KEY" < <(zeros $((1 << 20))) >/dev/null + expect_fail "tdes-cfb8: 2^20 bytes plus one" \ + "$BC_RUST" tdes-cfb8 -d encrypt --key "$KEY" < <(zeros $(((1 << 20) + 1))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_tkw_counts_six_blocks_per_semiblock() { + local bound=$(((1 << 20) / 6 * 4)) + expect_ok "tdes-tkw: $bound bytes is 2^20 blocks" \ + "$BC_RUST" tdes-tkw -d encrypt --key "$KEY" < <(zeros "$bound") >/dev/null + expect_fail "tdes-tkw: one semiblock more" \ + "$BC_RUST" tdes-tkw -d encrypt --key "$KEY" < <(zeros $((bound + 4))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_ctr_runs_out_of_counter_first() { + expect_fail "tdes-ctr: 2^16 blocks plus a byte exhausts the 16-bit counter" \ + "$BC_RUST" tdes-ctr -d encrypt --key "$KEY" < <(zeros $(((1 << 19) + 1))) + assert_stderr_has "DataLimitExceeded" +} + +test_ecb_takes_the_limit_and_refuses_a_block_more() { + expect_ok "tdes-ecb: exactly 2^20 blocks" \ + "$BC_RUST" tdes-ecb -d encrypt --key "$KEY" < <(zeros "$LIMIT") >/dev/null + expect_fail "tdes-ecb: 2^20 blocks plus one" \ + "$BC_RUST" tdes-ecb -d encrypt --key "$KEY" < <(zeros $((LIMIT + 8))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cfb_takes_exactly_the_limit() { + expect_ok "tdes-cfb: exactly 2^20 blocks" \ + "$BC_RUST" tdes-cfb -d encrypt --key "$KEY" < <(zeros "$LIMIT") >/dev/null +} + +test_ctr_takes_its_whole_counter_space() { + expect_ok "tdes-ctr: exactly 2^16 blocks" \ + "$BC_RUST" tdes-ctr -d encrypt --key "$KEY" < <(zeros $((1 << 19))) >/dev/null +} + +test_decrypt_is_not_limited() { + # 2^20 + 1 blocks of ciphertext decrypt (ECB, no padding; CFB with the IV read from the + # input), under either bundle, since the limit is on protecting. + expect_ok "tdes-ecb decrypt past the limit" \ + "$BC_RUST" tdes-ecb -d decrypt --key "$KEY" < <(zeros $((LIMIT + 8))) >/dev/null + expect_ok "tdes2-ecb decrypt past the limit" \ + "$BC_RUST" tdes2-ecb -d decrypt --key "$KEY2" < <(zeros $((LIMIT + 8))) >/dev/null + expect_ok "tdes2-cfb decrypt past the limit" \ + "$BC_RUST" tdes2-cfb -d decrypt --key "$KEY2" < <(zeros $((LIMIT + 16))) >/dev/null +} + +run_all diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs index d5036e85c..2e9c8257b 100644 --- a/crypto/aes/src/kw.rs +++ b/crypto/aes/src/kw.rs @@ -102,27 +102,27 @@ use bouncycastle_cipher::modes::{Kw, Kwp}; /// [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey /// [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey #[allow(non_camel_case_types)] -pub type AES_KW_128<'k> = Kw<'k, AES128Internal>; +pub type AES_KW_128<'k, Dir> = Kw<'k, AES128Internal, Dir>; /// AES-192 Key Wrap (SP 800-38F KW; RFC 3394 with a 192-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_192<'k> = Kw<'k, AES192Internal>; +pub type AES_KW_192<'k, Dir> = Kw<'k, AES192Internal, Dir>; /// AES-256 Key Wrap (SP 800-38F KW; RFC 3394 with a 256-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_256<'k> = Kw<'k, AES256Internal>; +pub type AES_KW_256<'k, Dir> = Kw<'k, AES256Internal, Dir>; /// AES-128 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 128-bit KEK): the worker an /// `AES128Key` hands out. #[allow(non_camel_case_types)] -pub type AES_KWP_128<'k> = Kwp<'k, AES128Internal>; +pub type AES_KWP_128<'k, Dir> = Kwp<'k, AES128Internal, Dir>; /// AES-192 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 192-bit KEK). See /// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_192<'k> = Kwp<'k, AES192Internal>; +pub type AES_KWP_192<'k, Dir> = Kwp<'k, AES192Internal, Dir>; /// AES-256 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 256-bit KEK). See /// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_256<'k> = Kwp<'k, AES256Internal>; +pub type AES_KWP_256<'k, Dir> = Kwp<'k, AES256Internal, Dir>; diff --git a/crypto/aes/tests/alg_name_tests.rs b/crypto/aes/tests/alg_name_tests.rs index f9676a50e..d6d0e5a42 100644 --- a/crypto/aes/tests/alg_name_tests.rs +++ b/crypto/aes/tests/alg_name_tests.rs @@ -158,25 +158,20 @@ fn cmac() { ); } -/// One worker serves both halves of a key wrap, so there is one name per alias to check. +/// A key wrap's two directions report the one name and strength. #[test] fn kw_kwp() { use SecurityStrength::*; - assert_eq!((AES_KW_128::ALG_NAME, AES_KW_128::MAX_SECURITY_STRENGTH), ("AES_KW_128", _128bit)); - assert_eq!((AES_KW_192::ALG_NAME, AES_KW_192::MAX_SECURITY_STRENGTH), ("AES_KW_192", _192bit)); - assert_eq!((AES_KW_256::ALG_NAME, AES_KW_256::MAX_SECURITY_STRENGTH), ("AES_KW_256", _256bit)); - assert_eq!( - (AES_KWP_128::ALG_NAME, AES_KWP_128::MAX_SECURITY_STRENGTH), - ("AES_KWP_128", _128bit) - ); - assert_eq!( - (AES_KWP_192::ALG_NAME, AES_KWP_192::MAX_SECURITY_STRENGTH), - ("AES_KWP_192", _192bit) - ); - assert_eq!( - (AES_KWP_256::ALG_NAME, AES_KWP_256::MAX_SECURITY_STRENGTH), - ("AES_KWP_256", _256bit) - ); + fn check(name: &str, strength: SecurityStrength) { + assert_eq!((Enc::ALG_NAME, Enc::MAX_SECURITY_STRENGTH), (name, strength)); + assert_eq!((Dec::ALG_NAME, Dec::MAX_SECURITY_STRENGTH), (name, strength)); + } + check::, AES_KW_128<'_, Decrypting>>("AES_KW_128", _128bit); + check::, AES_KW_192<'_, Decrypting>>("AES_KW_192", _192bit); + check::, AES_KW_256<'_, Decrypting>>("AES_KW_256", _256bit); + check::, AES_KWP_128<'_, Decrypting>>("AES_KWP_128", _128bit); + check::, AES_KWP_192<'_, Decrypting>>("AES_KWP_192", _192bit); + check::, AES_KWP_256<'_, Decrypting>>("AES_KWP_256", _256bit); } fn key_for>() -> AES128Key { diff --git a/crypto/aes/tests/kw_tests.rs b/crypto/aes/tests/kw_tests.rs index 032c64602..6ba63e106 100644 --- a/crypto/aes/tests/kw_tests.rs +++ b/crypto/aes/tests/kw_tests.rs @@ -15,6 +15,7 @@ use bouncycastle_aes::{ AES192Key, AES256Key, }; use bouncycastle_cipher::modes::{KW, KWP, ModeNames}; +use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::Mode; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; @@ -210,18 +211,18 @@ fn the_three_key_lengths_are_not_interchangeable() { #[test] fn the_keys_hand_out_the_aliased_workers() { let kek = key128::(&RFC3394_KEK[..32]); - let _: AES_KW_128<'_> = kek.create_wrapper(); - let _: AES_KW_128<'_> = kek.create_unwrapper(); + let _: AES_KW_128<'_, Encrypting> = kek.create_wrapper(); + let _: AES_KW_128<'_, Decrypting> = kek.create_unwrapper(); let kek = key192::(&RFC3394_KEK[..48]); - let _: AES_KW_192<'_> = kek.create_wrapper(); + let _: AES_KW_192<'_, Encrypting> = kek.create_wrapper(); let kek = key256::(RFC3394_KEK); - let _: AES_KW_256<'_> = kek.create_unwrapper(); + let _: AES_KW_256<'_, Decrypting> = kek.create_unwrapper(); let kek = key128::(&RFC3394_KEK[..32]); - let _: AES_KWP_128<'_> = kek.create_wrapper(); + let _: AES_KWP_128<'_, Encrypting> = kek.create_wrapper(); let kek = key192::(&RFC3394_KEK[..48]); - let _: AES_KWP_192<'_> = kek.create_unwrapper(); + let _: AES_KWP_192<'_, Decrypting> = kek.create_unwrapper(); let kek = key256::(RFC3394_KEK); - let _: AES_KWP_256<'_> = kek.create_wrapper(); + let _: AES_KWP_256<'_, Encrypting> = kek.create_wrapper(); } /// The sizes the docs quote: the key is the schedule, the worker is the borrow. @@ -233,8 +234,8 @@ fn sizes() { [size_of::(), size_of::(), size_of::()] ); assert_eq!(size_of::>(), size_of::>()); - assert_eq!(size_of::(), size_of::<&AES128Internal>()); - assert_eq!(size_of::(), size_of::<&AES256Internal>()); + assert_eq!(size_of::>(), size_of::<&AES128Internal>()); + assert_eq!(size_of::>(), size_of::<&AES256Internal>()); } /// Equality follows the material: the same bytes compare equal, as does a clone, and different diff --git a/crypto/chacha20-poly1305-aead/src/lib.rs b/crypto/chacha20-poly1305-aead/src/lib.rs index d9db5b671..e21e551f5 100644 --- a/crypto/chacha20-poly1305-aead/src/lib.rs +++ b/crypto/chacha20-poly1305-aead/src/lib.rs @@ -105,6 +105,7 @@ use bouncycastle_chacha20::hazmat::{ChaCha20KeyStream, keystream_at}; use bouncycastle_chacha20::{ChaCha20, ChaCha20Key}; +use bouncycastle_cipher::stream::hazmat::stream_cipher_over; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::KeyStream; @@ -195,7 +196,7 @@ impl<'k, Dir> ChaCha20_Poly1305<'k, Dir> { // already enforced on the parent cipher key; no entropy heuristic applies here. let mac = key_from_bytes(poly1305_key_gen(&mut keystream, &mut block)).into_mac(); Self { - cipher: ChaCha20::from_keystream(keystream), + cipher: stream_cipher_over(keystream), mac, aad_len: 0, data_len: 0, @@ -515,7 +516,7 @@ mod tests { let mut cipher = fresh(key); // `check_data_len` relies on MAX_MESSAGE_LEN being exactly the data keystream's capacity. assert_eq!(cipher.cipher.keystream().remaining_blocks() * 64, MAX_MESSAGE_LEN); - cipher.cipher = ChaCha20::from_keystream(keystream_at(key, &[9; 12], u32::MAX)); + cipher.cipher = stream_cipher_over(keystream_at(key, &[9; 12], u32::MAX)); cipher.data_len = MAX_MESSAGE_LEN - 64; cipher } diff --git a/crypto/chacha20/src/hazmat.rs b/crypto/chacha20/src/hazmat.rs index e7843a958..d83a061b0 100644 --- a/crypto/chacha20/src/hazmat.rs +++ b/crypto/chacha20/src/hazmat.rs @@ -8,6 +8,7 @@ //! and is the cipher to use. use crate::{BLOCK_LEN, ChaCha20Key, KEY_LEN, NONCE_LEN}; +use bouncycastle_cipher::stream::hazmat::PermutationFreeKeyStream; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::KeyStream; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -123,6 +124,9 @@ pub fn keystream_at<'k>( ChaCha20KeyStream::start_at(&key.state, nonce, initial_counter) } +// The block function is all there is: no block permutation, so no decrypt-only one. +impl PermutationFreeKeyStream for ChaCha20KeyStream<'_> {} + impl Algorithm for ChaCha20KeyStream<'_> { const ALG_NAME: &'static str = "ChaCha20"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; diff --git a/crypto/chacha20/src/lib.rs b/crypto/chacha20/src/lib.rs index ff809f1d7..78a46f36a 100644 --- a/crypto/chacha20/src/lib.rs +++ b/crypto/chacha20/src/lib.rs @@ -83,6 +83,7 @@ pub mod hazmat; use bouncycastle_cipher::stream::StreamCipher; +use bouncycastle_cipher::stream::hazmat::stream_cipher_over; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; @@ -154,7 +155,7 @@ impl SymmetricEncryptionKey for ChaCha20Key { ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { let mut nonce = [0u8; NONCE_LEN]; rng.next_bytes_out(&mut nonce)?; - Ok((ChaCha20::from_keystream(ChaCha20KeyStream::start_at(&self.state, &nonce, 0)), nonce)) + Ok((stream_cipher_over(ChaCha20KeyStream::start_at(&self.state, &nonce, 0)), nonce)) } } @@ -165,6 +166,6 @@ impl SymmetricDecryptionKey for ChaCha20Key { Self: 'k; fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { - ChaCha20::from_keystream(ChaCha20KeyStream::start_at(&self.state, nonce, 0)) + stream_cipher_over(ChaCha20KeyStream::start_at(&self.state, nonce, 0)) } } diff --git a/crypto/chacha20/tests/chacha20_bc-test-data.rs b/crypto/chacha20/tests/chacha20_bc-test-data.rs index 2081b61cd..086a2ad5a 100644 --- a/crypto/chacha20/tests/chacha20_bc-test-data.rs +++ b/crypto/chacha20/tests/chacha20_bc-test-data.rs @@ -2,6 +2,7 @@ use bouncycastle_chacha20::hazmat::keystream_at; use bouncycastle_chacha20::{ChaCha20, ChaCha20Key}; use bouncycastle_cipher::Encrypting; +use bouncycastle_cipher::stream::hazmat::stream_cipher_over; use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; @@ -45,5 +46,5 @@ fn rfc8439_blocks_encryption_and_one_time_keys() { /// The RFC's vectors start at counters other than 0, which only the hazmat keystream takes. fn at<'k>(key: &'k ChaCha20Key, nonce: &[u8; 12], counter: u32) -> ChaCha20<'k, Encrypting> { - ChaCha20::from_keystream(keystream_at(key, nonce, counter)) + stream_cipher_over(keystream_at(key, nonce, counter)) } diff --git a/crypto/chacha20/tests/chacha20_tests.rs b/crypto/chacha20/tests/chacha20_tests.rs index e8dc97dfd..9a40e1d67 100644 --- a/crypto/chacha20/tests/chacha20_tests.rs +++ b/crypto/chacha20/tests/chacha20_tests.rs @@ -1,5 +1,6 @@ use bouncycastle_chacha20::hazmat::{ChaCha20KeyStream, keystream_at}; use bouncycastle_chacha20::{ChaCha20, ChaCha20Key}; +use bouncycastle_cipher::stream::hazmat::stream_cipher_over; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::KeyStream; @@ -23,7 +24,7 @@ fn key() -> ChaCha20Key { } fn at<'k>(key: &'k ChaCha20Key, nonce: &[u8; 12], counter: u32) -> ChaCha20<'k, Encrypting> { - ChaCha20::from_keystream(keystream_at(key, nonce, counter)) + stream_cipher_over(keystream_at(key, nonce, counter)) } #[test] @@ -98,7 +99,7 @@ fn a_keystream_at_its_last_counter_has_no_blocks_left() { ks.apply_blocks(&mut [[0u8; 64]; 1]); assert_eq!(ks.remaining_blocks(), 0); assert!(matches!( - ChaCha20::::from_keystream(ks).do_encrypt_inplace(&mut [0u8; 1]), + stream_cipher_over::<_, Encrypting, 12, 64>(ks).do_encrypt_inplace(&mut [0u8; 1]), Err(SymmetricCipherError::DataLimitExceeded) )); } diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs index a6632f1a9..9045a361a 100644 --- a/crypto/cipher/src/lib.rs +++ b/crypto/cipher/src/lib.rs @@ -45,12 +45,13 @@ pub struct Encrypting; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Decrypting; -mod sealed { +pub(crate) mod sealed { /// Private supertrait of [`Direction`](super::Direction): only this module can name it, so /// only the two markers below can implement `Direction`. pub trait Sealed {} impl Sealed for super::Encrypting {} impl Sealed for super::Decrypting {} + // The keystream types implement it beside their definitions, for `KeyStreamPolicy`. } /// Selects a type by direction: `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. @@ -81,12 +82,18 @@ mod sealed { pub trait Direction: sealed::Sealed { /// `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. type Select; + + /// `true` for [`Encrypting`]: the direction a permutation that decrypts only + /// (`BlockPermutation::DECRYPT_ONLY`) refuses, at compile time, however the worker is made. + const IS_ENCRYPTING: bool; } impl Direction for Encrypting { type Select = Enc; + const IS_ENCRYPTING: bool = true; } impl Direction for Decrypting { type Select = Dec; + const IS_ENCRYPTING: bool = false; } diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index a4866f2e5..682a7c506 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -147,6 +147,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; let iv = random_iv::(rng)?; Ok((PaddedBlockCipherEncryptor::from_inner(Cbc::from_perm(state, iv)), iv)) } @@ -185,6 +191,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; let iv = random_iv::(rng)?; Ok((Cbc::from_perm(state, iv), iv)) } diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 373f57cb9..7b2d9d453 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -98,7 +98,7 @@ use crate::modes::ModeNames; use crate::modes::ctr::apply_counter_blocks; use crate::modes::iv::random_iv; -use crate::stream::StreamCipher; +use crate::stream::{KeyStreamPolicy, StreamCipher}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, @@ -114,7 +114,7 @@ use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; /// CCM (SP 800-38C) over any [`ElectronicCodeBook`] with a 128-bit block, for Sec 3's "packet /// environment, i.e., when all of the data is available in storage before CCM is applied": the @@ -167,6 +167,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The spec's `q`: the octet length of the payload-length field `Q`. A.1 requires `n + q = 15`. const Q_LEN: usize = CcmKeyStream::::Q_LEN; @@ -621,6 +622,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: for Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The permutation's name for this mode, [`ModeNames::CCM_ALG_NAME`]. const ALG_NAME: &'static str = P::CCM_ALG_NAME; @@ -717,6 +719,21 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> crate::sealed::Sealed + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> +where + P: BlockPermutation + ModeNames, +{ +} + +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> KeyStreamPolicy + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> +where + P: BlockPermutation + ModeNames, +{ + const DECRYPT_ONLY: bool = P::DECRYPT_ONLY; +} + impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> KeyStream for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where @@ -788,6 +805,7 @@ impl< > CcmAdapter<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The compile-time check for the trait adapters, run from both [`CcmPacketEncryptor`]'s and /// [`CcmPacketDecryptor`]'s constructors and from their one-shots, which never build an adapter, so diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index 67ba40ffc..108227dc0 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -184,6 +184,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; // `I1 = IV`. let iv = random_iv::(rng)?; Ok((Cfb::start(state, iv), iv)) diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs index 498d939bc..d7231605d 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -93,6 +93,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; // `I1 = IV`. let iv = random_iv::(rng)?; Ok((Cfb8 { perm: state, chain: iv, _dir: PhantomData }, iv)) diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index 23f1694dd..f395e4261 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -163,6 +163,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; let nonce = random_iv::(rng)?; Ok((StreamCipher::from_keystream(CtrKeyStream::from_perm(state, nonce)), nonce)) } diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 409da163d..4e0b91795 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -186,7 +186,7 @@ use crate::modes::Ctr; use crate::modes::ModeNames; use crate::modes::hazmat::ctr_key_stream::CtrKeyStream; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ AeadDecryptMode, AeadEncryptMode, BlockPermutation, ElectronicCodeBook, Mode, @@ -354,6 +354,7 @@ where impl<'k, P, Dir, const TAG_LEN: usize> Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { /// The compile-time shape check: `TAG_LEN` must be one of Sec 5.2.1.2's five recommended tag /// lengths in bytes (96, 104, 112, 120, 128 bits -- Appendix C's 32- and 64-bit tags are a @@ -493,6 +494,7 @@ where impl<'k, P, Dir, const TAG_LEN: usize> Algorithm for Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { const ALG_NAME: &'static str = P::GCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; @@ -1235,7 +1237,7 @@ mod tests { /// The CTR half positioned at the last counter block under `nonce`: 16 bytes of keystream /// left, as a worker that had encrypted `2^32 - 3` blocks would have. - fn ctr_at_the_last_block( + fn ctr_at_the_last_block( state: &GcmKeyState, nonce: [u8; GCM_NONCE_LEN], ) -> Ctr<'_, Toy, Dir, 16, GCM_NONCE_LEN> { diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index f239d3bc6..69c05c29f 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -11,7 +11,7 @@ use bouncycastle_core::traits::Algorithm; #[allow(unused_imports)] use crate::modes::Ctr; #[allow(unused_imports)] -use crate::stream::StreamCipher; +use crate::stream::{KeyStreamPolicy, StreamCipher}; // end of imports needed for docs /// The CTR keystream `Oj = CIPH_K(Tj)` (SP 800-38A Sec 6.5) over any [`BlockPermutation`], with @@ -126,6 +126,21 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> crate::sealed::Sealed + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> +where + P: BlockPermutation + ModeNames, +{ +} + +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> KeyStreamPolicy + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> +where + P: BlockPermutation + ModeNames, +{ + const DECRYPT_ONLY: bool = P::DECRYPT_ONLY; +} + impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> KeyStream for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index 8a34e4051..bea3e25e4 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -117,6 +117,12 @@ where P: 'k; fn encrypt_init<'k>(state: &'k Self::KeyState) -> Self::Encryptor<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; Ecb::from_perm(state) } } @@ -153,6 +159,12 @@ where state: &'k Self::KeyState, _rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; 0]), SymmetricCipherError> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" + ) + }; Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state)), [])) } } diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index e7f8ad1c6..8a6c1bcfa 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -75,12 +75,13 @@ //! )); //! ``` //! -//! # No direction marker +//! # The direction is in the type //! -//! Unlike [`Ecb`](crate::modes::hazmat::ecb::Ecb) and the other modes, [`Kw`] has no `Dir` -//! parameter. The wrapping and unwrapping halves are already separate traits ([`KeyWrapper`] / -//! [`KeyUnwrapper`]), so a policy can import one trait and not the other, and there is no -//! per-direction state for a marker type to select. +//! [`Kw<'k, P, Dir>`](Kw) carries [`Encrypting`] or [`Decrypting`] as the cipher modes do: +//! [`KeyWrapper`] is implemented for the encrypting worker alone and [`KeyUnwrapper`] for the +//! decrypting one, so an unwrapper has no wrapping method to call, and a permutation that +//! decrypts only ([`BlockPermutation::DECRYPT_ONLY`]) is refused a wrapper at compile time in +//! `wrap_init`. The worker has no per-direction state; the marker is zero-sized. //! //! # The wrapping function runs in place, without the shift register //! @@ -122,6 +123,7 @@ //! [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey use super::ModeNames; +use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -131,6 +133,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; /// The block length, in bytes, of the permutation under KW and KWP. SP 800-38F Sec 5.1: "For KW /// and KWP, the underlying block cipher shall be approved, and the block size shall be 128 bits." @@ -169,71 +172,87 @@ pub(crate) const fn kw_wrapped_len(len: usize) -> usize { len.saturating_add(SEMIBLOCK_LEN) } -/// The wrapping function W (SP 800-38F Algorithm 1), in place over `S = a || r`. +/// The wrapping function W (SP 800-38F Algorithm 1), in place over `S = a || r`, with +/// `SEMI`-byte semiblocks over a `BLOCK`-byte permutation: 8 and 16 for KW and KWP, 4 and 8 for +/// TKW, whose TW (Algorithm 7) is this same loop with `[t]32` in place of `[t]64`. /// /// `a` holds `S1` and `r` holds `S2 .. Sn`, so `r.len() == n - 1`, which must be at least 2 /// (Algorithm 1 is defined for `n >= 3`). On return `a` holds `C1` and `r` holds `C2 .. Cn`. /// /// Runs in place rather than through the shift register of the spec's step 2; see the module /// docs for why the two are the same computation. -pub(crate) fn w

(perm: &P, a: &mut [u8; SEMIBLOCK_LEN], r: &mut [[u8; SEMIBLOCK_LEN]]) -where - P: BlockPermutation, +pub(crate) fn w( + perm: &P, + a: &mut [u8; SEMI], + r: &mut [[u8; SEMI]], +) where + P: BlockPermutation, { + const { assert!(2 * SEMI == BLOCK, "a semiblock is half a block (SP 800-38F Sec 4.1)") }; debug_assert!(r.len() >= 2, "Algorithm 1 is defined on n >= 3 semiblocks"); // Everything that passes through `block` is derived from the key being wrapped, so it is // scrubbed when this function returns. - let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); + let mut block: Secret<[u8; BLOCK]> = Secret::new(); // Step 1a: s = 6(n-1) steps, indexed by t = 1, ..., s. let mut t: u64 = 0; for _ in 0..6 { for r_i in r.iter_mut() { t += 1; // Step 2: CIPH_K(A_{t-1} || R2_{t-1}), where R2_{t-1} is this register (module docs). - let (halves, _) = block.as_chunks_mut::(); + let (halves, _) = block.as_chunks_mut::(); halves[0] = *a; halves[1] = *r_i; perm.encrypt_block(&mut block); - let (halves, _) = block.as_chunks::(); - // Step 2a: A_t = MSB64(CIPH_K(...)) xor [t]64. + let (halves, _) = block.as_chunks::(); + // Step 2a: A_t = MSB(CIPH_K(...)) xor [t]: the counter as a big-endian semiblock, the + // low SEMI bytes of its 8-byte form. + let t_bytes = t.to_be_bytes(); for (a_byte, (c_byte, t_byte)) in - a.iter_mut().zip(halves[0].iter().zip(t.to_be_bytes())) + a.iter_mut().zip(halves[0].iter().zip(&t_bytes[8 - SEMI..])) { *a_byte = c_byte ^ t_byte; } - // Step 2c: R_n_t = LSB64(CIPH_K(...)), which lands back in this register. + // Step 2c: R_n_t = LSB(CIPH_K(...)), which lands back in this register. *r_i = halves[1]; } } debug_assert_eq!(t as usize, 6 * r.len(), "Algorithm 1 step 1a: s = 6(n-1)"); } -/// The unwrapping function W⁻¹ (SP 800-38F Algorithm 2), in place over `C = a || r`. +/// The unwrapping function W⁻¹ (SP 800-38F Algorithm 2), in place over `C = a || r`, with the +/// same `BLOCK` / `SEMI` parameters as [`w`] (TW⁻¹, Algorithm 8, is the 4-byte-semiblock form). /// /// `a` holds `C1` and `r` holds `C2 .. Cn`, with `r.len() == n - 1 >= 2`. On return `a` holds /// `S1` and `r` holds `S2 .. Sn`. The registers are visited in the reverse of the order [`w`] /// visits them, so that step `t` here undoes step `t` there. -pub(crate) fn w_inv

(perm: &P, a: &mut [u8; SEMIBLOCK_LEN], r: &mut [[u8; SEMIBLOCK_LEN]]) -where - P: BlockPermutation, +pub(crate) fn w_inv( + perm: &P, + a: &mut [u8; SEMI], + r: &mut [[u8; SEMI]], +) where + P: BlockPermutation, { + const { assert!(2 * SEMI == BLOCK, "a semiblock is half a block (SP 800-38F Sec 4.1)") }; debug_assert!(r.len() >= 2, "Algorithm 2 is defined on n >= 3 semiblocks"); - let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); + let mut block: Secret<[u8; BLOCK]> = Secret::new(); // Step 1a: s = 6(n-1); step 2 runs t = s, s-1, ..., 1. let mut t: u64 = 6 * r.len() as u64; for _ in 0..6 { for r_i in r.iter_mut().rev() { - // Steps 2a/2b: CIPH⁻¹_K((A_t xor [t]64) || R_n_t), where R_n_t is this register. - let (halves, _) = block.as_chunks_mut::(); - for (b, (a_byte, t_byte)) in halves[0].iter_mut().zip(a.iter().zip(t.to_be_bytes())) { + // Steps 2a/2b: CIPH⁻¹_K((A_t xor [t]) || R_n_t), where R_n_t is this register. + let t_bytes = t.to_be_bytes(); + let (halves, _) = block.as_chunks_mut::(); + for (b, (a_byte, t_byte)) in + halves[0].iter_mut().zip(a.iter().zip(&t_bytes[8 - SEMI..])) + { *b = a_byte ^ t_byte; } halves[1] = *r_i; perm.decrypt_block(&mut block); - let (halves, _) = block.as_chunks::(); - // Step 2a: A_{t-1} = MSB64(...). + let (halves, _) = block.as_chunks::(); + // Step 2a: A_{t-1} = MSB(...). *a = halves[0]; - // Step 2b: R2_{t-1} = LSB64(...), which lands back in this register. + // Step 2b: R2_{t-1} = LSB(...), which lands back in this register. *r_i = halves[1]; t -= 1; } @@ -263,11 +282,17 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Wrapper<'k> - = Kw<'k, P> + = Kw<'k, P, Encrypting> where P: 'k; fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; Kw::from_perm(state) } } @@ -277,7 +302,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Unwrapper<'k> - = Kw<'k, P> + = Kw<'k, P, Decrypting> where P: 'k; @@ -289,28 +314,29 @@ where /// KW over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a /// [`KW`]-bound key hands out, borrowing the keyed permutation for `'k`. /// -/// There is no state beyond the borrow and no direction marker: both halves of the algorithm are -/// the [`KeyWrapper`] and [`KeyUnwrapper`] traits, which this type implements together. See the -/// module docs for usage and the security notes. -pub struct Kw<'k, P> +/// There is no state beyond the borrow; `Dir` is [`Encrypting`] for the wrapper, which alone +/// implements [`KeyWrapper`], and [`Decrypting`] for the unwrapper, which alone implements +/// [`KeyUnwrapper`]. See the module docs for usage and the security notes. +pub struct Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { perm: &'k P, + _dir: PhantomData

, } -impl<'k, P> Kw<'k, P> +impl<'k, P, Dir> Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { /// Over an already-keyed permutation, which the worker borrows: what [`KW`]'s mode impls /// build for a key's roles. pub(crate) fn from_perm(perm: &'k P) -> Self { - Self { perm } + Self { perm, _dir: PhantomData } } } -impl<'k, P> Algorithm for Kw<'k, P> +impl<'k, P, Dir> Algorithm for Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { @@ -320,7 +346,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P> KeyWrapper for Kw<'k, P> +impl<'k, P> KeyWrapper for Kw<'k, P, Encrypting> where P: BlockPermutation + ModeNames, { @@ -376,12 +402,12 @@ where r.copy_from_slice(plaintext); let (r, _) = r.as_chunks_mut::(); // Step 3: C = W(S). - w(self.perm, a, r); + w::(self.perm, a, r); Ok(ct_len) } } -impl<'k, P> KeyUnwrapper for Kw<'k, P> +impl<'k, P> KeyUnwrapper for Kw<'k, P, Decrypting> where P: BlockPermutation + ModeNames, { @@ -439,7 +465,7 @@ where let r = &mut plaintext[..pt_len]; r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); let (r, _) = r.as_chunks_mut::(); - w_inv(self.perm, &mut a, r); + w_inv::(self.perm, &mut a, r); // Step 3: if MSB64(S) != ICV1, FAIL. Compared in constant time, and the buffer is scrubbed // so that a caller who ignores the `Result` is not left holding the unauthenticated bytes. diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index 9c26d69e7..b36c19d5e 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -62,6 +62,7 @@ use super::ModeNames; use super::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; +use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -71,6 +72,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_zero_bytes}; use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; /// ICV2, the integrity check value of KWP (Algorithm 5, step 1): `0xA65959A6`. const ICV2: [u8; 4] = [0xA6, 0x59, 0x59, 0xA6]; @@ -125,11 +127,17 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Wrapper<'k> - = Kwp<'k, P> + = Kwp<'k, P, Encrypting> where P: 'k; fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; Kwp::from_perm(state) } } @@ -139,7 +147,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Unwrapper<'k> - = Kwp<'k, P> + = Kwp<'k, P, Decrypting> where P: 'k; @@ -149,27 +157,28 @@ where } /// KWP over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a -/// [`KWP`]-bound key hands out, borrowing the keyed permutation for `'k`, as -/// [`Kw`](super::kw::Kw) does. See the module docs. -pub struct Kwp<'k, P> +/// [`KWP`]-bound key hands out, borrowing the keyed permutation for `'k` and carrying its +/// direction in `Dir`, as [`Kw`](super::kw::Kw) does. See the module docs. +pub struct Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { perm: &'k P, + _dir: PhantomData, } -impl<'k, P> Kwp<'k, P> +impl<'k, P, Dir> Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { /// Over an already-keyed permutation, which the worker borrows: what [`KWP`]'s mode impls /// build for a key's roles. pub(crate) fn from_perm(perm: &'k P) -> Self { - Self { perm } + Self { perm, _dir: PhantomData } } } -impl<'k, P> Algorithm for Kwp<'k, P> +impl<'k, P, Dir> Algorithm for Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { @@ -179,7 +188,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P> KeyWrapper for Kwp<'k, P> +impl<'k, P> KeyWrapper for Kwp<'k, P, Encrypting> where P: BlockPermutation + ModeNames, { @@ -246,13 +255,13 @@ where let (a, r) = s.split_at_mut(SEMIBLOCK_LEN); let (a, _) = a.as_chunks_mut::(); let (r, _) = r.as_chunks_mut::(); - w(self.perm, &mut a[0], r); + w::(self.perm, &mut a[0], r); } Ok(ct_len) } } -impl<'k, P> KeyUnwrapper for Kwp<'k, P> +impl<'k, P> KeyUnwrapper for Kwp<'k, P, Decrypting> where P: BlockPermutation + ModeNames, { @@ -340,7 +349,7 @@ where a.copy_from_slice(&ciphertext[..SEMIBLOCK_LEN]); r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); let (r, _) = r.as_chunks_mut::(); - w_inv(self.perm, &mut a, r); + w_inv::(self.perm, &mut a, r); } // Steps 4-8: every check is made, and the verdicts combined with `&` rather than `&&`, so diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs index 3f8821c1c..1eb35d145 100644 --- a/crypto/cipher/src/modes/mod.rs +++ b/crypto/cipher/src/modes/mod.rs @@ -22,8 +22,9 @@ //! | GMAC | [`gmac`] | SP 800-38D Sec 5.2 | A MAC, not a cipher: GCM with no plaintext, so the tag is bound to a 96-bit nonce | //! | KW | [`kw`] | SP 800-38F Sec 6.2 | Key Wrap (RFC 3394). **Authenticated**, deterministic, no IV; whole 8-byte semiblocks only | //! | KWP | [`kwp`] | SP 800-38F Sec 6.3 | Key Wrap with Padding (RFC 5649). As KW, for data of any length | +//! | TKW | [`tkw`] | SP 800-38F Sec 7 | TDEA Key Wrap: KW at 64-bit blocks and 32-bit semiblocks. **Legacy only** | //! -//! They divide three ways, plus the key-wrap pair. +//! They divide three ways, plus the key-wrap trio. //! //! **ECB and CBC are block ciphers** ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]): whole //! blocks in, whole blocks out, and arbitrary-length data needs the padding layer. @@ -39,7 +40,7 @@ //! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with no option to provide AAD, and //! the tag inline. //! -//! **KW and KWP are key-wrap algorithms** ([`KeyWrapper`] / [`KeyUnwrapper`]): one-shot, +//! **KW, KWP and TKW are key-wrap algorithms** ([`KeyWrapper`] / [`KeyUnwrapper`]): one-shot, //! deterministic and authenticated, with no IV and no streaming API, for wrapping a key (or a //! small, non-repeating secret) under a key encryption key. SP 800-38F Sec 3.1 approves them for //! general data as well, but they are deterministic, so for data that may repeat an AEAD is the @@ -53,7 +54,7 @@ //! //! Each mode is also a **mode type** -- [`CBC`], [`CCM`], [`CCM_Packet`], [`CFB`], [`CFB8`], //! [`CTR`], [`GCM`], [`hazmat::ecb::ECB`], the MACs [`CMAC`] and [`GMAC`], and the key wraps -//! [`KW`] and [`KWP`] -- the `M` a mode-bound +//! [`KW`], [`KWP`] and [`TKW`] -- the `M` a mode-bound //! key such as `bouncycastle_aes::AES128Key` is parameterised by. The mode type implements the //! hazmat mode traits of [`bouncycastle_core::hazmat`], which is how a key hands out the mode's //! encryptors and decryptors through the role traits of `bouncycastle_core::symmetric_key`; a @@ -178,6 +179,7 @@ pub mod hazmat; mod iv; pub mod kw; pub mod kwp; +pub mod tkw; pub use cbc::{CBC, Cbc}; pub use ccm::{ @@ -191,6 +193,7 @@ pub use gcm::{GCM, GCM_NONCE_LEN, Gcm, GcmKeyState}; pub use gmac::{GMAC, Gmac}; pub use kw::{KW, KW_BLOCK_LEN, Kw, SEMIBLOCK_LEN}; pub use kwp::{KWP, Kwp}; +pub use tkw::{TKW, TKW_BLOCK_LEN, TKW_SEMIBLOCK_LEN, Tkw}; /// Selects the whole-block form of a mode that also has a padded form: `CBC` is a key /// for the block-aligned [`Cbc`] itself, on the block key path, where `CBC` is one for @@ -222,7 +225,8 @@ pub trait ModeNames { const GCM_ALG_NAME: &'static str; /// Reported by [`Gmac`]. const GMAC_ALG_NAME: &'static str; - /// Reported by [`Kw`]. + /// Reported by [`Kw`] over a 128-bit block and by [`Tkw`] over a 64-bit one: a permutation + /// has one block length, so one key-wrap algorithm. const KW_ALG_NAME: &'static str; /// Reported by [`Kwp`]. const KWP_ALG_NAME: &'static str; diff --git a/crypto/cipher/src/modes/tkw.rs b/crypto/cipher/src/modes/tkw.rs new file mode 100644 index 000000000..a71cb7314 --- /dev/null +++ b/crypto/cipher/src/modes/tkw.rs @@ -0,0 +1,314 @@ +//! The TDEA Key Wrap algorithm, TKW (NIST SP 800-38F Sec 7), over any 64-bit block permutation. +//! +//! TKW is [`Kw`](crate::modes::kw::Kw) at half the width: the same wrapping function over 4-byte +//! semiblocks with a 32-bit step counter (TW, Algorithm 7) and the 32-bit ICV3 +//! `0xA6A6A6A6`, wrapping data that is a whole number of 4-byte semiblocks under a TDEA key +//! encryption key. Everything in the [`kw`](crate::modes::kw) module docs -- the key path, the +//! trait shape, the in-place wrapping function, the security notes -- applies here too; this +//! module only documents what differs. SP 800-38F Sec 5.1 fixes the block cipher to TDEA, so the +//! only key type is `bouncycastle_tdes::TDES3Key`, whose aliases `TDES_TKW` and the +//! unwrap-only `TDES2_TKW` are in that crate. +//! +//! # Legacy only +//! +//! SP 800-38F Sec 8: TKW-AE and TKW-AD "are approved to support legacy systems but should not be +//! used for new applications". Its forgery probability is 2^-32 per attempt against KW's 2^-64 +//! (Appendix A.3), and TDEA itself is a legacy algorithm; see the `bouncycastle-tdes` crate's +//! security considerations. Two-key TDEA may not wrap at all (SP 800-131A Rev 2 Table 6): a +//! permutation that decrypts only +//! ([`BlockPermutation::DECRYPT_ONLY`]) refuses to make a wrapper at compile time, as the +//! encrypting modes refuse their encryptors, while its unwrapper works -- and the unwrapper, +//! `Tkw<'k, P, Decrypting>`, is a distinct type with no wrapping method. +//! +//! # The length limits +//! +//! Table 1: a plaintext of 2 to 2^28 - 1 semiblocks, a ciphertext of 3 to 2^28. The fixed-length +//! API checks them at compile time and the run-time API returns +//! [`SymmetricCipherError::InvalidInputLength`] outside them. + +use super::ModeNames; +use super::kw::{w, w_inv}; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::{ + BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, +}; +use bouncycastle_core::key_material::KeyType; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +/// The block length, in bytes, of the permutation under TKW. SP 800-38F Sec 5.1: "For TKW, the +/// underlying block cipher is specified to be TDEA, and the block size is therefore 64 bits". +pub const TKW_BLOCK_LEN: usize = 8; + +/// A TKW semiblock (SP 800-38F Sec 4.1): half the block, 32 bits. +pub const TKW_SEMIBLOCK_LEN: usize = TKW_BLOCK_LEN / 2; + +/// ICV3, the integrity check value of TKW (Algorithm 9, step 1): `0xA6A6A6A6`. +const ICV3: [u8; TKW_SEMIBLOCK_LEN] = [0xA6; TKW_SEMIBLOCK_LEN]; + +/// The most semiblocks of plaintext TKW-AE accepts. Table 1: 2 to 2^28 - 1 semiblocks, the upper +/// bound being a requirement (Sec 5.3.1) motivated in Appendix A.4. +const TKW_MAX_PLAINTEXT_SEMIBLOCKS: u64 = (1u64 << 28) - 1; + +/// Whether `len` bytes is a plaintext length TKW is defined on: 2 to 2^28 - 1 whole semiblocks +/// (SP 800-38F Table 1). +pub(crate) const fn tkw_plaintext_len_is_valid(len: usize) -> bool { + len.is_multiple_of(TKW_SEMIBLOCK_LEN) + && len >= 2 * TKW_SEMIBLOCK_LEN + && (len / TKW_SEMIBLOCK_LEN) as u64 <= TKW_MAX_PLAINTEXT_SEMIBLOCKS +} + +/// Whether `len` bytes is a ciphertext length TKW-AD is defined on: 3 to 2^28 whole semiblocks +/// (SP 800-38F Table 1). +pub(crate) const fn tkw_ciphertext_len_is_valid(len: usize) -> bool { + len.is_multiple_of(TKW_SEMIBLOCK_LEN) + && len >= 3 * TKW_SEMIBLOCK_LEN + && (len / TKW_SEMIBLOCK_LEN) as u64 <= TKW_MAX_PLAINTEXT_SEMIBLOCKS + 1 +} + +/// The TKW ciphertext length for a plaintext of `len` bytes: one semiblock longer. Algorithm 9 +/// computes `C = TW(ICV3 || P)`, and TW preserves the length of its input. +pub(crate) const fn tkw_wrapped_len(len: usize) -> usize { + len.saturating_add(TKW_SEMIBLOCK_LEN) +} + +/// The TKW mode type, as a key type names it: `TDES3Key`. Its key state is the keyed +/// permutation alone, and its workers are [`Tkw`] on both sides. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TKW; + +impl

Mode

for TKW +where + P: BlockPermutation + ModeNames + Clone, +{ + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl KeyWrapMode for TKW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Wrapper<'k> + = Tkw<'k, P, Encrypting> + where + P: 'k; + + fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; + Tkw::from_perm(state) + } +} + +impl KeyUnwrapMode for TKW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Unwrapper<'k> + = Tkw<'k, P, Decrypting> + where + P: 'k; + + fn unwrap_init<'k>(state: &'k Self::KeyState) -> Self::Unwrapper<'k> { + Tkw::from_perm(state) + } +} + +/// TKW over any 64-bit permutation that impls [`BlockPermutation`]: the one-shot worker a +/// [`TKW`]-bound key hands out, borrowing the keyed permutation for `'k` and carrying its +/// direction in `Dir`, as [`Kw`](super::kw::Kw) does. It reports the permutation's +/// [`ModeNames::KW_ALG_NAME`], since a permutation has one block length and so one key-wrap +/// algorithm. See the module docs. +pub struct Tkw<'k, P, Dir> +where + P: BlockPermutation + ModeNames, +{ + perm: &'k P, + _dir: PhantomData

, +} + +impl<'k, P, Dir> Tkw<'k, P, Dir> +where + P: BlockPermutation + ModeNames, +{ + /// Over an already-keyed permutation, which the worker borrows: what [`TKW`]'s mode impls + /// build for a key's roles. + pub(crate) fn from_perm(perm: &'k P) -> Self { + Self { perm, _dir: PhantomData } + } +} + +impl<'k, P, Dir> Algorithm for Tkw<'k, P, Dir> +where + P: BlockPermutation + ModeNames, +{ + /// The permutation's name for its key wrap, [`ModeNames::KW_ALG_NAME`]. + const ALG_NAME: &'static str = P::KW_ALG_NAME; + /// TKW does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl<'k, P> KeyWrapper for Tkw<'k, P, Encrypting> +where + P: BlockPermutation + ModeNames, +{ + /// TKW-AE with the lengths fixed at compile time. `KEY_LEN` must be a valid TKW plaintext + /// length and `CT_LEN` one semiblock more; anything else fails to compile. + fn wrap_key_out( + &self, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + const { + assert!( + tkw_plaintext_len_is_valid(KEY_LEN), + "TKW wraps 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == tkw_wrapped_len(KEY_LEN), + "a TKW ciphertext is exactly one 4-byte semiblock longer than its plaintext" + ); + } + self.wrap_out(key, ciphertext) + } + + /// One semiblock more than the plaintext. + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + tkw_wrapped_len(plaintext_len) + } + + /// TKW-AE (SP 800-38F Algorithm 9). + fn wrap_out( + &self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + // Nothing stale may survive a failure, so the whole buffer starts from zero. + ciphertext.fill(0); + let ct_len = tkw_wrapped_len(plaintext.len()); + if ciphertext.len() < ct_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); + } + if !tkw_plaintext_len_is_valid(plaintext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "TKW wraps 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)", + )); + } + + // Steps 1-2: S = ICV3 || P, assembled in the caller's buffer so that the plaintext is + // copied exactly once and TW can run on it in place. + let (a, r) = ciphertext[..ct_len].split_at_mut(TKW_SEMIBLOCK_LEN); + let (a, _) = a.as_chunks_mut::(); + let a = &mut a[0]; + *a = ICV3; + r.copy_from_slice(plaintext); + let (r, _) = r.as_chunks_mut::(); + // Step 3: C = TW(S). + w::(self.perm, a, r); + Ok(ct_len) + } +} + +impl<'k, P> KeyUnwrapper for Tkw<'k, P, Decrypting> +where + P: BlockPermutation + ModeNames, +{ + /// TKW-AD with the lengths fixed at compile time; the same pairs as + /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. + fn unwrap_key_out( + &self, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + const { + assert!( + tkw_plaintext_len_is_valid(KEY_LEN), + "TKW unwraps to 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == tkw_wrapped_len(KEY_LEN), + "a TKW ciphertext is exactly one 4-byte semiblock longer than its plaintext" + ); + } + // `key` is exactly the n-1 semiblocks the run-time method writes into, and that method + // scrubs the buffer on every failure it can reach from here. + self.unwrap_out(ciphertext, &mut key[..]) + } + + /// One semiblock less than the ciphertext: exact for TKW. + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TKW_SEMIBLOCK_LEN) + } + + /// TKW-AD (SP 800-38F Algorithm 10). + fn unwrap_out( + &self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + // Nothing this function writes may survive a failure, so start from zero and only ever + // return `Ok` with the buffer fully written. + plaintext.fill(0); + let pt_len = self.unwrap_out_max_len(ciphertext.len()); + if plaintext.len() < pt_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); + } + if !tkw_ciphertext_len_is_valid(ciphertext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "a TKW ciphertext is 3 to 2^28 whole 4-byte semiblocks (SP 800-38F Table 1)", + )); + } + + // Step 3: S = TW⁻¹(C). C1 goes to a scratch semiblock that is scrubbed on return; C2..Cn + // go straight into the caller's buffer, which is exactly n-1 semiblocks, so TW⁻¹ runs + // in place and the recovered plaintext is never copied. + let mut a: Secret<[u8; TKW_SEMIBLOCK_LEN]> = Secret::new(); + a.copy_from_slice(&ciphertext[..TKW_SEMIBLOCK_LEN]); + let r = &mut plaintext[..pt_len]; + r.copy_from_slice(&ciphertext[TKW_SEMIBLOCK_LEN..]); + let (r, _) = r.as_chunks_mut::(); + w_inv::(self.perm, &mut a, r); + + // Step 4: if MSB32(S) != ICV3, FAIL. Compared in constant time, and the buffer is + // scrubbed so that a caller who ignores the `Result` is not left holding the + // unauthenticated bytes. + if !ct_eq_bytes(&*a, &ICV3) { + plaintext.fill(0); + return Err(SymmetricCipherError::DecryptionFailed); + } + // Step 5: P = LSB32(n-1)(S), which is already in place. + Ok(pt_len) + } +} + +#[cfg(test)] +mod tests { + //! The 2^28 upper bounds of SP 800-38F Table 1, which no buffer can reach: the last valid + //! plaintext is 2^28 - 1 semiblocks, the last valid ciphertext 2^28. The lower bounds and + //! the alignment rule are pinned through the key in `bouncycastle-tdes`. + + use super::*; + + #[test] + fn the_table_1_upper_bounds() { + const MAX_PT: usize = ((1usize << 28) - 1) * TKW_SEMIBLOCK_LEN; + assert!(tkw_plaintext_len_is_valid(MAX_PT)); + assert!(!tkw_plaintext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN / 2)); + assert!(!tkw_plaintext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN)); + assert!(tkw_ciphertext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN)); + assert!(!tkw_ciphertext_len_is_valid(MAX_PT + 2 * TKW_SEMIBLOCK_LEN)); + } +} diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index 06035e1f6..cc267f3a6 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -9,7 +9,7 @@ //! //! A mode whose keystream depends on the data, such as CFB, implements the traits itself. -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting, sealed}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::KeyStream; use bouncycastle_core::security_strength::SecurityStrength; @@ -54,6 +54,17 @@ where _marker: PhantomData, } +/// Whether a keystream's permutation decrypts only (`BlockPermutation::DECRYPT_ONLY`), so that +/// [`StreamCipher`] can refuse, at compile time, to be made in the encrypting direction over it, +/// whatever constructor makes it. `KeyStream` itself is `dyn`-compatible and so carries no +/// constant; this sealed trait is implemented for the crate's keystream types. A keystream from +/// outside the crate reaches [`StreamCipher`] only through [`hazmat::stream_cipher_over`], whose +/// marker trait is the implementor's promise that no such permutation is under it. +pub trait KeyStreamPolicy: sealed::Sealed { + /// The permutation's `DECRYPT_ONLY`. + const DECRYPT_ONLY: bool; +} + impl StreamCipher where @@ -62,11 +73,21 @@ where /// Wraps a keystream that has already been constructed and positioned, such as one that /// starts part-way into its counter space (GCM's GCTR starts at `inc32(J0)`). /// - /// # 🚨 Security Considerations 🚨 - /// This bypasses init-data generation: the keystream's nonce is whatever it was constructed - /// with, and the caller is responsible for it never repeating under the key. A key's - /// `create_encryptor` is the safe path. - pub fn from_keystream(keystream: KS) -> Self { + /// Crate-private: it bypasses init-data generation (the keystream's nonce is whatever it was + /// constructed with, and a repeat under the key is the two-time pad), and a decryptor's + /// keystream wrapped in the encrypting direction would be an encryptor over a permutation + /// that decrypts only. A key's `create_encryptor` is the public path. + pub(crate) fn from_keystream(keystream: KS) -> Self + where + KS: KeyStreamPolicy, + Dir: Direction, + { + const { + assert!( + !(Dir::IS_ENCRYPTING && KS::DECRYPT_ONLY), + "this keystream's permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting stream cipher may be made over it" + ) + }; Self::check_shape(); Self { keystream, pending: Secret::new(), used: BLOCK_LEN, _marker: PhantomData } } @@ -277,3 +298,36 @@ where self.apply(data) } } + +/// Raw ways into [`StreamCipher`] whose safe use is the caller's responsibility; see +/// [`bouncycastle_core::hazmat`] for what the path means and the supported uses. +pub mod hazmat { + use super::StreamCipher; + use bouncycastle_core::hazmat::KeyStream; + use bouncycastle_utils::secret::Secret; + use core::marker::PhantomData; + + /// A keystream that runs over no [`BlockPermutation`](bouncycastle_core::hazmat::BlockPermutation), + /// such as ChaCha20's, and so has no decrypt-only permutation for [`stream_cipher_over`] to + /// refuse. Implemented beside such a keystream's definition, and deliberately unsealed: + /// implementing it is the implementor's promise that no permutation that decrypts only is + /// under the keystream. The keystreams of this crate's modes do not implement it. + pub trait PermutationFreeKeyStream {} + + /// A [`StreamCipher`] over a keystream that is already constructed and positioned, for a + /// keystream defined outside this crate. + /// + /// # 🚨 Security Considerations 🚨 + /// The nonce and the starting counter are whatever the keystream was constructed with: no + /// init data is drawn, and a nonce or counter range repeated under the key is the two-time + /// pad. A key's `create_encryptor`, which draws the nonce, is the cipher to use. + pub fn stream_cipher_over( + keystream: KS, + ) -> StreamCipher + where + KS: KeyStream + PermutationFreeKeyStream, + { + StreamCipher::::check_shape(); + StreamCipher { keystream, pending: Secret::new(), used: BLOCK_LEN, _marker: PhantomData } + } +} diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index ec8319835..276bef2b5 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -65,8 +65,10 @@ fn each_mode_reports_its_own_name() { check::>(Toy::CMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); - check::>(Toy::KW_ALG_NAME); - check::>(Toy::KWP_ALG_NAME); + check::>(Toy::KW_ALG_NAME); + check::>(Toy::KW_ALG_NAME); + check::>(Toy::KWP_ALG_NAME); + check::>(Toy::KWP_ALG_NAME); } /// The padding layer and the stream-cipher wrapper add no name of their own. diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs index 893277551..2fa9e2395 100644 --- a/crypto/cipher/tests/modes/kw_tests.rs +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -11,6 +11,7 @@ mod common; mod toy_key; use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; +use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -227,7 +228,7 @@ fn kwp_costs_nothing_extra_for_aligned_data() { #[test] fn sizes() { use core::mem::size_of; - assert_eq!(size_of::>(), size_of::<&Toy>()); - assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::>(), size_of::<&Toy>()); assert_eq!(size_of::(), size_of::()); } diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 08c970728..2686bdb4a 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -41,6 +41,9 @@ pub enum KeyMaterialError { InvalidLength, /// SecurityStrength(&'static str), + /// The key is one the algorithm's specification says to avoid: a weak or semi-weak key, or a + /// key bundle whose components are not distinct (e.g. a TDEA bundle that collapses to single DES). + WeakKey(&'static str), } /// diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs index e64384772..2755ea21e 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -35,6 +35,25 @@ pub trait BlockPermutation: Algorithm + Sized { /// The inverse cipher function, in place. fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); + /// Whether this library allows the permutation to decrypt only: no encrypting worker may be + /// made over it, by a key's `create_encryptor` or by any other constructor of one, a key + /// wrapper included. A mode checks this in an inline `const` in every such constructor, so + /// the refusal is a compile error (reported after monomorphization, so by `cargo build` or + /// `cargo test` rather than `cargo check`). + /// + /// This records the library's policy, not a standard's status. It is `true` for two-key TDEA + /// alone, which NIST SP 800-131A Rev 2 Table 1 lists as "Disallowed" for encryption and + /// "Legacy use" for decryption, and whose key wrapping its Table 6 lists as "Disallowed" + /// too. Three-key TDEA encryption, which Table 1 disallows + /// after 2023, keeps `false` because the OpenSSL provider this library serves still exposes + /// it; the tdes crate's Security Considerations say so. + /// + /// The block methods themselves are not gated: a decrypting CFB or CTR needs the forward + /// cipher function, and a raw permutation is not something to encrypt data with in any case + /// (see the trait docs). Those decryptors are therefore an encryption capability in a + /// determined caller's hands, which the tdes crate's docs spell out. + const DECRYPT_ONLY: bool = false; + /// The forward cipher function on two *independent* blocks, in place. /// /// Required, with no default, so that every implementor decides for itself how to run a pair. diff --git a/crypto/tdes/Cargo.toml b/crypto/tdes/Cargo.toml new file mode 100644 index 000000000..888f667d6 --- /dev/null +++ b/crypto/tdes/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bouncycastle-tdes" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +# The modes the keys hand out, the `ModeNames` impls and the `TDES_*` / `TDES2_*` aliases; the +# engines themselves do not use it. +bouncycastle-cipher.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "tdes_benches" +harness = false diff --git a/crypto/tdes/benches/tdes_benches.rs b/crypto/tdes/benches/tdes_benches.rs new file mode 100644 index 000000000..e69339a91 --- /dev/null +++ b/crypto/tdes/benches/tdes_benches.rs @@ -0,0 +1,96 @@ +//! Criterion benchmarks for the constant-time TDESInternal engine. +//! +//! The permutation processes one block per call and its pair and four-block entry points are +//! single-block loops, so the figures to watch are the single-block throughput in each direction +//! and the cost of key expansion. The `encrypt_4blocks` case is included to confirm that the +//! four-block path costs the same as four single-block calls, i.e. that nothing is lost by a mode +//! preferring it. + +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use bouncycastle_tdes::hazmat::TDESInternal; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 2048 TDESInternal blocks. +const NUM_BLOCKS: usize = 2048; +const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; BLOCK_LEN]> { + let mut blocks = vec![[0u8; BLOCK_LEN]; NUM_BLOCKS]; + let mut generator = rng::DefaultRNG::default(); + for block in blocks.iter_mut() { + generator.next_bytes_out(block).unwrap(); + } + blocks +} + +/// A random key bundle. Three random 8-byte keys are distinct and non-weak with overwhelming +/// probability; the loop covers the remainder. +fn key() -> KeyMaterial { + let mut generator = rng::DefaultRNG::default(); + loop { + let mut bytes = [0u8; KEY_LEN]; + generator.next_bytes_out(&mut bytes).unwrap(); + let key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .unwrap(); + if TDESInternal::new(&key).is_ok() { + return key; + } + } +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("tdes::key expansion"); + let key = key(); + group.bench_function("TDES::new()", |b| { + b.iter(|| black_box(TDESInternal::new(black_box(&key)).unwrap())) + }); + group.finish(); +} + +fn bench_tdes(c: &mut Criterion) { + let tdes = TDESInternal::new(&key()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("tdes::TDES"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x2048", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + tdes.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x2048", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + tdes.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_4blocks() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for four in buf.as_chunks_mut::<4>().0 { + tdes.encrypt_4blocks(black_box(four)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_tdes); +criterion_main!(benches); diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs new file mode 100644 index 000000000..72d872385 --- /dev/null +++ b/crypto/tdes/src/cbc.rs @@ -0,0 +1,170 @@ +//! Type aliases for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), +//! with padding. The docs are on [`TDES_CBC`], which the crate root re-exports. + +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// TDES in CBC mode with a padding scheme: what a [`TDES3Key`](crate::TDES3Key) under +/// `CBC` hands out. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`] and `Pad` is [`PKCS7`] or [`NoPadding`]; the wrong +/// direction is a compile error, not a runtime check. The IV is drawn from the caller's RNG by +/// encryption and returned; it is never supplied. It is one block: 8 bytes. +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::PKCS7; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let key = TDES3Key::>::from_key_material(&material).expect("a valid key bundle"); +/// +/// // 5 bytes: PKCS#7 pads it to one block, so the padding does the work CBC cannot. +/// let (iv, ciphertext) = key.encrypt(&mut rng, b"hello").expect("encryption"); +/// assert_eq!(ciphertext.len(), 8); +/// +/// let plaintext = key.decrypt(&iv, &ciphertext).expect("decryption"); +/// assert_eq!(plaintext, b"hello"); +/// ``` +/// +/// With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +/// error at `do_encrypt_final` rather than something silently padded: +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::NoPadding; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let key = TDES3Key::>::from_key_material(&material).unwrap(); +/// +/// // A whole block is fine, and comes out the same length. +/// let mut out = [0u8; 8]; +/// let (written, _iv) = key.encrypt_out(&mut rng, &[0u8; 8], &mut out).expect("aligned"); +/// assert_eq!(written, 8); +/// +/// // Five bytes is not, and is refused rather than padded. +/// assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); +/// ``` +/// +/// The padding scheme is part of the key type, so the two schemes' workers are different types +/// and cannot be interchanged. A value built with one will not satisfy a binding annotated with +/// the other: +/// +/// ```compile_fail +/// use bouncycastle_tdes::{TDES_CBC, TDES3Key}; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let key = TDES3Key::>::from_key_material(&material).unwrap(); +/// +/// // Built from a NoPadding key, annotated as PKCS7: mismatched types. +/// let (enc, _iv) = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()).unwrap(); +/// let _mismatched: TDES_CBC = enc; +/// ``` +/// +/// See `bouncycastle_cipher::modes::cbc` for the construction. The aliases name the padded +/// workers, which accept input of any size; `NoPadding` accepts only whole blocks but goes through +/// the same adapter. The workers come from a key: [`TDES3Key>`](crate::TDES3Key) (or +/// `CBC`) hands them out through the [`SymmetricEncryptionKey`] / +/// [`SymmetricDecryptionKey`] pair, `TDES3Key>` hands out the unpadded `Cbc` itself +/// through the block pair, and [`TDES2Key`](crate::TDES2Key) hands out the decryptor alone. +/// +/// # Why the padding is part of the key +/// +/// CBC is defined only on whole blocks (SP 800-38A Sec 5.2), and the recommendation puts the +/// formatting of anything else outside its scope (Appendix A). So CBC on real data is always CBC +/// *plus a padding scheme*, and the scheme is not an implementation detail: it changes the +/// ciphertext, and both ends must agree on it. Naming it in the key's mode makes that choice +/// explicit at every use, and makes a mismatched pair a compile error rather than a decryption +/// that returns plausible-looking rubbish. +/// +/// The two schemes `bouncycastle_cipher::padding` provides are [`PKCS7`], which is what almost +/// everyone means by "padded CBC" (RFC 5652 s. 6.3; Java's `DESede/CBC/PKCS5Padding`), and +/// [`NoPadding`], which adds nothing and instead *rejects* a message that is not a whole number +/// of blocks -- useful for formats already defined on block boundaries, where silently padding +/// would be wrong. +#[allow(non_camel_case_types)] +pub type TDES_CBC<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor< + Cbc<'k, TDESInternal, Encrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc<'k, TDESInternal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, + >, +>; + +/// Two-key TDES in CBC mode with a padding scheme, **decryption only**: what a +/// [`TDES2Key`](crate::TDES2Key) under `CBC` hands out. +/// +/// `Pad` is [`PKCS7`] or [`NoPadding`]. There is no direction parameter: two-key TDEA is disallowed +/// for encryption (SP 800-131A Rev 2 Table 1), so this is the [`PaddedBlockCipherDecryptor`] alone, +/// and the key's `create_encryptor` does not compile; see [`TDES2Internal`]. The IV is one 8-byte +/// block, supplied by the caller as it was received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::NoPadding; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// +/// // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let material = KeyMaterial::<16>::from_bytes_as_type( +/// &[0xbc, 0xfe, 0x7f, 0x25, 0xf2, 0xb0, 0xa7, 0x2c, +/// 0x62, 0x76, 0xae, 0x1f, 0x38, 0x9d, 0x8a, 0xe9], +/// KeyType::SymmetricCipherKey, +/// ).expect("a 16-byte symmetric cipher key"); +/// let key = TDES2Key::>::from_key_material(&material) +/// .expect("a valid two-key bundle"); +/// let iv = [0xf2, 0x9d, 0x4b, 0xa5, 0x17, 0x2f, 0xd2, 0x97]; +/// let ciphertext = [0x1b, 0x38, 0x89, 0xc9, 0xdb, 0xe1, 0x0d, 0x03]; +/// +/// let plaintext = key.decrypt(&iv, &ciphertext).expect("decryption"); +/// assert_eq!(plaintext, [0x06, 0x11, 0xa4, 0x74, 0xfb, 0x09, 0x09, 0x78]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CBC<'k, Pad> = PaddedBlockCipherDecryptor< + Cbc<'k, TDES2Internal, Decrypting, BLOCK_LEN>, + Pad, + BLOCK_LEN, + BLOCK_LEN, +>; diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs new file mode 100644 index 000000000..8eed3cd87 --- /dev/null +++ b/crypto/tdes/src/cfb.rs @@ -0,0 +1,93 @@ +//! Type aliases for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. The docs are on +//! [`TDES_CFB`], which the crate root re-exports. + +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Cfb; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// TDES in CFB64 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::CFB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let key = TDES3Key::::from_key_material(&material).expect("a valid key bundle"); +/// +/// // 47 bytes: a stream cipher does not need a whole number of blocks. +/// let message = [0u8; 47]; +/// let mut data = message; +/// let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +/// assert_ne!(data, message); +/// key.decrypt_inplace(&iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, at any byte boundary: +/// let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); +/// let mut first = [0u8; 5]; +/// let mut rest = [1u8; 30]; +/// enc.do_encrypt_inplace(&mut first).unwrap(); +/// enc.do_encrypt_inplace(&mut rest).unwrap(); +/// let mut dec = key.create_decryptor(&iv); +/// dec.do_decrypt_inplace(&mut first).unwrap(); +/// dec.do_decrypt_inplace(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 5]); +/// assert_eq!(rest, [1u8; 30]); +/// ``` +/// +/// See `bouncycastle_cipher::modes::cfb` for the construction. The aliases are stream ciphers: +/// the data is a `&mut [u8]` of any length, encrypted or decrypted in place. They come from a key, +/// [`TDES3Key`](crate::TDES3Key), through the [`SymmetricEncryptionKey`] / +/// [`SymmetricDecryptionKey`] pair; the IV is one block (8 bytes), drawn from the caller's RNG by +/// encryption and returned, and there is no API for supplying one. +/// +/// The segment size is the full block, so this is **CFB64** -- the `s = 64` case of the TCFB mode +/// that SP 800-38A Appendix E says is "equivalent to the CFB mode in this recommendation, with the +/// TDEA as the underlying block cipher". SP 800-38A's `s = 8` variant is a different, +/// non-interoperable mode with its own alias, [`TDES_CFB8`](crate::TDES_CFB8); `s = 1` is not +/// implemented. See the `bouncycastle_cipher::modes::Cfb` docs. +#[allow(non_camel_case_types)] +pub type TDES_CFB<'k, Dir> = Cfb<'k, TDESInternal, Dir, BLOCK_LEN>; + +/// Two-key TDES in CFB64 mode, **decryption only** (SP 800-131A Rev 2 Table 1). No direction +/// parameter: this is the `Decrypting` direction, and the key's `create_encryptor` does not +/// compile; see [`TDES2Internal`]. The IV is one 8-byte block, supplied by the caller as received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CFB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// +/// // NIST CAVP TCFB64MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let material = KeyMaterial::<16>::from_bytes_as_type( +/// &[0x9e, 0xe0, 0xb5, 0x9b, 0x25, 0x86, 0x51, 0x54, +/// 0x58, 0x85, 0x51, 0x34, 0x1c, 0x4f, 0xef, 0x9e], +/// KeyType::SymmetricCipherKey, +/// ).expect("a 16-byte symmetric cipher key"); +/// let key = TDES2Key::::from_key_material(&material).expect("a valid two-key bundle"); +/// let iv = [0x6e, 0x37, 0xd1, 0x97, 0x37, 0x6d, 0xb5, 0x95]; +/// let mut data = [0x63, 0xca, 0xd5, 0x22, 0x60, 0xe0, 0xa1, 0xcd]; +/// +/// key.decrypt_inplace(&iv, &mut data).expect("decryption"); +/// assert_eq!(data, [0xdc, 0xd3, 0xcf, 0x97, 0x46, 0xd6, 0xe4, 0x2b]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CFB<'k> = Cfb<'k, TDES2Internal, Decrypting, BLOCK_LEN>; diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs new file mode 100644 index 000000000..06a897ded --- /dev/null +++ b/crypto/tdes/src/cfb8.rs @@ -0,0 +1,86 @@ +//! Type aliases for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). The docs are on +//! [`TDES_CFB8`], which the crate root re-exports. + +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Cfb8; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// TDES in CFB8 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::{CFB, CFB8}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let cfb8_key = TDES3Key::::from_key_material(&material).expect("a valid key bundle"); +/// +/// // 5 bytes: CFB8's segment is one byte, so any length at all is fine. +/// let message = *b"hello"; +/// let mut data = message; +/// let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); +/// assert_ne!(data, message); +/// cfb8_key.decrypt_inplace(&iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // CFB8 and CFB64 are not interchangeable: same key bundle, same IV, different ciphertext. +/// let cfb64_key = TDES3Key::::from_key_material(&material).unwrap(); +/// let mut as_cfb8 = message; +/// let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut as_cfb8).unwrap(); +/// let mut as_cfb64 = as_cfb8; +/// cfb64_key.decrypt_inplace(&iv, &mut as_cfb64).unwrap(); +/// assert_ne!(as_cfb64, message); +/// ``` +/// +/// See `bouncycastle_cipher::modes::cfb8` for the construction. The aliases are stream ciphers +/// with a one-byte segment, and come from a key, [`TDES3Key`](crate::TDES3Key), through the +/// [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair; the IV is one block (8 bytes), +/// drawn from the caller's RNG by encryption and returned. +/// +/// CFB8 is a **different, non-interoperable mode** from CFB64, not a variant of it: their +/// ciphertexts differ from the second byte, and it costs a full TDES call per byte, eight times +/// the work of [`TDES_CFB`](crate::TDES_CFB). It is the `s = 8` case of the TCFB mode of SP 800-38A +/// Appendix E. See the `bouncycastle_cipher::modes::Cfb8` docs for when that is the right trade. +#[allow(non_camel_case_types)] +pub type TDES_CFB8<'k, Dir> = Cfb8<'k, TDESInternal, Dir, BLOCK_LEN>; + +/// Two-key TDES in CFB8 mode, **decryption only** (SP 800-131A Rev 2 Table 1). No direction +/// parameter: this is the `Decrypting` direction, and the key's `create_encryptor` does not +/// compile; see [`TDES2Internal`]. The IV is one 8-byte block, supplied by the caller as received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CFB8; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// +/// // NIST CAVP TCFB8MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let material = KeyMaterial::<16>::from_bytes_as_type( +/// &[0xfb, 0x97, 0x8a, 0x0b, 0x6d, 0xc2, 0xc4, 0x67, +/// 0xe3, 0xcb, 0x52, 0x32, 0x9d, 0xe9, 0x51, 0x61], +/// KeyType::SymmetricCipherKey, +/// ).expect("a 16-byte symmetric cipher key"); +/// let key = TDES2Key::::from_key_material(&material).expect("a valid two-key bundle"); +/// let iv = [0x8b, 0x97, 0x57, 0x9e, 0xa5, 0xac, 0x30, 0x0f]; +/// let mut data = [0x05]; +/// +/// key.decrypt_inplace(&iv, &mut data).expect("decryption"); +/// assert_eq!(data, [0x80]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CFB8<'k> = Cfb8<'k, TDES2Internal, Decrypting, BLOCK_LEN>; diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs new file mode 100644 index 000000000..cd5e4c85e --- /dev/null +++ b/crypto/tdes/src/ctr.rs @@ -0,0 +1,129 @@ +//! Type aliases for TDES in CTR mode (NIST SP 800-38A Sec 6.5). The docs are on [`TDES_CTR`], which +//! the crate root re-exports. + +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Ctr; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// The nonce length the [`TDES_CTR`] alias uses, leaving a 2-byte counter. See the module docs. +pub const CTR_NONCE_LEN: usize = 6; + +/// TDES in CTR mode with a 6-byte nonce. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime +/// check. +/// +/// A message longer than 65 536 blocks (512 KiB) is an error, not a repeated counter; see the +/// module docs. +/// +/// ``` +/// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES3Key}; +/// use bouncycastle_cipher::modes::CTR; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let key = TDES3Key::>::from_key_material(&material) +/// .expect("a valid key bundle"); +/// +/// // 47 bytes: a stream cipher does not need a whole number of blocks. +/// let message = [0u8; 47]; +/// let mut data = message; +/// let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); +/// assert_eq!(nonce.len(), 6); +/// assert_ne!(data, message); +/// key.decrypt_inplace(&nonce, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, at any byte boundary: +/// let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); +/// let mut first = [0u8; 5]; +/// let mut rest = [1u8; 30]; +/// enc.do_encrypt_inplace(&mut first).unwrap(); +/// enc.do_encrypt_inplace(&mut rest).unwrap(); +/// let mut dec = key.create_decryptor(&nonce); +/// dec.do_decrypt_inplace(&mut first).unwrap(); +/// dec.do_decrypt_inplace(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 5]); +/// assert_eq!(rest, [1u8; 30]); +/// ``` +/// +/// See `bouncycastle_cipher::modes::ctr` for the construction. The aliases are stream ciphers: +/// the data is a `&mut [u8]` of any length, encrypted or decrypted in place since the ciphertext +/// is exactly as long as the plaintext. They come from a key, +/// [`TDES3Key>`](crate::TDES3Key), through the +/// [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair; the nonce is drawn from the +/// caller's RNG by encryption and returned, and there is no API for supplying one. `CTR`'s +/// defaults are AES's 12-byte nonce on a 16-byte block, so both parameters must be spelled out +/// for TDEA. +/// +/// SP 800-38A Appendix E: "the Triple DES algorithm, i.e., TDEA, as described above, may be used +/// as the underlying block cipher algorithm for the six modes in this recommendation. One of the +/// resulting modes of Triple DES is new, i.e., not specified in ANSI X9.52: the CTR mode of the +/// TDEA." +/// +/// # The nonce is 6 bytes, so the counter is 2 +/// +/// `Ctr` splits the 8-byte counter block into a nonce and a counter by the length of its init +/// data. A 64-bit block leaves very little room, and the split is a trade between two limits: +/// +/// * The counter bounds one message. Two bytes is 65 536 blocks, 512 KiB, after which the mode +/// errors rather than repeat a counter block. +/// * The nonce is random (Appendix B.2), so two messages under one key bundle collide with +/// probability about `n^2 / 2^(8 * nonce_len + 1)` after `n` messages. A collision repeats the +/// keystream, which is fatal for CTR. +/// +/// SP 800-67r2 Sec 3.4 caps a key bundle at `2^20` blocks in total, so at most `2^20` messages can +/// ever be encrypted under one bundle, and 512 KiB per message is one sixteenth of the bundle's +/// whole budget. Against that, the collision probability over a bundle's lifetime is at most about +/// `2^-9` with a 6-byte nonce, `2^-1` with 5 bytes, and a certainty with 4. Six is the widest nonce +/// whose counter can still reach a useful message size, and it is what [`CTR_NONCE_LEN`] chooses. +/// Name `CTR` with another `N` as the key's mode for a different split. +#[allow(non_camel_case_types)] +pub type TDES_CTR<'k, Dir> = Ctr<'k, TDESInternal, Dir, BLOCK_LEN, CTR_NONCE_LEN>; + +/// Two-key TDES in CTR mode with a 6-byte nonce, **decryption only** (SP 800-131A Rev 2 Table 1). +/// No direction parameter: this is the `Decrypting` direction, and the key's `create_encryptor` +/// does not compile; see [`TDES2Internal`]. The nonce is supplied by the caller as received, and +/// the counter block is `nonce || counter` with the counter starting at zero, as for [`TDES_CTR`]. +/// +/// There is no NIST vector for TDEA in CTR mode, so this example checks the mode's definition +/// instead: the first keystream block is the forward cipher function of `nonce || 0x0000`. +/// +/// ``` +/// use bouncycastle_tdes::hazmat::TDES2Internal; +/// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES2Key}; +/// use bouncycastle_cipher::modes::CTR; +/// use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let nonce = [1, 2, 3, 4, 5, 6]; +/// +/// // What a legacy sender would have produced: the first counter block, through the permutation. +/// let mut keystream = [1, 2, 3, 4, 5, 6, 0, 0]; +/// TDES2Internal::new(&material).unwrap().encrypt_block(&mut keystream); +/// let plaintext = *b"8 bytes!"; +/// let mut data: [u8; 8] = core::array::from_fn(|i| plaintext[i] ^ keystream[i]); +/// +/// let key = TDES2Key::>::from_key_material(&material).unwrap(); +/// key.decrypt_inplace(&nonce, &mut data).expect("decryption"); +/// assert_eq!(data, plaintext); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CTR<'k> = Ctr<'k, TDES2Internal, Decrypting, BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/tdes/src/des.rs b/crypto/tdes/src/des.rs new file mode 100644 index 000000000..4f8589132 --- /dev/null +++ b/crypto/tdes/src/des.rs @@ -0,0 +1,404 @@ +//! The DEA cryptographic engine (SP 800-67r2 Sec 2): the initial and final permutations, the +//! function `f`, and the sixteen iterations. +//! +//! # Bit numbering +//! +//! The spec numbers the bits of a block from 1 at the left (footnote 10). A 64-bit block is held as +//! two `u32` words `L` and `R`, `L` being the first four bytes big-endian, so spec bit `k` of `L` +//! (1..32) is word bit `32 - k`, and spec bit `k` of the block (1..64) is bit `32 - k` of `L` for +//! `k <= 32` and bit `64 - k` of `R` otherwise. The same convention is used for the 32-bit input +//! and output of `f` and `P`. All constants below are written against this layout and the tests +//! check each one against the spec's table with every unit vector, which for a bit permutation is a +//! proof. +//! +//! # The six input planes +//! +//! `E` (Table 1) maps the 32 bits of `R` to eight 6-bit blocks `B1..B8`: block `i` is bits `4i-4, +//! 4i-3, 4i-2, 4i-1, 4i, 4i+1` of `R` (indices taken modulo 32 in 1..32). The middle four are +//! exactly the `i`-th nibble of `R`; the first is the last bit of the nibble before it and the last +//! is the first bit of the nibble after it. So `E` never needs to be materialised as a 48-bit +//! value: [`planes`] builds six words, one per input-bit position, in which nibble `i` holds bit +//! `j` of `Bi` replicated four times. That is precisely the input layout +//! [`crate::sbox::sbox_layer`] wants, and it is obtained with masks, shifts and two rotations by a +//! whole nibble. +//! +//! The round key `Kn` is XORed in the same layout (eq (6), `B1..B8 = K xor E(R)`). The key schedule +//! stores each `Kn` as two words shaped to make that cheap; see [`crate::schedule`]. +//! +//! # Provenance +//! +//! The `IP` / `IP^-1` networks are the well-known five-swap form by Richard Outerbridge, taken via +//! Crypto++'s `des.cpp` (public domain) as reproduced in BearSSL's `des_support.c`; the rotation +//! groups of `P` were regenerated from Table 3 and agree with BearSSL's `des_ct.c`. Both are pinned +//! to the spec's tables by the unit-vector tests below. + +use crate::sbox::sbox_layer; +use bouncycastle_utils::ct; + +/// One expanded DEA key: sixteen round keys `K1..K16`, two words each -- `[lo, hi]` for round `n` +/// at `[2n, 2n + 1]`. See [`crate::schedule`] for the packing. +pub(crate) type Subkeys = [u32; 32]; + +/// Every nibble's least significant bit. +const NIBBLE_LSB: u32 = 0x1111_1111; + +/// Targets whose integer multiply is fixed-latency, so [`spread`] may be left for LLVM to fold +/// into one. A `const` rather than a `cfg` on the body, so both arms compile on every target. +const MULTIPLY_IS_FIXED_LATENCY: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); + +/// Spreads the least significant bit of each nibble to all four bits of that nibble. +/// +/// The input must have no other bits set. Written as shifts rather than a multiply by 15, but +/// LLVM sees that the four shifted copies OR disjoint bits and folds them back into `v * 15` +/// wherever it may: two `lea`s on x86-64, where that is harmless, and a `muls` on Cortex-M0, +/// whose timing depends on the operand. So off the two fixed-latency targets `v` first goes +/// through [`ct::value_barrier`], which hides its known-zero bits, and the ORs stay ORs; this is +/// the same guard as `bouncycastle-aes`'s round-key widening, and +/// `dev_scripts/check_aes_ct_asm.sh` greps the round function's assembly for it on Cortex-M0 +/// and RISC-V 32I. +#[inline(always)] +fn spread(v: u32) -> u32 { + let v = if MULTIPLY_IS_FIXED_LATENCY { v } else { ct::value_barrier(v) }; + v | (v << 1) | (v << 2) | (v << 3) +} + +/// The initial permutation `IP` (Sec 2.1). +/// +/// The `IP` table has a structure that a lookup hides: output row `j` (its `j`-th byte) takes one +/// fixed bit position from each input byte, input bytes in the order 8, 7, ..., 1, with rows 1-4 +/// taking positions 2, 4, 6, 8 and rows 5-8 taking 1, 3, 5, 7. That is a transposition of the +/// block as an 8x8 bit matrix, with its rows and columns reordered, and a transposition is what +/// this network of five conditional swaps performs. The test checks all 64 unit vectors against +/// the table. +#[inline(always)] +pub(crate) fn ip(l: u32, r: u32) -> (u32, u32) { + let (mut l, mut r) = (l, r); + let mut t; + t = ((l >> 4) ^ r) & 0x0F0F_0F0F; + r ^= t; + l ^= t << 4; + t = ((l >> 16) ^ r) & 0x0000_FFFF; + r ^= t; + l ^= t << 16; + t = ((r >> 2) ^ l) & 0x3333_3333; + l ^= t; + r ^= t << 2; + t = ((r >> 8) ^ l) & 0x00FF_00FF; + l ^= t; + r ^= t << 8; + t = ((l >> 1) ^ r) & 0x5555_5555; + r ^= t; + l ^= t << 1; + (l, r) +} + +/// The final permutation `IP^-1` (Sec 2.1): the swaps of [`ip`] in reverse order. +#[inline(always)] +pub(crate) fn inverse_ip(l: u32, r: u32) -> (u32, u32) { + let (mut l, mut r) = (l, r); + let mut t; + t = ((l >> 1) ^ r) & 0x5555_5555; + r ^= t; + l ^= t << 1; + t = ((r >> 8) ^ l) & 0x00FF_00FF; + l ^= t; + r ^= t << 8; + t = ((r >> 2) ^ l) & 0x3333_3333; + l ^= t; + r ^= t << 2; + t = ((l >> 16) ^ r) & 0x0000_FFFF; + r ^= t; + l ^= t << 16; + t = ((l >> 4) ^ r) & 0x0F0F_0F0F; + r ^= t; + l ^= t << 4; + (l, r) +} + +/// `K xor E(R)` (Table 1 and eq (6)) as six input planes for the S-box layer. +/// +/// `lo` and `hi` are the two words of the round key: nibble `i` of `lo` holds bits 2-5 of `Bi`'s +/// key block in the same positions as `R`'s nibble holds bits 2-5 of `E(R)`, so for those four +/// planes the XOR is done once on the words before spreading. Bits 1 and 6 come from the +/// neighbouring nibbles of `R` -- the nibble above (a rotate right by 4 brings it down) and the +/// nibble below (rotate left by 4) -- and from bits 0 and 1 of `hi`'s nibble. +#[inline(always)] +pub(crate) fn planes(r: u32, lo: u32, hi: u32) -> [u32; 6] { + let rk = r ^ lo; + [ + // b1: bit 4i-4 of R, the last bit of the previous nibble (bit 32 for i = 1). + spread((r.rotate_right(4) & NIBBLE_LSB) ^ (hi & NIBBLE_LSB)), + // b2..b5: bits 4i-3 .. 4i of R, the nibble itself, most significant first. + spread((rk >> 3) & NIBBLE_LSB), + spread((rk >> 2) & NIBBLE_LSB), + spread((rk >> 1) & NIBBLE_LSB), + spread(rk & NIBBLE_LSB), + // b6: bit 4i+1 of R, the first bit of the next nibble (bit 1 for i = 8). + spread(((r.rotate_left(4) >> 3) & NIBBLE_LSB) ^ ((hi >> 1) & NIBBLE_LSB)), + ] +} + +/// The permutation `P` (Sec 2.3, Table 3). +/// +/// Output bit `k` is input bit `P[k]`, which in the word layout is a rotate left by `P[k] - k` +/// (mod 32). The 32 moves fall into nineteen distinct distances, so each line masks the input +/// bits that share a distance and rotates them together. The comment on each line lists those +/// input bits in the spec's numbering. Generated from Table 3 and checked against it by the tests. +#[inline(always)] +fn permute_p(y: u32) -> u32 { + (y & 0x0000_0004).rotate_left(3) // 30 + | (y & 0x0000_4000).rotate_left(4) // 18 + | (y & 0x1202_0120).rotate_left(5) // 4, 7, 15, 24, 27 + | (y & 0x0010_0000).rotate_left(6) // 12 + | (y & 0x0000_8000).rotate_left(9) // 17 + | (y & 0x0400_0000).rotate_left(10) // 6 + | (y & 0x0000_0001).rotate_left(11) // 32 + | (y & 0x2000_0200).rotate_left(12) // 3, 23 + | (y & 0x0020_0000).rotate_left(13) // 11 + | (y & 0x0000_0040).rotate_left(14) // 26 + | (y & 0x0001_0000).rotate_left(15) // 16 + | (y & 0x0000_0002).rotate_left(16) // 31 + | (y & 0x4080_1800).rotate_left(17) // 2, 9, 20, 21 + | (y & 0x0008_0000).rotate_left(19) // 13 + | (y & 0x0000_0010).rotate_left(21) // 28 + | (y & 0x0100_0000).rotate_left(22) // 8 + | (y & 0x8800_0008).rotate_left(24) // 1, 5, 29 + | (y & 0x0000_0480).rotate_left(25) // 22, 25 + | (y & 0x0044_2000).rotate_left(26) // 10, 14, 19 +} + +/// The function `f(R, K)` (Sec 2.3): `P(S1(B1) S2(B2) ... S8(B8))` with `B1..B8 = K xor E(R)`, +/// eqs (6) and (7). +#[inline(always)] +fn f(r: u32, lo: u32, hi: u32) -> u32 { + permute_p(sbox_layer(&planes(r, lo, hi))) +} + +/// The sixteen iterations of the DEA on an already-permuted block, returning the pre-output block. +/// +/// Forward (Sec 2.1, eq (3)): `Ln = Rn-1`, `Rn = Ln-1 xor f(Rn-1, Kn)` for `n = 1..16`, and the +/// pre-output is `R16 L16` -- the final interchange, which is the `(r, l)` returned. +/// +/// Inverse (Sec 2.2, eq (5)): "the very same algorithm ... taking care that at each iteration of +/// the computation, the same block of key bits K is used ... K16 is used in the first iteration, +/// K15 in the second, and so on, with K1 used in the 16th iteration." So `inverse` only reverses +/// the order in which the stored round keys are read; there is no second schedule. The direction +/// is not a secret, so selecting the round key with it is fine. +/// +/// `IP` and `IP^-1` are the caller's business: TDEA (Sec 3.1) chains three of these, and the +/// `IP^-1` that ends one DEA transformation cancels the `IP` that begins the next, so +/// [`crate::hazmat::tdes::forward`] (and `inverse`) applies each permutation once around all +/// forty-eight rounds. Every +/// intermediate value is identical to the spec's; only two redundant permutations are skipped. +#[inline(always)] +pub(crate) fn rounds(l: u32, r: u32, sk: &Subkeys, inverse: bool) -> (u32, u32) { + let (mut l, mut r) = (l, r); + for n in 0..16 { + let round = if inverse { 15 - n } else { n }; + let t = l ^ f(r, sk[2 * round], sk[2 * round + 1]); + l = r; + r = t; + } + (r, l) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `IP` as printed in Sec 2.1: entry `k` (0-based) is the input bit that becomes output bit + /// `k + 1`. + #[rustfmt::skip] + const IP_TABLE: [u8; 64] = [ + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + ]; + + /// `IP^-1` as printed in Sec 2.1. + #[rustfmt::skip] + const INVERSE_IP_TABLE: [u8; 64] = [ + 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + ]; + + /// `E`, Table 1: entry `k` is the bit of `R` that becomes bit `k + 1` of the 48-bit `E(R)`. + #[rustfmt::skip] + const E_TABLE: [u8; 48] = [ + 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1, + ]; + + /// `P`, Table 3. + #[rustfmt::skip] + const P_TABLE: [u8; 32] = [ + 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25, + ]; + + /// Applies a table-defined bit permutation to a 64-bit block, spec numbering (bit 1 = MSB). + fn permute64(table: &[u8; 64], input: u64) -> u64 { + let mut out = 0u64; + for (k, &src) in table.iter().enumerate() { + let bit = (input >> (64 - src as u32)) & 1; + out |= bit << (63 - k); + } + out + } + + fn to_words(x: u64) -> (u32, u32) { + ((x >> 32) as u32, x as u32) + } + + fn from_words(l: u32, r: u32) -> u64 { + ((l as u64) << 32) | r as u64 + } + + #[test] + fn test_ip_matches_the_table_on_every_unit_vector() { + // A bit permutation is linear, so agreement on the 64 unit vectors is agreement everywhere. + for k in 0..64 { + let input = 1u64 << k; + let (l, r) = to_words(input); + let (l, r) = ip(l, r); + assert_eq!(from_words(l, r), permute64(&IP_TABLE, input), "IP, input bit {}", 64 - k); + } + } + + #[test] + fn test_inverse_ip_matches_the_table_on_every_unit_vector() { + for k in 0..64 { + let input = 1u64 << k; + let (l, r) = to_words(input); + let (l, r) = inverse_ip(l, r); + assert_eq!( + from_words(l, r), + permute64(&INVERSE_IP_TABLE, input), + "IP^-1, bit {}", + 64 - k + ); + } + } + + #[test] + fn test_the_two_tables_are_inverses() { + // Not a test of this code, but of the transcription: Sec 2.1 says IP^-1 inverts IP. + for k in 0..64 { + let input = 1u64 << k; + assert_eq!(permute64(&INVERSE_IP_TABLE, permute64(&IP_TABLE, input)), input); + } + } + + #[test] + fn test_p_matches_table_3_on_every_unit_vector() { + for k in 0..32 { + let input = 1u32 << k; + let mut expected = 0u32; + for (o, &src) in P_TABLE.iter().enumerate() { + let bit = (input >> (32 - src as u32)) & 1; + expected |= bit << (31 - o); + } + assert_eq!(permute_p(input), expected, "P, input bit {}", 32 - k); + } + } + + /// `E(R)` per Table 1, as the six planes the S-box layer expects: plane `j` holds bit `j + 1` + /// of `Bi` in all four bits of nibble `i`. + fn e_planes_reference(r: u32) -> [u32; 6] { + let mut x = [0u32; 6]; + for (k, &src) in E_TABLE.iter().enumerate() { + let bit = (r >> (32 - src as u32)) & 1; + let (i, j) = (k / 6, k % 6); // block i (0-based), input bit j + x[j] |= spread(bit << (28 - 4 * i)); + } + x + } + + /// The compact round-key words unpacked to the same plane layout: `lo`'s nibble `i` holds key + /// block bits 2-5 (bit 2 at the top), `hi`'s nibble holds bit 1 at bit 0 and bit 6 at bit 1. + fn key_planes_reference(lo: u32, hi: u32) -> [u32; 6] { + let mut x = [0u32; 6]; + for i in 0..8 { + let shift = 28 - 4 * i; + let nib_lo = (lo >> shift) & 0xF; + let nib_hi = (hi >> shift) & 0xF; + let bits = [ + nib_hi & 1, + nib_lo >> 3, + (nib_lo >> 2) & 1, + (nib_lo >> 1) & 1, + nib_lo & 1, + (nib_hi >> 1) & 1, + ]; + for j in 0..6 { + x[j] |= spread(bits[j] << shift); + } + } + x + } + + #[test] + fn test_planes_match_table_1_on_every_unit_vector() { + for k in 0..32 { + let r = 1u32 << k; + assert_eq!(planes(r, 0, 0), e_planes_reference(r), "E, input bit {}", 32 - k); + } + } + + #[test] + fn test_planes_xor_the_round_key_in_the_documented_layout() { + let mut seed = 0x9E37_79B9u32; + let mut next = || { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + seed + }; + for _ in 0..500 { + let r = next(); + let lo = next(); + let hi = next() & 0x3333_3333; // only bits 0 and 1 of each nibble are used + let e = e_planes_reference(r); + let kp = key_planes_reference(lo, hi); + let expected: [u32; 6] = core::array::from_fn(|j| e[j] ^ kp[j]); + assert_eq!(planes(r, lo, hi), expected); + } + } + + #[test] + fn test_inverse_rounds_undo_forward_rounds() { + // With arbitrary round keys the sixteen iterations are still a permutation, and reading the + // keys backwards inverts it (Sec 2.2). This checks the Feistel plumbing independently of + // the key schedule. + let mut sk = [0u32; 32]; + for (i, w) in sk.iter_mut().enumerate() { + *w = (i as u32).wrapping_mul(0x9E37_79B9) ^ 0x5A5A_1234; + } + for (l, r) in [(0u32, 0u32), (u32::MAX, 0), (0x0123_4567, 0x89AB_CDEF), (1, 1 << 31)] { + let (a, b) = rounds(l, r, &sk, false); + assert_ne!((a, b), (l, r)); + assert_eq!(rounds(a, b, &sk, true), (l, r)); + } + } +} diff --git a/crypto/tdes/src/hazmat/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs new file mode 100644 index 000000000..ec89ee567 --- /dev/null +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -0,0 +1,131 @@ +//! Type aliases for TDES in ECB mode (NIST SP 800-38A Sec 6.1; "TECB" in SP 800-38A Appendix E), +//! with padding. +//! +//! See `bouncycastle_cipher::modes::hazmat::ecb` for the construction. The aliases name the +//! padded workers, handed out by a [`TDES3Key`](crate::TDES3Key) under `ECB` through the +//! [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] pair; under `ECB` the key +//! hands out the unpadded `Ecb` through [`EcbEncryptionKey`] / [`EcbDecryptionKey`] instead, and +//! a [`TDES2Key`](crate::TDES2Key) hands out the decryptors alone. +//! +//! **ECB is not a confidentiality mode for data.** Under a given key every plaintext block maps to +//! the same ciphertext block (Sec 6.1), so the structure of the plaintext shows through, and blocks +//! can be reordered, repeated or removed undetectably. Padding does not change that in the least: +//! it makes ECB accept any length, not make it safe. These aliases exist for interoperability with +//! systems that use ECB and for driving test vectors. See the crate docs, "A block permutation is +//! not a cipher". +//! +//! ECB is defined only on whole blocks (SP 800-38A Sec 5.2), so ECB on data of any other length is +//! always ECB *plus a padding scheme*, and the scheme changes the ciphertext; it is part of the +//! key's mode for the reasons given at [`TDES_CBC`](crate::TDES_CBC). ECB has no IV, so +//! `INIT_DATA_LEN` is 0: encryption returns an empty array and decryption takes one, and the +//! ciphertext is exactly the padded plaintext with nothing prepended. + +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::hazmat::ecb::Ecb; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey}; +#[allow(unused_imports)] +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; +// end of imports needed for docs + +/// TDES in ECB mode with a padding scheme: what a [`TDES3Key`](crate::TDES3Key) under +/// `ECB` hands out. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`] and `Pad` is [`PKCS7`] or [`NoPadding`]; the wrong +/// direction is a compile error, not a runtime check. There is no IV: encryption returns an empty +/// array and decryption takes one. +/// +/// **Not confidential for data** -- see the module docs. Padding makes ECB accept any length; it +/// does not make it safe. +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::hazmat::ecb::ECB; +/// use bouncycastle_cipher::padding::PKCS7; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let mut rng = HashDRBG_SHA512::new_from_os(); +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let key = TDES3Key::>::from_key_material(&material).expect("a valid key bundle"); +/// +/// // 5 bytes: PKCS#7 pads it to one 8-byte block. The init data is empty, ECB having no IV. +/// let (no_iv, ciphertext) = key.encrypt(&mut rng, b"hello").expect("encryption"); +/// assert_eq!(no_iv, [0u8; 0]); +/// assert_eq!(ciphertext.len(), 8); +/// +/// let recovered = key.decrypt(&no_iv, &ciphertext).expect("decryption"); +/// assert_eq!(recovered, b"hello"); +/// ``` +/// +/// The codebook property survives padding, which is the whole objection to ECB: two identical +/// plaintext blocks still give two identical ciphertext blocks. +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::hazmat::ecb::ECB; +/// use bouncycastle_cipher::padding::NoPadding; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let key = TDES3Key::>::from_key_material(&material).unwrap(); +/// +/// // Two identical blocks in... +/// let (_, ciphertext) = key.encrypt(&mut HashDRBG_SHA512::new_from_os(), &[0x5Au8; 16]).unwrap(); +/// // ...two identical blocks out. No mode here chains, so nothing hides the repetition. +/// assert_eq!(ciphertext[..8], ciphertext[8..]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES_ECB<'k, Dir, Pad> = ::Select< + PaddedBlockCipherEncryptor, Pad, 0, BLOCK_LEN>, + PaddedBlockCipherDecryptor, Pad, 0, BLOCK_LEN>, +>; + +/// Two-key TDES in ECB mode with a padding scheme, **decryption only**: what a +/// [`TDES2Key`](crate::TDES2Key) under `ECB` hands out. +/// +/// `Pad` is [`PKCS7`] or [`NoPadding`]. There is no direction parameter: two-key TDEA is disallowed +/// for encryption (SP 800-131A Rev 2 Table 1), so this is the [`PaddedBlockCipherDecryptor`] alone; +/// see [`TDES2Internal`]. There is no IV, so decryption takes an empty array. The ECB warning in +/// the module docs applies unchanged. +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::hazmat::ecb::ECB; +/// use bouncycastle_cipher::padding::NoPadding; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// +/// // NIST CAVP TECBMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let material = KeyMaterial::<16>::from_bytes_as_type( +/// &[0x15, 0x1f, 0x10, 0x38, 0x3d, 0x6d, 0x19, 0x9b, +/// 0x4a, 0x76, 0x3b, 0xd5, 0x4a, 0x46, 0xa4, 0x45], +/// KeyType::SymmetricCipherKey, +/// ).expect("a 16-byte symmetric cipher key"); +/// let key = TDES2Key::>::from_key_material(&material) +/// .expect("a valid two-key bundle"); +/// +/// let ciphertext = [0x89, 0x32, 0x1b, 0xa7, 0x5b, 0xa5, 0x45, 0xdb]; +/// let plaintext = key.decrypt(&[], &ciphertext).expect("decryption"); +/// assert_eq!(plaintext, [0xd8, 0xda, 0x89, 0x29, 0x88, 0x78, 0xed, 0x7d]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_ECB<'k, Pad> = + PaddedBlockCipherDecryptor, Pad, 0, BLOCK_LEN>; diff --git a/crypto/tdes/src/hazmat/mod.rs b/crypto/tdes/src/hazmat/mod.rs new file mode 100644 index 000000000..f452f8488 --- /dev/null +++ b/crypto/tdes/src/hazmat/mod.rs @@ -0,0 +1,17 @@ +//! The raw TDEA permutations and the ECB aliases; see [`bouncycastle_core::hazmat`] for what the +//! path means and the supported uses. +//! +//! [`TDESInternal`] and the decryption-only [`TDES2Internal`] transform exactly one block and are +//! the engines under the keys at the crate root, not a cipher for data. [`TDES_ECB`] and +//! [`TDES2_ECB`] are that operation block by block with padding: equal plaintext blocks give equal +//! ciphertext blocks, so they are here for interoperability and test vectors. Use +//! [`TDES3Key`](crate::TDES3Key) under [`TDES_CBC`](crate::TDES_CBC) and the other modes at the +//! crate root. + +pub(crate) mod ecb; +pub(crate) mod tdes; +pub(crate) mod tdes2; + +pub use ecb::{TDES_ECB, TDES2_ECB}; +pub use tdes::TDESInternal; +pub use tdes2::TDES2Internal; diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs new file mode 100644 index 000000000..69a562ca7 --- /dev/null +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -0,0 +1,273 @@ +//! The TDEA forward and inverse cipher operations (SP 800-67r2 Sec 3.1), the key-bundle checks of +//! Sec 3.1 and Sec 3.3.2, and the three-key engine type. The two-key engine is in +//! [`super::tdes2`] and shares the checks defined here. + +use crate::des::{Subkeys, inverse_ip, ip, rounds}; +use crate::schedule::{PARITY_MASK, expand, is_weak}; +use crate::{BLOCK_LEN, KEY_LEN}; +use bouncycastle_cipher::modes::ModeNames; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::ct::ct_eq_zero_bytes; +use bouncycastle_utils::secret::Secret; + +pub(crate) type Block = [u8; BLOCK_LEN]; + +/// The three-key TDEA keyed permutation (SP 800-67r2 Sec 3.1), "3TDEA": the engine under +/// [`TDES3Key`](crate::TDES3Key). +/// +/// The only state is the three expanded DEA keys, held in a [`Secret`] so that they are zeroized +/// on drop and redacted from `Debug`. There is no direction flag: the inverse transformation reads +/// the same round keys backwards (Sec 2.2), so one value serves both directions, and a +/// constructed value is always ready to use, so there is no `init()` or `reset()`. +/// +/// Only bundles of three distinct keys are accepted; its [`ElectronicCodeBook::new`] says what is +/// rejected and why. For two-key TDEA, which is decryption-only, see +/// [`TDES2Internal`](super::TDES2Internal). See the crate docs for the standing of TDEA today. +#[derive(Clone)] +pub struct TDESInternal { + schedule: Secret<[Subkeys; 3]>, +} + +// `Secret`'s equality inspects every byte of the schedule, so the time taken does not say where two +// differ. The parity bits are not in the schedule (PC-1 drops them), so two bundles differing only +// in parity are equal, as they are to the engine. +impl PartialEq for TDESInternal { + fn eq(&self, other: &Self) -> bool { + self.schedule == other.schedule + } +} + +impl Eq for TDESInternal {} + +/// Checks the [`KeyMaterial`] wrapper before the key bytes are looked at. +/// +/// The key must be tagged [`KeyType::SymmetricCipherKey`], must be exactly `key_len` bytes, and +/// must carry a [`SecurityStrength`] of at least `min_strength` -- the engine's +/// [`Algorithm::MAX_SECURITY_STRENGTH`]. A key from a correctly-instantiated RNG or KDF will carry +/// more than that; the check catches material that arrived from somewhere it should not have. +pub(crate) fn validate_wrapper( + key: &dyn KeyMaterialTrait, + key_len: usize, + min_strength: SecurityStrength, +) -> Result<(), SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "TDES requires a key of type KeyType::SymmetricCipherKey.", + ) + .into()); + } + if key.key_len() != key_len { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < min_strength { + return Err(KeyMaterialError::SecurityStrength( + "The provided key has a lower security strength than this TDES engine provides.", + ) + .into()); + } + Ok(()) +} + +/// Whether two DEA keys are the same key to the engine: equal once the parity bits, which the +/// algorithm ignores (Appendix A), are cleared. Constant time. +pub(crate) fn same_key(a: u64, b: u64) -> bool { + ct_eq_zero_bytes(&((a ^ b) & PARITY_MASK).to_be_bytes()) +} + +/// Sec 3.1's requirement that the component keys of a bundle be distinct, as an error. +pub(crate) const REPEATED_KEY: KeyMaterialError = KeyMaterialError::WeakKey( + "TDES requires distinct component keys (SP 800-67r2 Sec 3.1); a bundle with a repeated key \ + collapses to single DES.", +); + +/// Sec 3.3.2: "weak, semi-weak and possibly weak keys ... should be avoided". Every component is +/// checked, so that timing does not say which one was weak. +pub(crate) fn any_weak(keys: &[u64]) -> Result<(), SymmetricCipherError> { + let mut weak = false; + for &k in keys { + weak |= is_weak(k); + } + if weak { + return Err(KeyMaterialError::WeakKey( + "TDES component key is a weak, semi-weak or possibly weak DES key (SP 800-67r2 Sec \ + 3.3.2).", + ) + .into()); + } + Ok(()) +} + +/// `F_Key3(I_Key2(F_Key1(d)))` on one block, with `IP` and `IP^-1` applied once around all +/// forty-eight iterations. Shared by the three-key and two-key engines. +/// +/// Each DEA transformation is `IP`, sixteen iterations, `IP^-1` (Sec 2.1). The `IP^-1` ending +/// one and the `IP` beginning the next cancel, so `IP` is applied once before the forty-eight +/// iterations and `IP^-1` once after; see [`rounds`]. +#[inline(always)] +pub(crate) fn forward(block: &mut Block, k1: &Subkeys, k2: &Subkeys, k3: &Subkeys) { + let (l, r) = split(block); + let (l, r) = ip(l, r); + let (l, r) = rounds(l, r, k1, false); // F_Key1 + let (l, r) = rounds(l, r, k2, true); // I_Key2 + let (l, r) = rounds(l, r, k3, false); // F_Key3 + join(block, inverse_ip(l, r)); +} + +/// `I_Key1(F_Key2(I_Key3(d)))` on one block. See [`forward`]. +#[inline(always)] +pub(crate) fn inverse(block: &mut Block, k1: &Subkeys, k2: &Subkeys, k3: &Subkeys) { + let (l, r) = split(block); + let (l, r) = ip(l, r); + let (l, r) = rounds(l, r, k3, true); // I_Key3 + let (l, r) = rounds(l, r, k2, false); // F_Key2 + let (l, r) = rounds(l, r, k1, true); // I_Key1 + join(block, inverse_ip(l, r)); +} + +/// The block as `L R`: the first four bytes and the last four, big-endian, so that spec bit 1 is +/// the most significant bit of `L`. +#[inline(always)] +fn split(block: &Block) -> (u32, u32) { + ( + u32::from_be_bytes([block[0], block[1], block[2], block[3]]), + u32::from_be_bytes([block[4], block[5], block[6], block[7]]), + ) +} + +#[inline(always)] +fn join(block: &mut Block, (l, r): (u32, u32)) { + block[..4].copy_from_slice(&l.to_be_bytes()); + block[4..].copy_from_slice(&r.to_be_bytes()); +} + +impl Algorithm for TDESInternal { + const ALG_NAME: &'static str = "TDES"; + /// SP 800-57 Part 1 Rev 5, Sec 5.6.1 Table 2: 3TDEA provides 112 bits of security strength. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_112bit; +} + +impl ElectronicCodeBook for TDESInternal { + /// Expands a 24-byte key bundle `Key1 || Key2 || Key3` into the three DEA key schedules. + /// + /// The parity bits (the last bit of each key byte, Appendix A) are ignored, as the algorithm + /// ignores them; a key with wrong parity is not rejected, and two keys differing only in + /// parity are the same key for every check below. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not [`KeyType::SymmetricCipherKey`]. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 24 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 112 bits, the + /// strength SP 800-57 Part 1 Rev 5, Sec 5.6.1 Table 2 assigns to 3TDEA. + /// * [`KeyMaterialError::WeakKey`] if the three component keys are not pairwise distinct. Sec + /// 3.1 defines three-key TDEA only for `Key1 != Key2`, `Key2 != Key3`, `Key3 != Key1`: + /// `Key1 = Key2` or `Key2 = Key3` makes two of the three DEA transformations cancel, leaving + /// single DES, and `Key1 = Key3` is two-key TDEA, which has its own decryption-only engine, + /// [`TDES2Internal`](super::TDES2Internal), taking the 16-byte `Key1 || Key2`. + /// * [`KeyMaterialError::WeakKey`] if any component is one of the 64 weak, semi-weak or + /// possibly weak DEA keys of Sec 3.3.2. + fn new(key: &KeyMaterial) -> Result { + validate_wrapper(key, KEY_LEN, Self::MAX_SECURITY_STRENGTH)?; + // `validate_wrapper` has established the length; this is a type change, not a check. + let bytes: &[u8; KEY_LEN] = + key.ref_to_bytes().try_into().map_err(|_| KeyMaterialError::InvalidLength)?; + let (keys, _) = bytes.as_chunks::<8>(); + let k: [u64; 3] = core::array::from_fn(|i| u64::from_be_bytes(keys[i])); + + // Sec 3.1: three distinct keys. Every comparison is evaluated so that timing does not say + // which pair matched. + if same_key(k[0], k[1]) | same_key(k[1], k[2]) | same_key(k[0], k[2]) { + return Err(REPEATED_KEY.into()); + } + any_weak(&k)?; + + let mut schedule = Secret::<[Subkeys; 3]>::new(); + for (i, sk) in schedule.iter_mut().enumerate() { + expand(k[i], sk); + } + Ok(Self { schedule }) + } +} + +impl BlockPermutation for TDESInternal { + /// The TDEA forward cipher operation, `O = F_Key3(I_Key2(F_Key1(d)))` (Sec 3.1), in place. + fn encrypt_block(&self, block: &mut Block) { + let [k1, k2, k3] = &*self.schedule; + forward(block, k1, k2, k3); + } + /// The TDEA inverse cipher operation, `O = I_Key1(F_Key2(I_Key3(d)))` (Sec 3.1), in place. + fn decrypt_block(&self, block: &mut Block) { + let [k1, k2, k3] = &*self.schedule; + inverse(block, k1, k2, k3); + } + // The engine has no natural unit larger than one block, so the pair and four-block methods + // are single-block loops. + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + self.encrypt_block(block) + } + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + self.decrypt_block(block) + } + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + self.encrypt_block(block) + } + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + self.decrypt_block(block) + } + } +} + +/// The cipher modes offered over TDEA (SP 800-38A Appendix E). No AEAD or MAC mode is, so those +/// names are empty. +impl ModeNames for TDESInternal { + const CBC_ALG_NAME: &'static str = "TDES_CBC"; + const CCM_ALG_NAME: &'static str = ""; + const CFB_ALG_NAME: &'static str = "TDES_CFB"; + const CFB8_ALG_NAME: &'static str = "TDES_CFB8"; + const CMAC_ALG_NAME: &'static str = ""; + const CTR_ALG_NAME: &'static str = "TDES_CTR"; + const ECB_ALG_NAME: &'static str = "TDES_ECB"; + const GCM_ALG_NAME: &'static str = ""; + const GMAC_ALG_NAME: &'static str = ""; + /// TKW, the TDEA key wrap of SP 800-38F Sec 7, is the key wrap of a 64-bit block. + const KW_ALG_NAME: &'static str = "TDES_TKW"; + const KWP_ALG_NAME: &'static str = ""; +} + +impl core::fmt::Debug for TDESInternal { + /// Prints the algorithm name only. The key schedule is secret and is never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(Self::ALG_NAME) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_and_join_round_trip_big_endian() { + let block: Block = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]; + let (l, r) = split(&block); + assert_eq!((l, r), (0x0123_4567, 0x89AB_CDEF)); + let mut out = [0u8; BLOCK_LEN]; + join(&mut out, (l, r)); + assert_eq!(out, block); + } + + #[test] + fn test_same_key_ignores_parity_only() { + assert!(same_key(0x0123_4567_89AB_CDEF, 0x0123_4567_89AB_CDEF ^ !PARITY_MASK)); + assert!(!same_key(0x0123_4567_89AB_CDEF, 0x0123_4567_89AB_CDEF ^ 2)); + } +} diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs new file mode 100644 index 000000000..dd26becdd --- /dev/null +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -0,0 +1,202 @@ +//! Two-key TDEA (SP 800-67r2 Sec 3.1, "2TDEA"), for decryption of legacy data only. + +use crate::BLOCK_LEN; +use crate::KEY_LEN_2KEY; +use crate::des::Subkeys; +use crate::hazmat::tdes::{ + Block, REPEATED_KEY, any_weak, forward, inverse, same_key, validate_wrapper, +}; +use crate::schedule::expand; +use bouncycastle_cipher::modes::ModeNames; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::secret::Secret; + +/// The two-key TDEA keyed permutation (SP 800-67r2 Sec 3.1, "2TDEA"): `Key3 = Key1`, so the bundle +/// is the 16 bytes `Key1 || Key2`. The engine under [`TDES2Key`](crate::TDES2Key). +/// +/// **Decryption only.** SP 800-67r2 Sec 3.1 allows two-key TDEA "for legacy use only, as defined +/// in SP 800-131A", and SP 800-131A Rev 2 Table 1 has it as "Disallowed" for encryption and +/// "Legacy use" for decryption. This type therefore sets +/// [`BlockPermutation::DECRYPT_ONLY`], and every mode in `bouncycastle_cipher::modes` refuses to +/// make an encrypting worker over it **at compile time**: a two-key key's `create_encryptor` +/// does not compile. (The error is reported after monomorphization, by `cargo build` +/// or `cargo test`, with its span in the mode's code; `cargo check` does not reach it.) +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::PKCS7; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let key = TDES2Key::>::from_key_material(&material).unwrap(); +/// +/// // Two-key TDEA may not apply protection: this does not compile. +/// let _ = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()); +/// ``` +/// +/// The stream modes check the same constant through their keystream, so the refusal holds for +/// CTR, CFB and CFB8 too: +/// +/// ```compile_fail +/// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES2Key}; +/// use bouncycastle_cipher::modes::CTR; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let key = TDES2Key::>::from_key_material(&material).unwrap(); +/// +/// // Two-key TDEA may not apply protection through CTR either: this does not compile. +/// let _ = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()); +/// ``` +/// +/// `create_decryptor` on every mode works, and this crate names those decryptors +/// [`TDES2_CBC`](crate::TDES2_CBC), [`TDES2_CFB`](crate::TDES2_CFB), +/// [`TDES2_CFB8`](crate::TDES2_CFB8), [`TDES2_CTR`](crate::TDES2_CTR) and +/// [`TDES2_ECB`](crate::TDES2_ECB). The raw +/// [`BlockPermutation::encrypt_block`] is still present, because CFB and CTR decryption are +/// built on the forward cipher function; a raw permutation is not something to encrypt data with +/// anyway (see the crate docs). For the same reason the gate stops accidental encryption through +/// the encrypting APIs, not deliberate use of the forward cipher: CTR decryption is CTR +/// encryption, and a CFB decryptor under a chosen IV gives `CIPH(IV)` for a zero block, so a +/// caller who wants two-key TDEA encryption can assemble it from those decryptors. A deployment +/// that must rule it out should expose only the CBC and ECB decryptors. +/// +/// # Security strength +/// +/// SP 800-57 Part 1 Rev 5, Sec 5.6.1 Table 2 rates 2TDEA at "<= 80" bits. That is below the lowest +/// level [`SecurityStrength`] models (112), so [`Algorithm::MAX_SECURITY_STRENGTH`] is +/// [`SecurityStrength::None`] and the key-strength check in `new` never rejects a key -- there is +/// no honest higher value to demand. +/// +/// The persistent state is the two expanded DEA keys, 256 bytes, in a [`Secret`]. +#[derive(Clone)] +pub struct TDES2Internal { + schedule: Secret<[Subkeys; 2]>, +} + +// `Secret`'s equality inspects every byte of the schedule, so the time taken does not say where two +// differ. Parity bits are not in the schedule, as for `TDESInternal`. +impl PartialEq for TDES2Internal { + fn eq(&self, other: &Self) -> bool { + self.schedule == other.schedule + } +} + +impl Eq for TDES2Internal {} + +impl Algorithm for TDES2Internal { + const ALG_NAME: &'static str = "TDES-2KEY"; + /// SP 800-57 Part 1 Rev 5, Sec 5.6.1 Table 2: 2TDEA provides at most 80 bits, below the lowest + /// modelled level. See the type docs. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +impl ElectronicCodeBook for TDES2Internal { + /// Expands a 16-byte key bundle `Key1 || Key2` into the two DEA key schedules. + /// + /// Parity bits are ignored, as for [`TDESInternal`](super::TDESInternal). + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not `KeyType::SymmetricCipherKey`. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::WeakKey`] if `Key1 = Key2` (with parity ignored), which collapses the + /// three DEA transformations to single DES -- Sec 3.1 requires `Key1 != Key2` for two-key + /// TDEA as well. + /// * [`KeyMaterialError::WeakKey`] if either component is one of the 64 weak, semi-weak or + /// possibly weak DEA keys of Sec 3.3.2. + fn new(key: &KeyMaterial) -> Result { + validate_wrapper(key, KEY_LEN_2KEY, Self::MAX_SECURITY_STRENGTH)?; + let bytes: &[u8; KEY_LEN_2KEY] = + key.ref_to_bytes().try_into().map_err(|_| KeyMaterialError::InvalidLength)?; + let (keys, _) = bytes.as_chunks::<8>(); + let k: [u64; 2] = core::array::from_fn(|i| u64::from_be_bytes(keys[i])); + + if same_key(k[0], k[1]) { + return Err(REPEATED_KEY.into()); + } + any_weak(&k)?; + + let mut schedule = Secret::<[Subkeys; 2]>::new(); + for (i, sk) in schedule.iter_mut().enumerate() { + expand(k[i], sk); + } + Ok(Self { schedule }) + } +} + +impl BlockPermutation for TDES2Internal { + /// Two-key TDEA decrypts only (SP 800-131A Rev 2 Table 1); see the type docs. + const DECRYPT_ONLY: bool = true; + + /// The forward cipher operation with `Key3 = Key1`: `O = F_Key1(I_Key2(F_Key1(d)))`. + /// + /// Present because CFB and CTR decryption need it, and because the trait requires it; the + /// encrypting modes will not compile over this type. See the type docs. + fn encrypt_block(&self, block: &mut Block) { + let [k1, k2] = &*self.schedule; + forward(block, k1, k2, k1); + } + /// The inverse cipher operation with `Key3 = Key1`: `O = I_Key1(F_Key2(I_Key1(d)))`. + fn decrypt_block(&self, block: &mut Block) { + let [k1, k2] = &*self.schedule; + inverse(block, k1, k2, k1); + } + // As for `TDESInternal`: no unit larger than one block, so the batch methods are single-block + // loops. + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + self.encrypt_block(block) + } + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + self.decrypt_block(block) + } + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + self.encrypt_block(block) + } + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + self.decrypt_block(block) + } + } +} + +/// The same modes as the three-key engine, under their own names. +impl ModeNames for TDES2Internal { + const CBC_ALG_NAME: &'static str = "TDES2_CBC"; + const CCM_ALG_NAME: &'static str = ""; + const CFB_ALG_NAME: &'static str = "TDES2_CFB"; + const CFB8_ALG_NAME: &'static str = "TDES2_CFB8"; + const CMAC_ALG_NAME: &'static str = ""; + const CTR_ALG_NAME: &'static str = "TDES2_CTR"; + const ECB_ALG_NAME: &'static str = "TDES2_ECB"; + const GCM_ALG_NAME: &'static str = ""; + const GMAC_ALG_NAME: &'static str = ""; + /// TKW-AD only: the engine decrypts only, so no wrapper is made over it. + const KW_ALG_NAME: &'static str = "TDES2_TKW"; + const KWP_ALG_NAME: &'static str = ""; +} + +impl core::fmt::Debug for TDES2Internal { + /// Prints the algorithm name only. The key schedule is secret and is never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(Self::ALG_NAME) + } +} diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs new file mode 100644 index 000000000..591af9be4 --- /dev/null +++ b/crypto/tdes/src/key.rs @@ -0,0 +1,277 @@ +//! The TDEA key objects: a key bundle checked and expanded once, under a mode, handing out that +//! mode's workers through the role traits. The docs are on [`TDESKey`], which the crate root +//! re-exports. + +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, + EcbEncryptionKey, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey, + SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::RNG; + +mod sealed { + pub trait Sealed {} +} + +/// The engines a [`TDESKey`] can be built over: [`TDESInternal`] and [`TDES2Internal`]. Sealed, +/// since the key's constructors are written per engine. +pub trait TDESEngine: sealed::Sealed {} + +impl sealed::Sealed for TDESInternal {} +impl TDESEngine for TDESInternal {} +impl sealed::Sealed for TDES2Internal {} +impl TDESEngine for TDES2Internal {} + +/// A TDEA key bundle under the mode `M`, over the engine `P`; see the module docs. +/// +/// Built once by [`FromKeyMaterial::from_key_material`], which runs every check the bundle must +/// pass (the key type, length and strength of the material, then the distinct-component and +/// weak-key rules of SP 800-67r2 Sec 3.1 and 3.3.2, as [`TDESInternal::new`] documents) and +/// expands the schedules. Every worker the key hands out afterwards borrows the result. +/// +/// [`TDESKey`] is generic over the engine `P` -- the three-key [`TDESInternal`] or the +/// decryption-only two-key [`TDES2Internal`] -- and the mode `M`, exactly as `AESKey` is in +/// `bouncycastle-aes`. [`TDES3Key`] and [`TDES2Key`] name the two engines. The key holds +/// the mode's key state (for every TDEA mode, the keyed permutation itself) and its workers borrow +/// it, so `create_encryptor` and `create_decryptor` copy no schedule. +/// +/// Which roles a key carries is decided by its mode: `CBC` puts it on the block role pair, +/// `CBC` on the symmetric pair, `ECB` on the ECB pair under `hazmat`, and `CFB`, +/// `CFB8` and `CTR<6, 8>` on the symmetric pair with stream-cipher workers. A `TDES2Key` carries +/// the same roles, but every encrypting worker over it is a compile error: the modes check +/// [`BlockPermutation::DECRYPT_ONLY`](bouncycastle_core::hazmat::BlockPermutation::DECRYPT_ONLY) +/// wherever they make one, and two-key TDEA is disallowed for encryption. See +/// [`TDES2Internal`]. +pub struct TDESKey> { + state: M::KeyState, +} + +/// A three-key TDEA key bundle (24 bytes, `Key1 || Key2 || Key3`) under the mode `M`. +#[allow(non_camel_case_types)] +pub type TDES3Key = TDESKey; + +/// A two-key TDEA key bundle (16 bytes, `Key1 || Key2`, with `Key3 = Key1`) under the mode `M`, +/// **for decryption only**: `create_encryptor` over it does not compile. See [`TDES2Internal`]. +#[allow(non_camel_case_types)] +pub type TDES2Key = TDESKey; + +// Written out rather than derived so that `M`, a marker type, need not be `Clone`. +impl> Clone for TDESKey { + fn clone(&self) -> Self { + Self { state: self.state.clone() } + } +} + +// Likewise for equality: two keys are equal when their mode states are, which every state over +// the engine decides in constant time, so the comparison follows the material without exposing it. +impl> PartialEq for TDESKey +where + M::KeyState: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.state == other.state + } +} + +impl> Eq for TDESKey where M::KeyState: Eq {} + +/// The key-type check the mode asks for (`Mode::KEY_TYPE`), ahead of the engine's own +/// validation. +fn check_key_type( + key: &dyn KeyMaterialTrait, + expected: KeyType, +) -> Result<(), SymmetricCipherError> { + if key.key_type() != expected { + return Err(KeyMaterialError::InvalidKeyType( + "The key is not of the KeyType this mode requires.", + ) + .into()); + } + Ok(()) +} + +// The constructors are per engine rather than once over `P`, because stable Rust cannot name +// `KeyMaterial<{ P::KEY_LEN }>` in a generic signature; the fixed-capacity argument is what makes +// a wrong-length bundle a compile error rather than a runtime one. +impl> FromKeyMaterial<{ crate::KEY_LEN }> for TDES3Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Checks and expands a 24-byte `Key1 || Key2 || Key3` bundle: the key type the mode asks + /// for, then the engine's own checks. Every mode with a role here asks for a + /// `SymmetricCipherKey`, so under a MAC mode, which asks for a `MACKey`, no material passes + /// both. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`], or not a + /// `SymmetricCipherKey`. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 112 bits. + /// * [`KeyMaterialError::WeakKey`] if the three components are not pairwise distinct, or any + /// of them is a weak, semi-weak or possibly weak DEA key; see [`TDESInternal::new`]. + fn from_key_material( + key: &KeyMaterial<{ crate::KEY_LEN }>, + ) -> Result { + check_key_type(key, M::KEY_TYPE)?; + Ok(Self { state: M::key_state(TDESInternal::new(key)?) }) + } +} + +impl> FromKeyMaterial<{ crate::KEY_LEN_2KEY }> for TDES2Key { + const KEY_TYPE: KeyType = M::KEY_TYPE; + + /// Checks and expands a 16-byte `Key1 || Key2` bundle; the checks are the three-key form's, + /// less the strength. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`], or not a + /// `SymmetricCipherKey`. + /// * [`KeyMaterialError::WeakKey`] if `Key1 = Key2`, or either component is a weak, semi-weak + /// or possibly weak DEA key; see [`TDES2Internal::new`]. No strength is too low for two-key + /// TDEA, whose own rating is below the lowest level modelled. + fn from_key_material( + key: &KeyMaterial<{ crate::KEY_LEN_2KEY }>, + ) -> Result { + check_key_type(key, M::KEY_TYPE)?; + Ok(Self { state: M::key_state(TDES2Internal::new(key)?) }) + } +} + +// The role impls forward to whichever mode traits `M` implements over this engine, so which +// roles a key type carries is decided by its mode, as for `AESKey`. +impl + SymmetricEncryptionKey for TDESKey +where + P: TDESEngine, + M: SymmetricEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + SymmetricDecryptionKey for TDESKey +where + P: TDESEngine, + M: SymmetricDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl + BlockEncryptionKey for TDESKey +where + P: TDESEngine, + M: BlockEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>( + &'k self, + rng: &mut dyn RNG, + ) -> Result<(Self::Encryptor<'k>, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + M::encrypt_init(&self.state, rng) + } +} + +impl + BlockDecryptionKey for TDESKey +where + P: TDESEngine, + M: BlockDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self, init_data: &[u8; INIT_DATA_LEN]) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state, init_data) + } +} + +impl EcbEncryptionKey + for TDESKey +where + P: TDESEngine, + M: EcbEncryptMode, +{ + type Encryptor<'k> + = M::Encryptor<'k> + where + Self: 'k; + + fn create_encryptor<'k>(&'k self) -> Self::Encryptor<'k> { + M::encrypt_init(&self.state) + } +} + +impl EcbDecryptionKey + for TDESKey +where + P: TDESEngine, + M: EcbDecryptMode, +{ + type Decryptor<'k> + = M::Decryptor<'k> + where + Self: 'k; + + fn create_decryptor<'k>(&'k self) -> Self::Decryptor<'k> { + M::decrypt_init(&self.state) + } +} + +impl KeyWrappingKey for TDESKey +where + P: TDESEngine, + M: KeyWrapMode, +{ + type Wrapper<'k> + = M::Wrapper<'k> + where + Self: 'k; + + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k> { + M::wrap_init(&self.state) + } +} + +impl KeyUnwrappingKey for TDESKey +where + P: TDESEngine, + M: KeyUnwrapMode, +{ + type Unwrapper<'k> + = M::Unwrapper<'k> + where + Self: 'k; + + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k> { + M::unwrap_init(&self.state) + } +} diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs new file mode 100644 index 000000000..3f521607d --- /dev/null +++ b/crypto/tdes/src/lib.rs @@ -0,0 +1,338 @@ +//! A constant-time, table-free Triple DES block cipher (NIST SP 800-67 Rev 2, TDEA), under the +//! library's key objects. +//! +//! [`TDES3Key`] is a three-key TDEA bundle under the mode `M`, and the decryption-only +//! [`TDES2Key`] a two-key one; each hands out the mode's workers through the role traits in +//! `bouncycastle_core::symmetric_key`. The engines underneath, [`TDESInternal`] and +//! [`TDES2Internal`], are implemented as a Boolean circuit over bit-planes rather than by lookups +//! in S-box tables, which makes them constant-time; see [Design](#design). They are +//! *permutations*, not ciphers you can encrypt data with, which is why they live under [`hazmat`], +//! and TDEA is a **legacy algorithm**: see [Security Considerations](#-security-considerations-) +//! before using it for anything new. +//! +//! # Usage Examples +//! +//! ## Modes of operation +//! +//! A key is built once, under a mode from `bouncycastle_cipher::modes`, and used for as many +//! messages as the data limit allows: `CBC` for CBC (SP 800-38A Sec 6.2) with PKCS#7 +//! padding, `CFB` for CFB64 (Sec 6.3), `CFB8` for the `s = 8` segment size, a different and +//! non-interoperable mode costing one TDES call per byte, `CTR<6, 8>` for CTR (Sec 6.5) with a +//! 6-byte nonce and a 2-byte counter, and `ECB` for ECB (Sec 6.1), which has no IV and is +//! for interoperability and test vectors only -- see +//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher); and `TKW` for +//! TDEA Key Wrap (SP 800-38F Sec 7), legacy only. The aliases [`TDES_CBC`], [`TDES_CFB`], +//! [`TDES_CFB8`], [`TDES_CTR`], [`TDES_ECB`] and [`TDES_TKW`] name the workers the key hands out. +//! +//! SP 800-38A Appendix E is what makes these the standard TDEA modes: TECB, TCBC and TCFB of ANSI +//! X9.52 "are equivalent to" ECB, CBC and CFB "with the TDEA as the underlying block cipher", and +//! CTR with TDEA is explicitly allowed as well. +//! +//! CBC is a block cipher, so it is defined only on whole blocks and the key's mode carries a +//! padding scheme to bridge the difference; the CFB modes and CTR are stream ciphers and take any +//! length with no padding at all. See the `bouncycastle_cipher::modes` crate docs for the +//! comparison, and [`TDES_CBC`] for why the scheme is named in the type. +//! +//! ``` +//! use bouncycastle_tdes::TDES3Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::PKCS7; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages +//! +//! // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +//! let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +//! let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +//! .expect("a 24-byte symmetric cipher key"); +//! let key = TDES3Key::>::from_key_material(&material).expect("a valid key bundle"); +//! +//! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. +//! let plaintext = [0x5Au8; 50]; +//! +//! // The IV is drawn from `rng` and returned; there is no API for supplying one. +//! let (iv, ciphertext) = key.encrypt(&mut rng, &plaintext).expect("encryption"); +//! assert_eq!(ciphertext.len(), 56, "50 bytes padded out to seven 8-byte blocks"); +//! +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! For the block-aligned API -- whole blocks in place, through the block role pair -- make the +//! key under `CBC`; the padded aliases wrap the same worker. +//! +//! ## The raw permutation +//! +//! The engine transforms one block, and `ElectronicCodeBook::new` on it is the one-shot: +//! +//! ``` +//! use bouncycastle_tdes::hazmat::TDESInternal; +//! use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! // NIST CAVP TECBMMT3.rsp, [ENCRYPT] COUNT = 0: KEY1 || KEY2 || KEY3. +//! let key = KeyMaterial::<24>::from_bytes_as_type( +//! &[0xe9, 0x7c, 0x83, 0x13, 0xba, 0x26, 0x5d, 0x43, +//! 0x25, 0x4c, 0xbf, 0x9e, 0x8f, 0x7c, 0x2a, 0xa8, +//! 0xa7, 0x54, 0xd6, 0x5e, 0x8a, 0xe9, 0x97, 0xe3], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 24-byte symmetric cipher key"); +//! +//! let tdes = TDESInternal::new(&key).expect("a valid TDES key bundle"); +//! +//! let mut block = [0x46, 0x56, 0xed, 0x81, 0xa8, 0xbc, 0x58, 0xa9]; +//! tdes.encrypt_block(&mut block); +//! assert_eq!(block, [0x42, 0x6a, 0x0a, 0xc2, 0x65, 0x86, 0xbf, 0x6c]); +//! +//! // The same value decrypts, from the same schedule -- there is no separate decryptor. +//! tdes.decrypt_block(&mut block); +//! assert_eq!(block, [0x46, 0x56, 0xed, 0x81, 0xa8, 0xbc, 0x58, 0xa9]); +//! ``` +//! +//! ## What a key bundle must satisfy +//! +//! A [`TDES3Key`] takes a 24-byte `Key1 || Key2 || Key3` and rejects it if the three component +//! keys are not pairwise distinct (SP 800-67r2 Sec 3.1) or if any of them is one of the 64 weak, +//! semi-weak or possibly weak DES keys (Sec 3.3.2). Parity bits are ignored throughout, as the +//! algorithm ignores them. The checks are listed at [`TDESInternal`]. +//! +//! ## Two-key TDEA: decryption only +//! +//! [`TDES2Key`] takes the 16-byte `Key1 || Key2` of two-key TDEA (`Key3 = Key1`), which +//! SP 800-131A Rev 2 Table 1 lists as "Disallowed" for encryption and "Legacy use" for decryption. +//! Its engine sets `BlockPermutation::DECRYPT_ONLY`, so no encrypting worker can be made over +//! it: the key's `create_encryptor` refuses to compile under every mode, while its decryptors +//! work. That gate +//! stops accidental encryption through the encrypting APIs, not deliberate use of the forward +//! cipher function: CTR decryption *is* CTR encryption, and a CFB decryptor under a chosen IV +//! maps a zero block to `CIPH(IV)`, so two-key encryption can be assembled from those decryptors +//! with the safe API alone (SP 800-38A defines the modes that way). A deployment that must rule +//! two-key encryption out altogether should expose only the CBC and ECB decryptors. The aliases +//! [`TDES2_CBC`], [`TDES2_CFB`], [`TDES2_CFB8`], [`TDES2_CTR`] and [`TDES2_ECB`] are those +//! decryptors, with the IV or nonce supplied by the caller as it was received, and [`TDES2_TKW`] +//! is the key unwrapper, with no wrapper beside it: +//! +//! ``` +//! use bouncycastle_tdes::TDES2Key; +//! use bouncycastle_cipher::modes::CBC; +//! use bouncycastle_cipher::padding::NoPadding; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +//! +//! // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0. +//! let material = KeyMaterial::<16>::from_bytes_as_type( +//! &[0xbc, 0xfe, 0x7f, 0x25, 0xf2, 0xb0, 0xa7, 0x2c, +//! 0x62, 0x76, 0xae, 0x1f, 0x38, 0x9d, 0x8a, 0xe9], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 16-byte symmetric cipher key"); +//! let key = TDES2Key::>::from_key_material(&material) +//! .expect("a valid two-key bundle"); +//! let iv = [0xf2, 0x9d, 0x4b, 0xa5, 0x17, 0x2f, 0xd2, 0x97]; +//! let ciphertext = [0x1b, 0x38, 0x89, 0xc9, 0xdb, 0xe1, 0x0d, 0x03]; +//! +//! let plaintext = key.decrypt(&iv, &ciphertext).expect("decryption"); +//! assert_eq!(plaintext, [0x06, 0x11, 0xa4, 0x74, 0xfb, 0x09, 0x09, 0x78]); +//! ``` +//! +//! # Design +//! +//! ## Why not a lookup table +//! +//! The DEA's eight selection functions S1..S8 are given as tables (SP 800-67r2 Appendix A), and +//! nearly every implementation, Bouncy Castle's `DESEngine` in the Java and C# ports among them, +//! stores them as such -- usually pre-combined with the permutation `P` into eight 64-entry word +//! tables. A table indexed by six bits of `K xor E(R)` is indexed by secret data, so on any CPU +//! with a data cache the memory access pattern, and hence the timing, depends on the key. +//! +//! ## An S-box layer with no table +//! +//! This crate has no tables at run time. The eight S-boxes are treated as thirty-two 6-to-1-bit +//! Boolean functions, and each of those has an algebraic normal form: an XOR of ANDs of its inputs. +//! The ANF coefficients are derived from the spec's tables at compile time, in a `const fn`, and +//! the round function evaluates all thirty-two ANFs at once by Horner's scheme -- 63 ANDs and 63 +//! XORs on `u32` words, with the six input bits of every S-box held one per word, replicated +//! across the four bit positions of the S-box's output nibble. Nothing is ever indexed by a +//! secret, and nothing branches on one. The `sbox` and `des` module docs give the full derivation +//! and the bit layout; they are the place to start when reading the source. +//! +//! The rest of the round is masks and rotations: `E` is never materialised as a 48-bit value but +//! read straight out of `R`'s nibbles and their neighbours, and `P` is nineteen mask-and-rotate +//! groups. The key schedule stores each round key in two words shaped to be XORed into that +//! layout, so no PC-2 or rearrangement happens per round. +//! +//! ## One schedule, both directions +//! +//! SP 800-67r2 Sec 2.2: the inverse transformation is "the very same algorithm" with the round +//! keys used in the opposite order. So the inverse cipher operation reads the same stored schedule +//! backwards, and one [`TDESInternal`] value encrypts and decrypts. TDEA's `IP^-1` at the end of +//! one DEA transformation and `IP` at the start of the next cancel, and are skipped, which changes +//! no intermediate value the spec defines. +//! +//! ## The key holds the engine, the workers borrow it +//! +//! For every TDEA mode the key's state is the expanded engine itself, and the workers a key hands +//! out borrow it, so `create_encryptor` and `create_decryptor` copy no schedule. That is the +//! shape `bouncycastle-aes` has, and [`TDESKey`] is generic over the engine for the same reason +//! `AESKey` is over its parameters. +//! +//! # Memory Usage +//! +//! There are no lookup tables and no heap allocation. The only persistent state is the DEA key +//! schedules, held once in the key: +//! +//! | Type | Key | Rounds | Schedule (persistent) | Tables | +//! |---|---|---|---|---| +//! | [`TDESInternal`] | 24 B | 3 x 16 | 384 B | 0 B | +//! | [`TDES2Internal`] | 16 B | 3 x 16 | 256 B | 0 B | +//! +//! Each round key is 48 bits stored as two 32-bit words (see the `schedule` module for the +//! packing), so 128 bytes per DEA key. Per-call stack usage is a few words of block state, the +//! six 4-byte input planes, and a 128-byte working array for the ANF evaluation, most of which the +//! compiler keeps in registers. A worker adds the mode's own state (an IV or counter block) and a +//! reference to the key. Measured as the deepest frame chain below each entry point's `measure` +//! closure in `mem_usage_benches/src/bench_tdes_mem_usage.rs`, from the compiler's frame-layout +//! remarks and the disassembly of the release build (x86-64, rustc 1.97 nightly, return addresses +//! included): `new` and the one-block calls keep a frame under the closure, the engine's `new` +//! with the schedule's `expand` below it and the out-of-line `encrypt_block` / `decrypt_block`, +//! while the two- and four-block calls are inlined into the closure. The two-key engine is +//! decryption-only, so only its decrypting entry points are measured: +//! +//! | Entry point | [`TDESInternal`] | [`TDES2Internal`] | +//! |---|---|---| +//! | `new` (key expansion) | 1304 B | 920 B | +//! | `encrypt_block` / `decrypt_block` | 104 B | 104 B (decrypt) | +//! | `encrypt_2blocks` / `decrypt_2blocks` | 80 B | 80 B (decrypt) | +//! | `encrypt_4blocks` / `decrypt_4blocks` | 112 B | 112 B (decrypt) | +//! +//! For comparison, the table-driven `DESEngine` carries 2 KiB of combined S/P tables. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## TDEA is a legacy algorithm +//! +//! NIST withdrew SP 800-67 Rev 2 on 1 January 2024. Its announcement +//! (): "The scheduled +//! withdrawal of SP 800-67 Rev. 2 will signify that TDEA is no longer an approved block cipher. +//! TDEA will continue to be allowed for the decryption, key unwrapping, and verification of MACs +//! of already-protected data". +//! SP 800-131A Rev 2, Table 1, gives the transition: three-key TDEA encryption "Deprecated through +//! 2023, Disallowed after 2023", three-key decryption "Legacy use", two-key encryption +//! "Disallowed". Its Table 6 says the same of key wrapping: two-key wrapping "Disallowed", +//! three-key wrapping "Deprecated through 2023, Disallowed after 2023" unless other NIST guidance +//! allows it, and unwrapping under either "Legacy use". SP 800-57 Part 1 Rev 5, Table 2, rates +//! 3TDEA at 112 bits of security, which is what [`TDESInternal`] reports as its maximum strength. +//! +//! This crate exists so that data and protocols that still depend on TDEA can be handled. Do not +//! choose it for a new design; use AES from `bouncycastle-aes`. +//! +//! `BlockPermutation::DECRYPT_ONLY` records this library's policy, not NIST's status: it is set +//! for two-key TDEA alone. Three-key TDEA encryption, which SP 800-131A Rev 2 also disallows +//! after 2023, stays available here because the OpenSSL provider this library serves exposes it, +//! and three-key TKW wrapping, the one SP 800-38F key wrap over TDEA, is kept on the same terms; +//! a deployment that must not encrypt or wrap under TDEA should not use [`TDES3Key`]'s +//! encrypting and wrapping roles. +//! +//! ## The 64-bit block, and the data limit +//! +//! SP 800-67r2 Sec 3.4: "One key bundle shall not be used to apply cryptographic protection (e.g., +//! encrypt) more than 2^20 64-bit data blocks" -- 8 MiB per key bundle, in total, across all +//! messages. The limit exists because a 64-bit block cipher in CBC or CFB leaks plaintext +//! relationships once about 2^32 blocks have been seen under one key (the birthday bound). This +//! crate cannot count blocks across the uses of a key, so the limit has to be enforced by whoever +//! manages the key. +//! +//! The CTR alias has a further constraint from the small block, explained at [`TDES_CTR`]. +//! +//! ## A block permutation is not a cipher +//! +//! [`TDESInternal`] transforms exactly 8 bytes. Using it directly on data means ECB, which is not +//! confidential: identical plaintext blocks produce identical ciphertext blocks, so structure in +//! the plaintext survives encryption. **Do not do it.** Use a mode of operation, and prefer an +//! authenticated one so that ciphertext tampering is detected. +//! +//! The [`TDES_ECB`] alias gives that same block-by-block operation the mode API, so that systems +//! and specifications which require ECB -- and test-vector harnesses -- can use it through the +//! same interface as the other modes. Like the CBC alias it carries a padding scheme, which is +//! what lets it accept data of any length. Neither the mode API nor the padding makes ECB +//! confidential. That is why the permutations and the ECB aliases live under [`hazmat`]; +//! [`bouncycastle_core::hazmat`] lists the supported uses. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, in +//! the cipher *or* in the key schedule (PC-1 and PC-2 iterate over public tables and shift the key +//! by public amounts) *or* in the weak-key check (a structural test on `C0` and `D0`, not a table +//! comparison). The only branches are the round loops and the public direction flag. +//! +//! Caveats worth stating plainly: +//! +//! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the +//! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual +//! ways of forcing the issue, but the property is not contractual. +//! * The working state of a block is not scrubbed after the call. Only the key schedule is wrapped +//! in `Secret`, and so only it is guaranteed to be zeroized on drop; the three 64-bit key words +//! examined during construction are ordinary locals. +//! * Constant-time execution says nothing about power or electromagnetic side channels. +//! +//! # Provenance +//! +//! * Normative reference: **NIST SP 800-67 Rev 2** (withdrawn January 2024), for the DEA engine +//! (Sec 2, Appendix A), TDEA (Sec 3.1), the key requirements (Sec 3.3) and the data limit (Sec +//! 3.4). Every table is transcribed from it and cross-checked against **FIPS 46-3**, whose +//! Appendix 1 is identical. Modes: **NIST SP 800-38A**, Appendix E for TDEA. Status: **SP +//! 800-131A Rev 2** Table 1; strength: **SP 800-57 Part 1 Rev 5** Table 2. +//! * The view of the S-boxes as thirty-two "T-boxes" evaluated in parallel by a tree of +//! `a ^ (x & b)` steps is from Thomas Pornin's `des_ct.c` in BearSSL (MIT licence). This crate +//! derives the tree's constants from the spec tables at compile time rather than hard-coding +//! them. The `IP` / `IP^-1` swap network is Richard Outerbridge's, via Crypto++ (public domain) +//! and BearSSL's `des_support.c`; the rotation groups of `P` were regenerated from Table 3. +//! Each is pinned to the spec's table by a unit-vector test, which for a bit permutation is +//! exhaustive. +//! * Behavioural cross-reference: Bouncy Castle Java's `DESedeEngine` and `DESedeParameters`. +//! Where this crate is stricter -- it rejects the 48 possibly-weak keys as well as the 16 weak +//! and semi-weak ones, requires distinct component keys, and confines two-key TDEA to a +//! decryption-only key -- the crate docs say so and cite the SP 800-67r2 and SP 800-131A Rev 2 +//! text that motivates it. +//! * Verified against the NIST CAVP TDES Multi-block Message Test vectors `TECBMMT{2,3}`, +//! `TCBCMMT{2,3}`, `TCFB64MMT{2,3}` and `TCFB8MMT{2,3}`, from the `bc-test-data` repository. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod cbc; +mod cfb; +mod cfb8; +mod ctr; +mod des; +pub mod hazmat; +mod key; +mod sbox; +mod schedule; +mod tkw; + +/// The DEA block length in bytes: 64 bits (SP 800-67r2 Sec 2). +pub const BLOCK_LEN: usize = 8; + +/// The length of a three-key TDEA key bundle in bytes: three 64-bit DEA keys, +/// `Key1 || Key2 || Key3` (SP 800-67r2 Sec 3.1). +pub const KEY_LEN: usize = 24; + +/// The length of a two-key TDEA key bundle in bytes: `Key1 || Key2`, with `Key3 = Key1` +/// (SP 800-67r2 Sec 3.1). +pub const KEY_LEN_2KEY: usize = 16; + +pub use cbc::{TDES_CBC, TDES2_CBC}; +pub use cfb::{TDES_CFB, TDES2_CFB}; +pub use cfb8::{TDES_CFB8, TDES2_CFB8}; +pub use ctr::{CTR_NONCE_LEN, TDES_CTR, TDES2_CTR}; +pub use key::{TDES2Key, TDES3Key, TDESEngine, TDESKey}; +pub use tkw::{TDES_TKW, TDES2_TKW}; + +// Imports needed for docs +#[allow(unused_imports)] +use hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; +// end of imports needed for docs diff --git a/crypto/tdes/src/sbox.rs b/crypto/tdes/src/sbox.rs new file mode 100644 index 000000000..18334db9f --- /dev/null +++ b/crypto/tdes/src/sbox.rs @@ -0,0 +1,297 @@ +//! The selection functions S1..S8 (SP 800-67r2 Sec 2.3 and Appendix A), evaluated as a Boolean +//! circuit rather than by table lookup. +//! +//! # The tables are only used at compile time +//! +//! [`S_BOXES`] is Appendix A transcribed verbatim, in the spec's own row/column layout so that it +//! can be checked against the document by eye. Nothing indexes it at run time. Instead [`ANF`] is +//! derived from it in a `const fn`, and the run-time S-box layer is a fixed straight-line program +//! over that derived constant: every step is an AND or an XOR of whole words, so no memory access +//! and no branch depends on the data. +//! +//! # T-boxes +//! +//! The eight 6-to-4-bit S-boxes are viewed as thirty-two 6-to-1-bit functions, one per output bit +//! -- "T-boxes", in BearSSL's terminology. T-box `pos` (0..32) produces bit `pos` of the 32-bit +//! block `S1(B1) S2(B2) ... S8(B8)` of eq (7), so it belongs to S-box `8 - pos / 4` and is its +//! output bit `4 - pos % 4` (the spec numbers the four output bits of an S-box left to right, and +//! spec bit 1 of the 32-bit block is the most significant bit of the word). +//! +//! A T-box is a Boolean function of six inputs, and every Boolean function has a unique algebraic +//! normal form (ANF): an XOR of monomials, each monomial an AND of some subset of the inputs. With +//! the inputs of S-box `i` replicated across the four bit positions of its T-boxes (see +//! [`crate::des`] for how the six input planes are built), a single `&` or `^` on a `u32` applies +//! one gate to all thirty-two T-boxes at once. [`ANF`] holds the coefficients: `ANF[k]` bit `pos` +//! is the coefficient, in T-box `pos`, of the monomial whose variables are the set bits of `k` +//! (bit 5 of `k` is `b1`, bit 0 is `b6`, matching the spec's left-to-right order of the six input +//! bits). It is computed from the truth tables by the Mobius transform, the standard butterfly +//! that turns a truth table into ANF coefficients. +//! +//! # Evaluating the ANF +//! +//! [`sbox_layer`] evaluates all thirty-two ANFs by Horner's scheme over one variable at a time: +//! `f = g ^ (b & h)`, where `g` collects the monomials without `b` and `h` those with it, each a +//! smaller ANF in the remaining variables. Peeling off `b6`, then `b5`, and so on to `b1` gives a +//! binary tree of 63 such steps -- 32 on the constants, 16, 8, 4, 2 and 1 -- i.e. 63 ANDs and 63 +//! XORs for the whole S-box layer. BearSSL's `des_ct.c` describes the same tree as "fake +//! multiplexers"; the coefficients here are derived rather than hard-coded, so that the S-box +//! tables remain the only DES-specific constants in the source. +//! +//! One consequence worth knowing: every output bit of every DES S-box is a balanced function (each +//! row of each table is a permutation of 0..15), and a balanced function never contains the +//! full-degree monomial, so `ANF[63]` is zero and the compiler drops that AND. The tests pin both +//! facts. + +/// The eight selection functions, SP 800-67r2 Appendix A (identical in FIPS 46-3 Appendix 1). +/// +/// `S_BOXES[i][row][col]` is the entry S-box `i + 1` gives for the 6-bit input `B` whose first and +/// last bits form `row` and whose middle four bits form `col` (Sec 2.3, the paragraph under +/// Table 2). Laid out exactly as printed in the standard, four rows of sixteen per box. +#[rustfmt::skip] +pub(crate) const S_BOXES: [[[u8; 16]; 4]; 8] = [ + // S1 + [ + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], + [ 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], + [ 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], + [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], + ], + // S2 + [ + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], + [ 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], + [ 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], + [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], + ], + // S3 + [ + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], + [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], + [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], + [ 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], + ], + // S4 + [ + [ 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], + [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], + [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], + [ 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], + ], + // S5 + [ + [ 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], + [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], + [ 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], + [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], + ], + // S6 + [ + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], + [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], + [ 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], + [ 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], + ], + // S7 + [ + [ 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], + [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], + [ 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], + [ 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], + ], + // S8 + [ + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], + [ 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], + [ 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], + [ 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ], +]; + +/// `Si(B)` exactly as Sec 2.3 describes the lookup: `B` is the 6-bit input with its first bit as +/// bit 5 and its last bit as bit 0; the first and last bits select the row, the middle four the +/// column. +/// +/// Used only at compile time (by [`anf_constants`]) and in the tests. Never on secret data. +pub(crate) const fn lookup(sbox: usize, b: usize) -> u8 { + let row = ((b >> 5) & 1) << 1 | (b & 1); + let col = (b >> 1) & 0xF; + S_BOXES[sbox][row][col] +} + +/// T-box `pos`: bit `pos` of the 32-bit block `S1(B1) ... S8(B8)` (eq (7)) as a function of the +/// 6-bit input of its own S-box. +/// +/// S1's four output bits are spec bits 1-4 of the block, i.e. word bits 31 down to 28, so S-box +/// `8 - pos / 4` (numbered from 1) feeds `pos`, and `pos % 4 == 3` is that S-box's leftmost output +/// bit -- the most significant bit of the table entry. +const fn tbox(pos: usize, b: usize) -> u32 { + let entry = lookup(7 - pos / 4, b) as u32; + (entry >> (pos % 4)) & 1 +} + +/// Derives [`ANF`]: truth tables first, then the Mobius transform. +/// +/// The transform is the usual butterfly -- for each variable, XOR the value at "variable clear" +/// into the value at "variable set" -- and because it is bitwise it runs on all 32 T-boxes at once, +/// one word per input value. +const fn anf_constants() -> [u32; 64] { + // c[b] bit pos = T-box pos evaluated at input b: the truth tables, one word per input. + let mut c = [0u32; 64]; + let mut b = 0; + while b < 64 { + let mut pos = 0; + while pos < 32 { + c[b] |= tbox(pos, b) << pos; + pos += 1; + } + b += 1; + } + + // Mobius transform: afterwards c[k] is the coefficient of the monomial over the set bits of k. + let mut var = 1; + while var < 64 { + let mut k = 0; + while k < 64 { + if k & var != 0 { + c[k] ^= c[k ^ var]; + } + k += 1; + } + var <<= 1; + } + c +} + +/// ANF coefficients of all thirty-two T-boxes. See the module docs. +/// +/// Index `k` names a monomial by its variables: bit 5 of `k` is `b1` (the first of the six input +/// bits, in the spec's order), bit 0 is `b6`. Bit `pos` of `ANF[k]` is that monomial's coefficient +/// in T-box `pos`. +pub(crate) const ANF: [u32; 64] = anf_constants(); + +/// The S-box layer: `S1(B1) S2(B2) ... S8(B8)` for all eight S-boxes at once (eq (7)). +/// +/// `x[j]` is the input plane for `b(j+1)`: for every S-box `i`, all four bit positions of the +/// nibble that will hold `Si(Bi)` carry input bit `j+1` of `Bi`. The result has `Si(Bi)` in that +/// nibble, leftmost output bit in the nibble's most significant bit -- which is the spec's +/// bit order for the consolidated 32-bit block, so the result feeds `P` directly. +/// +/// Horner's scheme over the ANF, one variable per level, `b6` first. At each level `y[w]` becomes +/// the ANF in the remaining variables of the monomials that agree with the bits of `w` on the +/// variables already consumed; the pair `(y[2w], y[2w+1])` differs only in the variable being +/// consumed, so `y[2w] ^ (b & y[2w+1])` fixes it. +#[inline(always)] +pub(crate) fn sbox_layer(x: &[u32; 6]) -> u32 { + let mut y = [0u32; 32]; + // Level b6: pairs of constants, ANF[2u] (monomials without b6) and ANF[2u + 1] (with it). + for (u, yu) in y.iter_mut().enumerate() { + *yu = ANF[2 * u] ^ (x[5] & ANF[2 * u + 1]); + } + // Levels b5 down to b2. Each writes y[w] from y[2w] and y[2w + 1]; ascending w never reads an + // entry already overwritten at this level, because 2w >= w. + for w in 0..16 { + y[w] = y[2 * w] ^ (x[4] & y[2 * w + 1]); + } + for w in 0..8 { + y[w] = y[2 * w] ^ (x[3] & y[2 * w + 1]); + } + for w in 0..4 { + y[w] = y[2 * w] ^ (x[2] & y[2 * w + 1]); + } + for w in 0..2 { + y[w] = y[2 * w] ^ (x[1] & y[2 * w + 1]); + } + // Level b1: the root. + y[0] ^ (x[0] & y[1]) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Puts the 6-bit input `b` into the plane layout for S-box `sbox` (0-based) only; every other + /// S-box sees an all-zero input. + fn planes_for(sbox: usize, b: usize) -> [u32; 6] { + let nibble = 0xFu32 << (28 - 4 * sbox); + let mut x = [0u32; 6]; + for (j, plane) in x.iter_mut().enumerate() { + // b's bit 5 is b1, i.e. x[0]. + if (b >> (5 - j)) & 1 == 1 { + *plane = nibble; + } + } + x + } + + #[test] + fn test_every_row_of_every_sbox_is_a_permutation() { + // A property of the DES S-boxes (each row is a permutation of 0..15), and a cheap check on + // the transcription: a repeated or dropped digit breaks it. + for (i, sbox) in S_BOXES.iter().enumerate() { + for (r, row) in sbox.iter().enumerate() { + let mut seen = [false; 16]; + for &v in row { + assert!(!seen[v as usize], "S{} row {r}: {v} appears twice", i + 1); + seen[v as usize] = true; + } + } + } + } + + #[test] + fn test_lookup_matches_the_worked_example_in_sec_2_3() { + // "for input 011011, the row is 01 (i.e., row 1), and the column is determined by 1101 + // (i.e., column 13). The number 5 appears in row 1, column 13, so the output is 0101." + assert_eq!(lookup(0, 0b011011), 5); + } + + #[test] + fn test_sbox_layer_matches_the_tables_exhaustively() { + // 8 S-boxes x 64 inputs, one S-box at a time so the other nibbles can be masked away. + for sbox in 0..8 { + for b in 0..64 { + let out = sbox_layer(&planes_for(sbox, b)); + let nibble = (out >> (28 - 4 * sbox)) & 0xF; + assert_eq!( + nibble as u8, + lookup(sbox, b), + "S{} input {b:06b}: circuit gave {nibble:x}", + sbox + 1 + ); + } + } + } + + #[test] + fn test_sbox_layer_evaluates_all_eight_independently() { + // All eight S-boxes at once, with different inputs, must equal eight separate lookups. + let mut seed = 0x1234_5678u32; + for _ in 0..2000 { + let mut inputs = [0usize; 8]; + let mut x = [0u32; 6]; + for (sbox, input) in inputs.iter_mut().enumerate() { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + *input = (seed >> 13) as usize & 0x3F; + let p = planes_for(sbox, *input); + for j in 0..6 { + x[j] |= p[j]; + } + } + let out = sbox_layer(&x); + for (sbox, &input) in inputs.iter().enumerate() { + let nibble = (out >> (28 - 4 * sbox)) & 0xF; + assert_eq!(nibble as u8, lookup(sbox, input)); + } + } + } + + #[test] + fn test_full_degree_monomial_is_absent() { + // Balanced functions have no degree-6 term, so the top coefficient is zero in every T-box. + assert_eq!(ANF[63], 0); + // ...and the constant term is the value at input 0. + for pos in 0..32 { + assert_eq!((ANF[0] >> pos) & 1, tbox(pos, 0)); + } + } +} diff --git a/crypto/tdes/src/schedule.rs b/crypto/tdes/src/schedule.rs new file mode 100644 index 000000000..7a661be59 --- /dev/null +++ b/crypto/tdes/src/schedule.rs @@ -0,0 +1,256 @@ +//! The key schedule `KS` (SP 800-67r2 Sec 2.1 eq (2), Appendix A: PC-1, the schedule of left +//! shifts, and PC-2), and the weak-key test of Sec 3.3.2. +//! +//! # Storage +//! +//! `KS(n, KEY)` is a 48-bit block, `Kn`. Each is stored as two words, `[lo, hi]`, already arranged +//! for the round function: eq (6) splits `K xor E(R)` into eight 6-bit blocks, and +//! [`crate::des::planes`] builds `E(R)` nibble by nibble, so the key bits are packed to line up +//! with the nibble their block belongs to. For block `i` (1..8) of `Kn`, whose six bits are +//! `Kn[6i-5 .. 6i]`: +//! +//! ```text +//! lo, nibble i: bit 3 = Kn[6i-4] bit 2 = Kn[6i-3] bit 1 = Kn[6i-2] bit 0 = Kn[6i-1] +//! hi, nibble i: bit 1 = Kn[6i] bit 0 = Kn[6i-5] (bits 2 and 3 unused) +//! ``` +//! +//! Nibble 1 is the most significant nibble of the word, matching the block layout in +//! [`crate::des`]. Sixteen round keys of two words each is 128 bytes per DEA key, 384 bytes for a +//! TDEA bundle, against 48 x 16 = 768 bits = 96 bytes for the raw `Kn` values: the packing costs a +//! third more space and saves the round function from doing PC-2 or any per-round rearrangement. +//! The six full planes are not stored (that would be three times the size again); they are +//! spread from `lo` and `hi` on the stack, in the same operations that spread `E(R)`. +//! +//! # Constant time +//! +//! The key is secret. PC-1 and PC-2 are applied by iterating over their tables -- public constants +//! -- and extracting the named bit of the key with a shift by a public amount, so the only +//! data-dependent quantity is the bit value being ORed in. The 28-bit rotations are shifts by +//! public amounts. Nothing indexes memory by a key bit. + +use crate::des::Subkeys; +use bouncycastle_utils::ct::ct_eq_zero_bytes; + +/// PC-1 (Appendix A). Entries are bit numbers of `KEY` (1..64); the first 28 select `C0`, the +/// last 28 select `D0`: "The bits of C(0) are respectively bits 57, 49, 41, ..., 44 and 36 of +/// Key_i, with the bits of D(0) being bits 63, 55, 47, ..., 12 and 4 of Key_i." +#[rustfmt::skip] +const PC1: [u8; 56] = [ + 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4, +]; + +/// The schedule of left shifts (Appendix A), iterations 1 to 16: "by a single left shift is meant +/// a rotation of the bits one place to the left". +const SHIFTS: [u32; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]; + +/// PC-2 (Appendix A). Entries are bit numbers of `Cn Dn` (1..56): "the first bit of Kn is the 14th +/// bit of CnDn, the second bit of Kn is the 17th bit of CnDn, and so on, with the 47th bit of Kn as +/// the 29th bit of CnDn, and the 48th bit of Kn as the 32nd bit of CnDn." +#[rustfmt::skip] +const PC2: [u8; 48] = [ + 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32, +]; + +/// Clears the parity bit of each key byte: "Bits 8, 16, ..., 64 are used for assuring that each +/// byte is of odd parity. (Note that these eight parity bits have no effect on the operation of +/// the algorithm.)" -- Appendix A. PC-1 never selects them. +pub(crate) const PARITY_MASK: u64 = 0xFEFE_FEFE_FEFE_FEFE; + +/// Bit `n` (1..64, bit 1 leftmost) of a 64-bit `KEY`. +#[inline(always)] +fn key_bit(key: u64, n: u8) -> u32 { + ((key >> (64 - n as u32)) & 1) as u32 +} + +/// Bit `n` (1..56, bit 1 leftmost) of the 56-bit `Cn Dn`. +#[inline(always)] +fn cd_bit(cd: u64, n: u8) -> u32 { + ((cd >> (56 - n as u32)) & 1) as u32 +} + +/// A left rotation of a 28-bit block held in the low 28 bits of a word. +#[inline(always)] +fn rotl28(x: u32, s: u32) -> u32 { + ((x << s) | (x >> (28 - s))) & 0x0FFF_FFFF +} + +/// PC-1: `(C0, D0)`, each 28 bits in the low bits of a word with its first bit uppermost. +fn pc1(key: u64) -> (u32, u32) { + let mut c = 0u32; + let mut d = 0u32; + for j in 0..28 { + c = (c << 1) | key_bit(key, PC1[j]); + d = (d << 1) | key_bit(key, PC1[28 + j]); + } + (c, d) +} + +/// Computes `K1..K16` for one 64-bit key and stores them packed as described in the module docs. +/// +/// Appendix A: `Cn` and `Dn` are obtained from `Cn-1` and `Dn-1` by the schedule of left shifts, +/// and `Kn` is PC-2 applied to `Cn Dn`. +pub(crate) fn expand(key: u64, out: &mut Subkeys) { + let (mut c, mut d) = pc1(key); + for n in 0..16 { + c = rotl28(c, SHIFTS[n]); + d = rotl28(d, SHIFTS[n]); + // Cn Dn as a 56-bit string, bit 1 uppermost. + let cd = ((c as u64) << 28) | d as u64; + + let mut lo = 0u32; + let mut hi = 0u32; + for i in 0..8 { + // Block i (0-based) of Kn is Kn bits 6i+1 .. 6i+6, i.e. PC2[6i] .. PC2[6i + 5]. + let b = |j: usize| cd_bit(cd, PC2[6 * i + j]); + // Nibble i (0-based, nibble 0 uppermost) is word bits shift + 3 .. shift. + let shift = 28 - 4 * i; + lo |= ((b(1) << 3) | (b(2) << 2) | (b(3) << 1) | b(4)) << shift; + hi |= (b(0) | (b(5) << 1)) << shift; + } + out[2 * n] = lo; + out[2 * n + 1] = hi; + } +} + +/// Whether a DEA key is one of the 64 keys Sec 3.3.2 says to avoid: the 4 weak keys, the 12 +/// semi-weak keys, or the 48 "possibly weak" keys "that produce only four distinct subkeys +/// (instead of 16)". +/// +/// All three lists share one structure, which is what this checks instead of comparing against a +/// table: after PC-1, `C0` and `D0` are each a 28-bit pattern of period 4 whose repeated nibble +/// has even weight (all zeros, all ones, `0101...`, `1010...`, `0011...`, `0110...`, `1100...` +/// or `1001...`). The schedule only ever rotates `C` and `D`, and a period-4 pattern has at most +/// four distinct rotations, hence at most four distinct subkeys -- one for the weak keys (period +/// 1), two for the semi-weak (period 2), four for the possibly weak. Eight patterns for `C0` +/// times eight for `D0` is exactly 64 keys, and PC-1 is a bijection on the 56 non-parity bits. +/// +/// The other eight period-4 nibbles, the odd-weight ones (`0001`, `0010`, ... `1110`), also give +/// only four distinct subkeys, but the spec's lists do not include them, and this predicate +/// follows the lists: the parity term below keeps those 192 further key classes accepted, as +/// BC Java's `DESParameters` and the spec's own count do. The tests enumerate all 256 +/// rotation-invariant `(C0, D0)` pairs against this predicate, and the spec's three lists. +/// +/// Parity bits are not consulted (PC-1 does not select them), which is also how the spec's lists +/// are meant: "the weak keys listed above and the semi-weak keys and the possibly weak keys listed +/// below are expressed with odd parity". +/// +/// Constant time: the two rotations and the comparison to zero do not branch on the key. +pub(crate) fn is_weak(key: u64) -> bool { + let (c, d) = pc1(key); + // Bit 0 of `x ^ (x >> 1) ^ (x >> 2) ^ (x >> 3)` is the parity of the low nibble, which for a + // period-4 pattern is the parity of every nibble. + let odd = |x: u32| (x ^ (x >> 1) ^ (x >> 2) ^ (x >> 3)) & 1; + let period4_even = (rotl28(c, 4) ^ c) | (rotl28(d, 4) ^ d) | odd(c) | odd(d); + ct_eq_zero_bytes(&period4_even.to_be_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Unpacks a stored round key back to its 48-bit `Kn` (bit 1 uppermost of the low 48 bits). + fn unpack(lo: u32, hi: u32) -> u64 { + let mut k = 0u64; + for i in 0..8 { + let shift = 28 - 4 * i; + let nib_lo = ((lo >> shift) & 0xF) as u64; + let nib_hi = ((hi >> shift) & 0xF) as u64; + let block = ((nib_hi & 1) << 5) | (nib_lo << 1) | ((nib_hi >> 1) & 1); + k = (k << 6) | block; + } + k + } + + /// `KS(n, KEY)` written out the long way from Appendix A, as a 48-bit value. + fn ks_reference(n: usize, key: u64) -> u64 { + let (mut c, mut d) = pc1(key); + for &shift in &SHIFTS[..n] { + c = rotl28(c, shift); + d = rotl28(d, shift); + } + let cd = ((c as u64) << 28) | d as u64; + let mut k = 0u64; + for &src in PC2.iter() { + k = (k << 1) | cd_bit(cd, src) as u64; + } + k + } + + #[test] + fn test_pc1_selects_no_parity_bit() { + for &n in PC1.iter() { + assert_ne!(n % 8, 0, "PC-1 must not select parity bit {n}"); + } + // ...and selects each of the other 56 bits exactly once. + let mut seen = [false; 65]; + for &n in PC1.iter() { + assert!(!seen[n as usize], "PC-1 selects bit {n} twice"); + seen[n as usize] = true; + } + assert_eq!(seen.iter().filter(|&&s| s).count(), 56); + } + + #[test] + fn test_pc2_selects_48_distinct_bits() { + let mut seen = [false; 57]; + for &n in PC2.iter() { + assert!((1..=56).contains(&n)); + assert!(!seen[n as usize], "PC-2 selects bit {n} twice"); + seen[n as usize] = true; + } + assert_eq!(seen.iter().filter(|&&s| s).count(), 48); + } + + #[test] + fn test_shifts_total_28() { + // Sixteen iterations bring C and D back to C0 and D0: the shifts sum to 28. + assert_eq!(SHIFTS.iter().sum::(), 28); + } + + #[test] + fn test_packed_round_keys_unpack_to_the_reference_schedule() { + let mut seed = 0x0123_4567_89AB_CDEFu64; + for _ in 0..200 { + seed = seed + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + let key = seed; + let mut sk = [0u32; 32]; + expand(key, &mut sk); + for n in 0..16 { + assert_eq!( + unpack(sk[2 * n], sk[2 * n + 1]), + ks_reference(n + 1, key), + "K{}", + n + 1 + ); + // Only bits 0 and 1 of each nibble of `hi` are ever set. + assert_eq!(sk[2 * n + 1] & !0x3333_3333, 0); + } + } + } + + #[test] + fn test_parity_bits_do_not_affect_the_schedule() { + let key = 0x0123_4567_89AB_CDEFu64; + let mut a = [0u32; 32]; + let mut b = [0u32; 32]; + expand(key, &mut a); + expand(key ^ !PARITY_MASK, &mut b); + assert_eq!(a, b); + } +} diff --git a/crypto/tdes/src/tkw.rs b/crypto/tdes/src/tkw.rs new file mode 100644 index 000000000..ad9b98b2e --- /dev/null +++ b/crypto/tdes/src/tkw.rs @@ -0,0 +1,132 @@ +//! Type aliases for TDEA Key Wrap, TKW (NIST SP 800-38F Sec 7). The docs are on [`TDES_TKW`], +//! which the crate root re-exports. + +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Tkw; + +/// TDEA Key Wrap: the `Tkw` worker of `bouncycastle-cipher` over the three-key TDEA +/// permutation, which implements `KeyWrapper` and `KeyUnwrapper`, from a `TDES3Key` of +/// `KeyType::SymmetricCipherKey` material through `KeyWrappingKey` and `KeyUnwrappingKey`. It +/// wraps data that is a whole number of 4-byte semiblocks, at least 8 bytes, to one semiblock +/// more; the construction, the one-shots and the security notes are those of +/// [`bouncycastle_cipher::modes::tkw`], and the API is that of `AES_KW_128` in `bouncycastle-aes` +/// at half the width. +/// +/// SP 800-38F Sec 8: TKW is "approved to support legacy systems but should not be used for new +/// applications". A forgery passes its check with probability 2^-32 per attempt (Appendix A.3), +/// against 2^-64 for KW, and TDEA is itself a legacy algorithm; see the crate's security +/// considerations. SP 800-38F publishes no vectors for it; the NIST CAVP `TKW_AE` and `TKW_AD` +/// sample files are run from `bc-test-data`. +/// +/// # Usage Examples +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +/// +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let kek = TDES3Key::::from_key_material(&material).expect("a valid key bundle"); +/// +/// // A 16-byte key wraps to 20 bytes; any other pair of lengths is a compile error. +/// let key_to_wrap = [0x5Au8; 16]; +/// let wrapped: [u8; 20] = kek.wrap_key(&key_to_wrap).expect("wrapping"); +/// assert_eq!(*kek.unwrap_key::<16, 20>(&wrapped).expect("unwrapping"), key_to_wrap); +/// +/// // Lengths known only at run time, through the allocating one-shots. +/// let wrapped = kek.wrap(&[0x5Au8; 24]).expect("wrapping"); +/// assert_eq!(kek.unwrap(&wrapped).expect("unwrapping"), [0x5Au8; 24]); +/// ``` +/// +/// `Dir` is [`Encrypting`](bouncycastle_cipher::Encrypting) for the wrapper and [`Decrypting`] +/// for the unwrapper, as for the cipher modes. +/// +/// # Memory Usage +/// +/// A key is the engine's expanded schedule, as for the other modes (see the crate's table). A +/// worker is the borrow of it, one pointer, and the wrapping function's scratch is one 8-byte +/// block on the stack; the data is wrapped and unwrapped in the caller's buffers. +#[allow(non_camel_case_types)] +pub type TDES_TKW<'k, Dir> = Tkw<'k, TDESInternal, Dir>; + +/// Two-key TDEA Key Wrap, unwrapping only: what a [`TDES2Key`](crate::TDES2Key) hands out +/// through `KeyUnwrappingKey`. Two-key TDEA may not wrap (SP 800-131A Rev 2 Table 6: "Disallowed"), +/// and its engine decrypts only, so the key's `create_wrapper` and every wrapping one-shot refuse +/// to compile, exactly as its `create_encryptor` does under the cipher modes: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyWrappingKey}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // Two-key TDEA may not wrap: this does not compile. +/// let _ = kek.wrap(&[0u8; 16]); +/// ``` +/// +/// The unwrapper the key hands out is `Tkw<'k, TDES2Internal, Decrypting>`, a type with no +/// wrapping method, so neither of these compiles either: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// use bouncycastle_core::traits::KeyWrapper; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // error[E0599]: no method named `wrap_out` found for the unwrapper +/// let mut out = [0u8; 20]; +/// let _ = kek.create_unwrapper().wrap_out(&[0u8; 16], &mut out); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// use bouncycastle_core::traits::KeyWrapper; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // error[E0277]: `KeyWrapper` is not implemented for the unwrapper +/// let unwrapper = kek.create_unwrapper(); +/// let _: &dyn KeyWrapper = &unwrapper; +/// ``` +/// +/// Unwrapping works, with the ciphertext as it was received: +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::errors::SymmetricCipherError; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // Not a ciphertext this key made, so it is rejected rather than unwrapped to garbage. +/// assert!(matches!(kek.unwrap(&[0u8; 20]), Err(SymmetricCipherError::DecryptionFailed))); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_TKW<'k> = Tkw<'k, TDES2Internal, Decrypting>; diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs new file mode 100644 index 000000000..e6d6121a5 --- /dev/null +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -0,0 +1,199 @@ +//! What each public TDES alias is: the algorithm it reports and the type it resolves to. +//! +//! Each alias reports its own algorithm through `Algorithm`, not the engine it is built on: +//! `TDES_CBC` is `"TDES_CBC"`, while `TDESInternal` stays `"TDES"`. The strength is the key's: +//! 112 bits for three-key TDEA, and none at all for two-key. The direction and the padding are +//! type parameters of one algorithm, not different algorithms, so every combination of them must +//! report the same name. + +use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; +use bouncycastle_cipher::modes::{CBC, Cbc, Cfb, Cfb8, Ctr}; +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::{BlockPermutation, Mode}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_tdes::hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{ + BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, TDES_TKW, + TDES2_CBC, TDES2_CFB, TDES2_CFB8, TDES2_CTR, TDES2_TKW, TDES3Key, +}; +use core::mem::size_of; + +/// Both directions of one algorithm report `name` and `strength`. +fn check(name: &str, strength: SecurityStrength) { + assert_eq!(Enc::ALG_NAME, name); + assert_eq!(Dec::ALG_NAME, name); + assert_eq!(Enc::MAX_SECURITY_STRENGTH, strength, "{name}"); + assert_eq!(Dec::MAX_SECURITY_STRENGTH, strength, "{name}"); +} + +/// The one direction a two-key alias has. +fn check_decrypting(name: &str) { + assert_eq!(Dec::ALG_NAME, name); + assert_eq!(Dec::MAX_SECURITY_STRENGTH, SecurityStrength::None, "{name}"); +} + +#[test] +fn cbc() { + use SecurityStrength::*; + check::, TDES_CBC<'_, Decrypting, NoPadding>>( + "TDES_CBC", _112bit, + ); + check::, TDES_CBC<'_, Decrypting, PKCS7>>("TDES_CBC", _112bit); + check_decrypting::>("TDES2_CBC"); + check_decrypting::>("TDES2_CBC"); +} + +/// One worker serves both halves of a key wrap, so the alias is checked against itself; the +/// two-key alias has only the unwrapping half. +#[test] +fn tkw() { + use SecurityStrength::*; + check::, TDES_TKW<'_, Decrypting>>("TDES_TKW", _112bit); + check_decrypting::>("TDES2_TKW"); +} + +#[test] +fn ecb() { + use SecurityStrength::*; + check::, TDES_ECB<'_, Decrypting, NoPadding>>( + "TDES_ECB", _112bit, + ); + check::, TDES_ECB<'_, Decrypting, PKCS7>>("TDES_ECB", _112bit); + check_decrypting::>("TDES2_ECB"); + check_decrypting::>("TDES2_ECB"); +} + +#[test] +fn cfb_cfb8_ctr() { + use SecurityStrength::*; + check::, TDES_CFB<'_, Decrypting>>("TDES_CFB", _112bit); + check::, TDES_CFB8<'_, Decrypting>>("TDES_CFB8", _112bit); + check::, TDES_CTR<'_, Decrypting>>("TDES_CTR", _112bit); + check_decrypting::>("TDES2_CFB"); + check_decrypting::>("TDES2_CFB8"); + check_decrypting::>("TDES2_CTR"); +} + +#[test] +fn the_engines_keep_their_own_names() { + assert_eq!(TDESInternal::ALG_NAME, "TDES"); + assert_eq!(TDESInternal::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!(TDES2Internal::ALG_NAME, "TDES-2KEY"); + assert_eq!(TDES2Internal::MAX_SECURITY_STRENGTH, SecurityStrength::None); +} + +// Three-key TDEA may encrypt; two-key TDEA may not (SP 800-131A Rev 2 Table 1), which is the +// gate the modes check at compile time -- so this is checked at compile time too. +const _: () = assert!(!>::DECRYPT_ONLY); +const _: () = assert!(>::DECRYPT_ONLY); + +/// The engines are exactly their packed schedules, the sizes the crate docs quote: sixteen +/// two-word round keys per DEA key, three or two keys, and nothing else. +#[test] +fn the_engines_are_exactly_their_schedules() { + assert_eq!(size_of::(), 3 * 16 * 2 * 4); + assert_eq!(size_of::(), 2 * 16 * 2 * 4); +} + +fn key_for>() -> TDES3Key { + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + let key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a 24-byte symmetric cipher key"); + TDES3Key::from_key_material(&key).expect("a valid key bundle") +} + +/// The aliases must resolve to exactly the types they claim to, at both directions, and be what +/// the keys hand out. +/// +/// A type alias that quietly resolved to something else -- the wrong padding, the wrong direction, +/// the wrong block length -- would still compile everywhere it is used, so this pins the projection +/// itself by asserting the layouts coincide with the fully spelled-out types. ECB's projection +/// carries `INIT_DATA_LEN = 0` where CBC's carries 8. +#[test] +fn the_aliases_name_the_expected_types() { + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Cbc<'_, TDESInternal, Encrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor< + Cbc<'_, TDESInternal, Decrypting, BLOCK_LEN>, + PKCS7, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + // The two directions are genuinely different types, so the encryptor and the decryptor do not + // have to agree in size -- and here they do not, which is itself evidence the projection + // selected two different adapters rather than one. + assert_ne!( + size_of::>(), + size_of::>() + ); + + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor< + Ecb<'_, TDESInternal, Encrypting, BLOCK_LEN>, + PKCS7, + 0, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor< + Ecb<'_, TDESInternal, Decrypting, BLOCK_LEN>, + PKCS7, + 0, + BLOCK_LEN, + >, + >() + ); + + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + + // ...and the key hands out exactly those types. + let mut rng = HashDRBG_SHA512::new_from_os(); + let key = key_for::>(); + let (enc, iv) = key.create_encryptor(&mut rng).unwrap(); + let _: TDES_CBC<'_, Encrypting, PKCS7> = enc; + let _: TDES_CBC<'_, Decrypting, PKCS7> = key.create_decryptor(&iv); + let key = key_for::>(); + let (enc, no_iv) = key.create_encryptor(&mut rng).unwrap(); + let _: TDES_ECB<'_, Encrypting, PKCS7> = enc; + let _: TDES_ECB<'_, Decrypting, PKCS7> = key.create_decryptor(&no_iv); +} diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs new file mode 100644 index 000000000..ecd65540a --- /dev/null +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -0,0 +1,359 @@ +//! Known-answer tests against the NIST CAVP "TDES Multi block Message Test" vectors from the +//! `bc-test-data` repository: `TECBMMT3`, `TCBCMMT3`, `TCFB64MMT3`, `TCFB8MMT3` (three-key, for +//! [`TDES3Key`]) and `TECBMMT2`, `TCBCMMT2`, `TCFB64MMT2`, `TCFB8MMT2` (two-key, for [`TDES2Key`]). +//! +//! The files are read through the test framework's `bc_test_data` loader: `bc-test-data` cloned +//! beside this repository, with the response files from NIST's `tdesmmt.zip` (CAVS 18.0) under +//! `crypto/aes_tdes_vectors/TDES/`. If the repository is absent the tests print a warning and +//! pass, as the AES ACVP and ML-KEM / ML-DSA suites do; if it is present and a file is missing, +//! the loader panics rather than letting that file's test pass vacuously. +//! +//! Every case is checked in **both** directions regardless of the section it came from: +//! encrypting `PLAINTEXT` must give `CIPHERTEXT` and decrypting `CIPHERTEXT` must give +//! `PLAINTEXT`. The `[DECRYPT]` cases use different keys from the `[ENCRYPT]` ones, so this doubles +//! the coverage rather than repeating it. The two-key files have only a `[DECRYPT]` section -- +//! NIST tests two-key TDEA in that direction alone, as this crate does. +//! +//! # How the modes are driven +//! +//! ECB vectors are block-permutation vectors and go through the engine directly, one block at a +//! time, as well as through the four-block path where the message is long enough. +//! +//! The CBC and CFB vectors carry an IV, and a key's `create_encryptor` draws the IV from the RNG +//! it is given, so three-key encryption passes a `FixedSeedRNG` that emits the vector's IV, and the +//! returned init data is asserted equal to it before any ciphertext is compared; decryption takes +//! the IV directly. The keys are made under the modes the `TDES_*` aliases name, so the aliases' +//! parameter choices (an 8-byte block, an 8-byte IV) are what is under test. +//! +//! Two-key TDEA is decryption-only -- `create_encryptor` does not compile on [`TDES2Key`] -- so +//! its CBC and CFB vectors are checked by decrypting `CIPHERTEXT` (both sections), and the forward +//! direction is checked on the ECB vectors through the raw permutation, which CFB decryption is +//! built on anyway. +//! +//! The Monte Carlo Test files (`tdesmct_intermediate.zip`) are **not** used: their chained update +//! rule is defined by SP 800-20, and implementing it is a separate piece of work from the engine. + +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook, Mode}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, + SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, +}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_rng::FixedSeedRNG; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +/// Where the response files sit under `bc-test-data`. +const CAVP_DIR: &str = "crypto/aes_tdes_vectors/TDES"; + +/// One MMT case. `iv` is empty for ECB. +#[derive(Default, Debug, Clone)] +struct MmtCase { + section: String, + count: u32, + key1: Vec, + key2: Vec, + key3: Vec, + iv: Vec, + plaintext: Vec, + ciphertext: Vec, +} + +/// Parses a CAVS `.rsp` file: `[SECTION]` headers, `COUNT = n` starting a case, `NAME = hex` +/// fields. Lines may end in CRLF. +fn parse_rsp(text: &str) -> Vec { + let mut cases: Vec = Vec::new(); + let mut section = String::new(); + for line in text.lines().map(str::trim) { + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { + section = name.to_string(); + continue; + } + let (name, value) = line.split_once(" = ").expect("`NAME = value` line"); + if name == "COUNT" { + cases.push(MmtCase { + section: section.clone(), + count: value.parse().expect("a decimal COUNT"), + ..Default::default() + }); + continue; + } + let case = cases.last_mut().expect("a field before any COUNT"); + let bytes = hex::decode(value).expect("hex field"); + match name { + "KEY1" => case.key1 = bytes, + "KEY2" => case.key2 = bytes, + "KEY3" => case.key3 = bytes, + "IV" => case.iv = bytes, + "PLAINTEXT" => case.plaintext = bytes, + "CIPHERTEXT" => case.ciphertext = bytes, + other => panic!("unexpected field {other}"), + } + } + cases +} + +/// Loads a response file, or `None` when the data is absent. +/// +/// The three-key files carry ten `[ENCRYPT]` and ten `[DECRYPT]` cases. The two-key files carry +/// ten `[DECRYPT]` cases and **no `[ENCRYPT]` section at all**: CAVP tests two-key TDEA in the +/// decrypt direction only, which is the same policy as [`TDES2Key`]. +fn load(file: &str) -> Option> { + let text = bc_test_data(CAVP_DIR, file)?; + let cases = parse_rsp(&text); + let encrypt = cases.iter().filter(|c| c.section == "ENCRYPT").count(); + let decrypt = cases.iter().filter(|c| c.section == "DECRYPT").count(); + assert_eq!(encrypt + decrypt, cases.len(), "{file}: unexpected section"); + assert_eq!(decrypt, 10, "{file}: ten decrypt cases"); + assert_eq!(encrypt, if file.ends_with("MMT2.rsp") { 0 } else { 10 }, "{file}: encrypt cases"); + println!("{file}: {} cases found under bc-test-data/{CAVP_DIR}", cases.len()); + Some(cases) +} + +fn material3(case: &MmtCase) -> KeyMaterial { + let bytes: Vec = [&case.key1[..], &case.key2[..], &case.key3[..]].concat(); + assert_eq!(bytes.len(), KEY_LEN); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// The two-key files carry `KEY3 = KEY1`; the engine takes `KEY1 || KEY2`. +fn material2(case: &MmtCase) -> KeyMaterial { + assert_eq!(case.key1, case.key3, "a two-key vector has KEY3 = KEY1"); + let bytes: Vec = [&case.key1[..], &case.key2[..]].concat(); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +fn key3>(case: &MmtCase) -> TDES3Key { + TDES3Key::from_key_material(&material3(case)).expect("a valid CAVP key bundle") +} + +fn key2>(case: &MmtCase) -> TDES2Key { + TDES2Key::from_key_material(&material2(case)).expect("a valid CAVP two-key bundle") +} + +fn iv(case: &MmtCase) -> [u8; BLOCK_LEN] { + case.iv.clone().try_into().expect("an 8-byte IV") +} + +fn label(file: &str, case: &MmtCase) -> String { + format!("{file} [{}] COUNT = {}", case.section, case.count) +} + +/// ECB through a raw permutation: single blocks, then the four-block path. +fn check_ecb>(file: &str, case: &MmtCase, perm: &P) { + let name = label(file, case); + assert_eq!(case.plaintext.len(), case.ciphertext.len()); + assert_eq!(case.plaintext.len() % BLOCK_LEN, 0, "{name}: whole blocks"); + + let mut buf = case.plaintext.clone(); + for block in buf.as_chunks_mut::().0 { + perm.encrypt_block(block); + } + assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + for block in buf.as_chunks_mut::().0 { + perm.decrypt_block(block); + } + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + + // The four-block methods must give the same answers; the longer cases (up to ten + // blocks) exercise them, with the remainder going through single blocks. + { + let (blocks, _) = buf.as_chunks_mut::(); + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours { + perm.encrypt_4blocks(four); + } + for block in rest { + perm.encrypt_block(block); + } + } + assert_eq!(buf, case.ciphertext, "{name}: encrypt_4blocks"); + { + let (blocks, _) = buf.as_chunks_mut::(); + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours { + perm.decrypt_4blocks(four); + } + for block in rest { + perm.decrypt_block(block); + } + } + assert_eq!(buf, case.plaintext, "{name}: decrypt_4blocks"); +} + +#[test] +fn tecbmmt3() { + let Some(cases) = load("TECBMMT3.rsp") else { return }; + for case in &cases { + let tdes = TDESInternal::new(&material3(case)).expect("a valid CAVP key bundle"); + check_ecb("TECBMMT3", case, &tdes); + } +} + +#[test] +fn tecbmmt2() { + let Some(cases) = load("TECBMMT2.rsp") else { return }; + for case in &cases { + let tdes = TDES2Internal::new(&material2(case)).expect("a valid CAVP two-key bundle"); + check_ecb("TECBMMT2", case, &tdes); + } +} + +#[test] +fn tcbcmmt3() { + let Some(cases) = load("TCBCMMT3.rsp") else { return }; + + for case in &cases { + let key = key3::>(case); + let iv = iv(case); + let name = label("TCBCMMT3", case); + + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::new(iv)).unwrap(); + assert_eq!(got_iv, iv, "{name}: the pinned RNG should reproduce the vector's IV"); + let mut buf = case.plaintext.clone(); + enc.do_encrypt_blocks_inplace(buf.as_chunks_mut::().0).unwrap(); + assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + + let mut dec = key.create_decryptor(&iv); + dec.do_decrypt_blocks_inplace(buf.as_chunks_mut::().0).unwrap(); + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + } +} + +#[test] +fn tcbcmmt2_decrypts() { + let Some(cases) = load("TCBCMMT2.rsp") else { return }; + + for case in &cases { + let name = label("TCBCMMT2", case); + let key = key2::>(case); + let mut dec = key.create_decryptor(&iv(case)); + let mut buf = case.ciphertext.clone(); + dec.do_decrypt_blocks_inplace(buf.as_chunks_mut::().0).unwrap(); + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + } +} + +/// One CFB64 or CFB8 case through a three-key key: both directions, whole and byte by byte. +fn check_stream3_case<'k, K>(name: &str, key: &'k K, case: &MmtCase) +where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + let iv = iv(case); + + let (mut enc, got_iv) = key.create_encryptor(&mut FixedSeedRNG::new(iv)).unwrap(); + assert_eq!(got_iv, iv, "{name}: the pinned RNG should reproduce the vector's IV"); + let mut buf = case.plaintext.clone(); + enc.do_encrypt_inplace(&mut buf).unwrap(); + assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + + let mut dec = key.create_decryptor(&iv); + dec.do_decrypt_inplace(&mut buf).unwrap(); + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + + // The same message fed one byte at a time must give the same ciphertext. + let (mut enc, _) = key.create_encryptor(&mut FixedSeedRNG::new(iv)).unwrap(); + let mut buf = case.plaintext.clone(); + for byte in buf.iter_mut() { + enc.do_encrypt_inplace(core::slice::from_mut(byte)).unwrap(); + } + assert_eq!(buf, case.ciphertext, "{name}: encrypt byte by byte"); +} + +/// One case through a two-key key: decryption only, whole and byte by byte. +fn check_stream2_case<'k, K>(name: &str, key: &'k K, case: &MmtCase) +where + K: SymmetricDecryptionKey, + K::Decryptor<'k>: StreamCipherDecryptor, +{ + let iv = iv(case); + + let mut dec = key.create_decryptor(&iv); + let mut buf = case.ciphertext.clone(); + dec.do_decrypt_inplace(&mut buf).unwrap(); + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + + let mut dec = key.create_decryptor(&iv); + let mut buf = case.ciphertext.clone(); + for byte in buf.iter_mut() { + dec.do_decrypt_inplace(core::slice::from_mut(byte)).unwrap(); + } + assert_eq!(buf, case.plaintext, "{name}: decrypt byte by byte"); +} + +#[test] +fn tcfb64mmt3() { + let Some(cases) = load("TCFB64MMT3.rsp") else { return }; + for case in &cases { + check_stream3_case(&label("TCFB64MMT3", case), &key3::(case), case); + } +} + +#[test] +fn tcfb8mmt3() { + let Some(cases) = load("TCFB8MMT3.rsp") else { return }; + for case in &cases { + check_stream3_case(&label("TCFB8MMT3", case), &key3::(case), case); + } +} + +#[test] +fn tcfb64mmt2_decrypts() { + let Some(cases) = load("TCFB64MMT2.rsp") else { return }; + for case in &cases { + check_stream2_case(&label("TCFB64MMT2", case), &key2::(case), case); + } +} + +#[test] +fn tcfb8mmt2_decrypts() { + let Some(cases) = load("TCFB8MMT2.rsp") else { return }; + for case in &cases { + check_stream2_case(&label("TCFB8MMT2", case), &key2::(case), case); + } +} + +#[test] +fn the_vector_files_have_the_expected_shape() { + // Messages of one block per COUNT (CFB8: one byte), all keys with odd parity as CAVP generates + // them, and KEY3 = KEY1 in the two-key files. A damaged copy of the data fails here rather than + // as a puzzling cipher mismatch. (`load` checks the case counts.) + let files = [ + ("TECBMMT3.rsp", BLOCK_LEN, false), + ("TCBCMMT3.rsp", BLOCK_LEN, false), + ("TCFB64MMT3.rsp", BLOCK_LEN, false), + ("TCFB8MMT3.rsp", 1, false), + ("TECBMMT2.rsp", BLOCK_LEN, true), + ("TCBCMMT2.rsp", BLOCK_LEN, true), + ("TCFB64MMT2.rsp", BLOCK_LEN, true), + ("TCFB8MMT2.rsp", 1, true), + ]; + for (file, unit, two_key) in files { + let Some(cases) = load(file) else { return }; + for case in &cases { + for k in [&case.key1, &case.key2, &case.key3] { + assert_eq!(k.len(), 8, "{file}: key length"); + for &b in k { + assert_eq!(b.count_ones() % 2, 1, "{file}: CAVP keys have odd parity"); + } + } + assert_eq!(case.key1 == case.key3, two_key, "{file}: KEY3 = KEY1 iff two-key"); + assert_eq!(case.plaintext.len(), unit * (case.count as usize + 1), "{file}"); + assert_eq!(case.ciphertext.len(), case.plaintext.len(), "{file}"); + } + } +} diff --git a/crypto/tdes/tests/electronic_code_book_tests.rs b/crypto/tdes/tests/electronic_code_book_tests.rs new file mode 100644 index 000000000..2972acf91 --- /dev/null +++ b/crypto/tdes/tests/electronic_code_book_tests.rs @@ -0,0 +1,28 @@ +//! `ElectronicCodeBook` / `BlockPermutation` conformance of the two engines, via the shared test +//! framework. +//! +//! The framework checks the properties every implementor must have -- both directions are +//! inverses, the permutation is injective, the pair and four-block methods are indistinguishable +//! from single-block calls *including their order*, and the key checks behave, including the +//! security-strength policy against `MAX_SECURITY_STRENGTH` (112 bits here, not the 192 a 24-byte +//! key would otherwise imply). +//! +//! The framework's key is the first 24 (or 16) bytes of `DUMMY_SEED`, `00 01 02 ..`, whose 8-byte +//! components are distinct and not weak, so it is a valid bundle for either engine. +//! +//! For [`TDES2Key`] the strength policy is against `MAX_SECURITY_STRENGTH = None`, so every +//! strength the framework tries must be accepted; the framework checks that too. + +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; + +#[test] +fn tdes_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::(); +} + +#[test] +fn tdes2key_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::(); +} diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs new file mode 100644 index 000000000..2374258c4 --- /dev/null +++ b/crypto/tdes/tests/key_tests.rs @@ -0,0 +1,397 @@ +//! Key-bundle handling: what the engines' `ElectronicCodeBook::new` accepts and rejects, and why. +//! The keys at the crate root run these checks through it, see `mode_key_tests.rs`. +//! +//! * The 4 weak, 12 semi-weak and 48 possibly weak DEA keys listed in SP 800-67r2 Sec 3.3.2, all +//! rejected in any component position, and shown to be exactly the 64 keys the engine's +//! structural test recognises. +//! * Bundles whose component keys are not pairwise distinct (Sec 3.1), including two-key TDEA and +//! components that differ only in parity bits. +//! * Parity bits are ignored by the cipher, as Appendix A says. +//! * The `KeyMaterial` checks: type, length and security strength. + +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; + +/// Sec 3.3.2, "Keys that are considered weak are (in hexadecimal format)". +const WEAK: [u64; 4] = + [0x0101010101010101, 0xFEFEFEFEFEFEFEFE, 0xE0E0E0E0F1F1F1F1, 0x1F1F1F1F0E0E0E0E]; + +/// Sec 3.3.2, "These semi-weak keys are (in hexadecimal format)", six pairs. +const SEMI_WEAK: [u64; 12] = [ + 0x011F011F010E010E, 0x1F011F010E010E01, 0x01E001E001F101F1, 0xE001E001F101F101, + 0x01FE01FE01FE01FE, 0xFE01FE01FE01FE01, 0x1FE01FE00EF10EF1, 0xE01FE01FF10EF10E, + 0x1FFE1FFE0EFE0EFE, 0xFE1FFE1FFE0EFE0E, 0xE0FEE0FEF1FEF1FE, 0xFEE0FEE0FEF1FEF1, +]; + +/// Sec 3.3.2, "There are also 48 keys that produce only four distinct subkeys (instead of 16) - +/// these are called possibly weak keys", in the order printed (three columns, read row by row). +const POSSIBLY_WEAK: [u64; 48] = [ + 0x01011F1F01010E0E, 0x1F1F01010E0E0101, 0xE0E01F1FF1F10E0E, 0x0101E0E00101F1F1, + 0x1F1FE0E00E0EF1F1, 0xE0E0FEFEF1F1FEFE, 0x0101FEFE0101FEFE, 0x1F1FFEFE0E0EFEFE, + 0xE0FE011FF1FE010E, 0x011F1F01010E0E01, 0x1FE001FE0EF101FE, 0xE0FE1F01F1FE0E01, + 0x011FE0FE010EF1FE, 0x1FE0E01F0EF1F10E, 0xE0FEFEE0F1FEFEF1, 0x011FFEE0010EFEF1, + 0x1FE0FE010EF1FE01, 0xFE0101FEFE0101FE, 0x01E01FFE01F10EFE, 0x1FFE01E00EFE01F1, + 0xFE011FE0FE010EF1, 0xFE01E01FFE01F10E, 0x1FFEE0010EFEF101, 0xFE1F01E0FE0E01F1, + 0x01E0E00101F1F101, 0x1FFEFE1F0EFEFE0E, 0xFE1FE001FE0EF101, 0x01E0FE1F01F1FE0E, + 0xE00101E0F10101F1, 0xFE1F1FFEFE0E0EFE, 0x01FE1FE001FE0EF1, 0xE0011FFEF1010EFE, + 0xFEE0011FFEF1010E, 0x01FEE01F01FEF10E, 0xE001FE1FF101FE0E, 0xFEE01F01FEF10E01, + 0x01FEFE0101FEFE01, 0xE01F01FEF10E01FE, 0xFEE0E0FEFEF1F1FE, 0x1F01011F0E01010E, + 0xE01F1FE0F10E0EF1, 0xFEFE0101FEFE0101, 0x1F01E0FE0E01F1FE, 0xE01FFE01F10EFE01, + 0xFEFE1F1FFEFE0E0E, 0x1F01FEE00E01FEF1, 0xE0E00101F1F10101, 0xFEFEE0E0FEFEF1F1, +]; + +/// PC-1 (Appendix A), for rebuilding a key from a `(C0, D0)` pair in the tests. +#[rustfmt::skip] +const PC1: [u8; 56] = [ + 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4, +]; + +const PARITY_MASK: u64 = 0xFEFE_FEFE_FEFE_FEFE; + +/// Three known-good, distinct, non-weak DEA keys (from CAVP `TECBMMT3` COUNT = 0). +const GOOD: [u64; 3] = [0xe97c8313ba265d43, 0x254cbf9e8f7c2aa8, 0xa754d65e8ae997e3]; + +fn bundle(k: [u64; 3]) -> KeyMaterial { + let mut bytes = [0u8; KEY_LEN]; + for (i, key) in k.iter().enumerate() { + bytes[8 * i..8 * i + 8].copy_from_slice(&key.to_be_bytes()); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn is_weak_key_error(result: Result) -> bool { + matches!(result, Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::WeakKey(_)))) +} + +/// The DEA key (odd parity bits cleared) whose PC-1 output is `(c, d)`: inverse PC-1, which is a +/// bijection on the 56 non-parity bits. +fn key_from_cd(c: u32, d: u32) -> u64 { + let cd = ((c as u64) << 28) | d as u64; + let mut key = 0u64; + for (j, &dst) in PC1.iter().enumerate() { + let bit = (cd >> (55 - j)) & 1; + key |= bit << (64 - dst as u32); + } + key & PARITY_MASK +} + +/// The engine's verdict on `key` in the first component position of an otherwise good bundle. +fn refused_as_weak(key: u64) -> bool { + is_weak_key_error(TDESInternal::new(&bundle([key, GOOD[1], GOOD[2]]))) +} + +/// The 28-bit pattern that repeats the nibble `n` seven times. +fn repeated(n: u32) -> u32 { + (0..7).fold(0, |acc, _| (acc << 4) | n) +} + +/// Every key that is invariant under the schedule's rotations -- `C0` and `D0` each one of the +/// 16 period-4 patterns, 256 pairs -- gets the documented verdict: refused exactly when both +/// nibbles have even weight, which is the spec's 64 keys, and accepted for the other 192 (such +/// as `1F0101010E010101`, `C0 = 0`, `D0 = 0x1111111`), which Sec 3.3.2 does not list though they +/// too give four distinct subkeys. This asks the predicate about every rotation-invariant key, +/// not only the listed ones. +#[test] +fn every_rotation_invariant_key_gets_the_documented_verdict() { + let mut refused = 0; + for cn in 0..16u32 { + for dn in 0..16u32 { + let key = key_from_cd(repeated(cn), repeated(dn)); + let listed = cn.count_ones().is_multiple_of(2) && dn.count_ones().is_multiple_of(2); + assert_eq!(refused_as_weak(key), listed, "C0 nibble {cn:04b}, D0 nibble {dn:04b}"); + refused += usize::from(listed); + } + } + assert_eq!(refused, 64); + assert!(!refused_as_weak(0x1F0101010E010101), "C0 = 0, D0 = 0x1111111 is not in Sec 3.3.2"); + assert!(refused_as_weak(0x1F1F1F1F0E0E0E0E), "the weak key with C0 = 0, D0 = 0xFFFFFFF is"); +} + +/// Keys whose `C0` and `D0` are equal but have no period are accepted: the predicate ORs the two +/// halves' period tests, and an XOR in its place would call every such key weak (2^28 classes). +#[test] +fn equal_halves_without_a_period_are_accepted() { + let mut x: u32 = 0x9E37_79B9; + let mut accepted = 0; + for _ in 0..2000 { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let half = x & 0x0FFF_FFFF; + if half.rotate_left(4) & 0x0FFF_FFFF == half + || (half << 4 | half >> 24) & 0x0FFF_FFFF == half + { + continue; + } + let key = key_from_cd(half, half); + if [GOOD[1], GOOD[2]].contains(&key) { + continue; + } + assert!(!refused_as_weak(key), "C0 = D0 = {half:07x} has no period 4"); + accepted += 1; + } + assert!(accepted > 1900, "{accepted} keys checked"); + assert!(!refused_as_weak(0x10BA7C00FE543800), "C0 = D0 = 0x1234567"); +} + +#[test] +fn a_good_bundle_is_accepted() { + assert!(TDESInternal::new(&bundle(GOOD)).is_ok()); +} + +#[test] +fn every_listed_weak_key_is_rejected_in_every_position() { + let all: Vec = + WEAK.iter().chain(SEMI_WEAK.iter()).chain(POSSIBLY_WEAK.iter()).copied().collect(); + assert_eq!(all.len(), 64); + for &weak in &all { + for position in 0..3 { + let mut k = GOOD; + k[position] = weak; + assert!( + is_weak_key_error(TDESInternal::new(&bundle(k))), + "weak key {weak:016x} in position {} should be rejected", + position + 1 + ); + // ...and with the parity bits flipped, since the algorithm ignores them. + k[position] = weak ^ !PARITY_MASK; + assert!( + is_weak_key_error(TDESInternal::new(&bundle(k))), + "weak key {weak:016x} with even parity" + ); + } + } +} + +#[test] +fn the_three_lists_are_exactly_the_period_four_keys() { + // The engine detects a weak key structurally: C0 and D0 each have period 4 (see the crate's + // `schedule` module). That is 8 x 8 = 64 keys, and PC-1 is a bijection on the non-parity bits, + // so this reconstructs all 64 from the eight 28-bit patterns and checks they are precisely the + // 64 keys the spec lists -- which also proves the transcription above. + let patterns: [u32; 8] = [ + 0x000_0000, 0xFFF_FFFF, // period 1 + 0x555_5555, 0xAAA_AAAA, // period 2 + 0x333_3333, 0x666_6666, 0xCCC_CCCC, 0x999_9999, // period 4 + ]; + let mut structural = std::collections::BTreeSet::new(); + for &c in &patterns { + for &d in &patterns { + structural.insert(key_from_cd(c, d)); + } + } + assert_eq!(structural.len(), 64); + + let listed: std::collections::BTreeSet = WEAK + .iter() + .chain(SEMI_WEAK.iter()) + .chain(POSSIBLY_WEAK.iter()) + .map(|k| k & PARITY_MASK) + .collect(); + assert_eq!(listed.len(), 64, "the spec's 64 keys are distinct once parity is ignored"); + assert_eq!(listed, structural); + + // The listed keys really do carry odd parity, as Sec 3.3.2 says. + for k in WEAK.iter().chain(SEMI_WEAK.iter()).chain(POSSIBLY_WEAK.iter()) { + for b in k.to_be_bytes() { + assert_eq!(b.count_ones() % 2, 1, "{k:016x} should have odd parity"); + } + } +} + +#[test] +fn a_key_one_bit_away_from_weak_is_accepted() { + // The structural test must not over-reach: flipping one non-parity bit of a weak key breaks + // its period and must give an ordinary, accepted key. + for &weak in WEAK.iter().chain(SEMI_WEAK.iter()).chain(POSSIBLY_WEAK.iter()) { + let nearly = weak ^ (1u64 << 62); // key bit 2, not a parity bit + let mut k = GOOD; + k[1] = nearly; + assert!( + TDESInternal::new(&bundle(k)).is_ok(), + "{nearly:016x} should not be treated as weak" + ); + } +} + +#[test] +fn repeated_component_keys_are_rejected() { + let [a, b, c] = GOOD; + // Key1 = Key2 and Key2 = Key3 collapse to single DES; Key1 = Key3 is two-key TDEA. + assert!(is_weak_key_error(TDESInternal::new(&bundle([a, a, c]))), "Key1 = Key2"); + assert!(is_weak_key_error(TDESInternal::new(&bundle([a, b, b]))), "Key2 = Key3"); + assert!(is_weak_key_error(TDESInternal::new(&bundle([a, b, a]))), "Key1 = Key3 (two-key TDEA)"); + assert!(is_weak_key_error(TDESInternal::new(&bundle([a, a, a]))), "three identical keys"); + // Differing only in parity is the same key to the engine, so it is still a repeat. + assert!( + is_weak_key_error(TDESInternal::new(&bundle([a, a ^ !PARITY_MASK, c]))), + "parity-only difference" + ); +} + +#[test] +fn parity_bits_do_not_change_the_cipher() { + let good = TDESInternal::new(&bundle(GOOD)).unwrap(); + let flipped = TDESInternal::new(&bundle([ + GOOD[0] ^ !PARITY_MASK, + GOOD[1] ^ 0x0000_0000_0000_0001, + GOOD[2] ^ 0x0100_0100_0100_0100, + ])) + .unwrap(); + for i in 0..64u8 { + let block = [i; BLOCK_LEN]; + let mut a = block; + let mut b = block; + good.encrypt_block(&mut a); + flipped.encrypt_block(&mut b); + assert_eq!(a, b); + assert_ne!(a, block); + } +} + +#[test] +fn a_non_parity_bit_does_change_the_cipher() { + // The complement of the test above: every one of the 56 x 3 effective key bits matters. + let good = TDESInternal::new(&bundle(GOOD)).unwrap(); + let block = [0x5Au8; BLOCK_LEN]; + let mut reference = block; + good.encrypt_block(&mut reference); + for position in 0..3 { + for bit in 0..64u32 { + if bit % 8 == 0 { + // A parity bit: bits 8, 16, ..., 64 in spec numbering are word bits 56, 48, .., 0. + continue; + } + let mut k = GOOD; + k[position] ^= 1u64 << bit; + let mut out = block; + TDESInternal::new(&bundle(k)).unwrap().encrypt_block(&mut out); + assert_ne!(out, reference, "key {} bit {bit} had no effect", position + 1); + } + } +} + +#[test] +fn the_key_material_checks() { + let mut bytes = [0u8; KEY_LEN]; + for (i, key) in GOOD.iter().enumerate() { + bytes[8 * i..8 * i + 8].copy_from_slice(&key.to_be_bytes()); + } + + // Wrong type. + let mac_key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::MACKey).unwrap(); + assert!(matches!( + TDESInternal::new(&mac_key), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + // Too weak a strength: 3TDEA is 112 bits (SP 800-57 Part 1 Rev 5 Table 2), and that is the + // floor. + let mut key = bundle(GOOD); + do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::None)).unwrap(); + assert!(matches!( + TDESInternal::new(&key), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) + )); + do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::_112bit)) + .unwrap(); + assert!(TDESInternal::new(&key).is_ok()); + + // An all-zero buffer is tagged Zeroized by KeyMaterial and rejected on type. + let zero = + KeyMaterial::::from_bytes_as_type(&[0u8; KEY_LEN], KeyType::SymmetricCipherKey) + .unwrap(); + assert!(matches!( + TDESInternal::new(&zero), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); +} + +#[test] +fn debug_output_names_the_algorithm_only() { + let tdes = TDESInternal::new(&bundle(GOOD)).unwrap(); + assert_eq!(format!("{tdes:?}"), "TDES"); +} + +// ---- two-key TDEA ----------------------------------------------------------------------------- + +fn bundle2(k: [u64; 2]) -> KeyMaterial { + let mut bytes = [0u8; KEY_LEN_2KEY]; + for (i, key) in k.iter().enumerate() { + bytes[8 * i..8 * i + 8].copy_from_slice(&key.to_be_bytes()); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn is_weak_key_error2(result: Result) -> bool { + matches!(result, Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::WeakKey(_)))) +} + +#[test] +fn two_key_bundle_is_the_three_key_bundle_with_key3_equal_to_key1() { + // Sec 3.1: 2TDEA is `Key3 = Key1`. `TDES` refuses that bundle; `TDES2Key` computes it. Check + // against a three-key engine built by hand from the same DEA keys is impossible (TDESInternal + // rejects it), so check the definition directly: F_K1(I_K2(F_K1(d))) via the single-DES + // identity F_K1(I_K1(x)) = x -- encrypting with (K1, K2) then decrypting with (K1, K2) + // round-trips, and the result differs from three-key TDEA with an unrelated third key. + let [a, b, c] = GOOD; + let two = TDES2Internal::new(&bundle2([a, b])).unwrap(); + let three = TDESInternal::new(&bundle([a, b, c])).unwrap(); + let block = [0x5Au8; BLOCK_LEN]; + let mut x = block; + two.encrypt_block(&mut x); + assert_ne!(x, block); + let mut y = block; + three.encrypt_block(&mut y); + assert_ne!(x, y, "a different third key gives a different permutation"); + two.decrypt_block(&mut x); + assert_eq!(x, block); +} + +#[test] +fn two_key_repeated_or_weak_components_are_rejected() { + let [a, b, _] = GOOD; + assert!(is_weak_key_error2(TDES2Internal::new(&bundle2([a, a]))), "Key1 = Key2 is single DES"); + assert!( + is_weak_key_error2(TDES2Internal::new(&bundle2([a, a ^ !PARITY_MASK]))), + "parity-only difference" + ); + for &weak in WEAK.iter().chain(SEMI_WEAK.iter()).chain(POSSIBLY_WEAK.iter()) { + assert!(is_weak_key_error2(TDES2Internal::new(&bundle2([weak, b]))), "{weak:016x} as Key1"); + assert!(is_weak_key_error2(TDES2Internal::new(&bundle2([a, weak]))), "{weak:016x} as Key2"); + } + assert!(TDES2Internal::new(&bundle2([a, b])).is_ok()); +} + +#[test] +fn two_key_material_checks_and_metadata() { + let [a, b, _] = GOOD; + let mut bytes = [0u8; KEY_LEN_2KEY]; + bytes[..8].copy_from_slice(&a.to_be_bytes()); + bytes[8..].copy_from_slice(&b.to_be_bytes()); + let mac_key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::MACKey).unwrap(); + assert!(matches!( + TDES2Internal::new(&mac_key), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + // 2TDEA is rated at most 80 bits (SP 800-57 Part 1 Rev 5 Table 2), below the lowest level the + // library models, so no strength is too low for it. + let mut key = bundle2([a, b]); + do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::None)).unwrap(); + assert!(TDES2Internal::new(&key).is_ok()); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!(::ALG_NAME, "TDES-2KEY"); + assert_eq!(format!("{:?}", TDES2Internal::new(&bundle2([a, b])).unwrap()), "TDES-2KEY"); +} diff --git a/crypto/tdes/tests/mode_key_tests.rs b/crypto/tdes/tests/mode_key_tests.rs new file mode 100644 index 000000000..5dd5cd08c --- /dev/null +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -0,0 +1,263 @@ +//! The TDEA keys through the shared conformance suites: construction policy for every mode a key +//! can carry, then the block, symmetric and stream suites on the three-key key, and the one thing +//! a two-key key can do, decrypt. + +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8, CMAC, CTR, ModeNames, TKW}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{BlockPermutation, EcbDecryptionKey, ElectronicCodeBook, Mode}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, StreamCipherDecryptor, StreamCipherEncryptor, +}; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_rng::{FixedSeedRNG, HashDRBG_SHA512}; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +/// The CTR mode at the alias's split: a 6-byte nonce on the 8-byte block. +type Ctr6 = CTR; + +fn material() -> KeyMaterial { + // Distinct, non-weak components at either bundle length. + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +fn key3>() -> TDES3Key { + TDES3Key::from_key_material(&material::()).expect("a valid key bundle") +} + +fn key2>() -> TDES2Key { + TDES2Key::from_key_material(&material::()).expect("a valid two-key bundle") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +fn construction_suite(name: &'static str, name_2key: &'static str) +where + M: Mode + Mode, +{ + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::>(name, SecurityStrength::_112bit); + framework.test::>(name_2key, SecurityStrength::None); +} + +#[test] +fn every_mode_key_enforces_the_material_policy() { + construction_suite::>(TDESInternal::CBC_ALG_NAME, TDES2Internal::CBC_ALG_NAME); + construction_suite::>(TDESInternal::CBC_ALG_NAME, TDES2Internal::CBC_ALG_NAME); + construction_suite::>(TDESInternal::CBC_ALG_NAME, TDES2Internal::CBC_ALG_NAME); + construction_suite::>(TDESInternal::ECB_ALG_NAME, TDES2Internal::ECB_ALG_NAME); + construction_suite::>(TDESInternal::ECB_ALG_NAME, TDES2Internal::ECB_ALG_NAME); + construction_suite::(TDESInternal::CFB_ALG_NAME, TDES2Internal::CFB_ALG_NAME); + construction_suite::(TDESInternal::CFB8_ALG_NAME, TDES2Internal::CFB8_ALG_NAME); + construction_suite::(TDESInternal::CTR_ALG_NAME, TDES2Internal::CTR_ALG_NAME); + construction_suite::(TDESInternal::KW_ALG_NAME, TDES2Internal::KW_ALG_NAME); +} + +/// The key-wrap suite over the three-key bundle: round trips, the length helpers, the error +/// cases and tamper rejection, at the shortest plaintext and two longer ones. The answers are +/// CAVP's, in `tkw_cavp_tests.rs`; the two-key bundle unwraps only, in `tkw_reference_tests.rs`. +#[test] +fn tkw_keys_pass_the_key_wrap_framework() { + let tf = TestFrameworkKeyWrap::new(); + tf.test::>(); + tf.test::>(); + tf.test::>(); +} + +#[test] +fn cbc_blocks_key_passes_the_block_cipher_framework() { + TestFrameworkBlockCipher::new() + .test_key::(&key3::>(), &mut rng()); +} + +#[test] +fn ecb_blocks_key_passes_the_block_cipher_framework() { + TestFrameworkBlockCipher::new().test_ecb_key::(&key3::>()); +} + +#[test] +fn padded_keys_pass_the_symmetric_cipher_framework() { + let framework = TestFrameworkSymmetricCipher::new(); + framework.test_key::(&key3::>(), &mut rng()); + framework.test_key::(&key3::>(), &mut rng()); +} + +#[test] +fn no_padding_keys_pass_the_symmetric_cipher_framework() { + let mut framework = TestFrameworkSymmetricCipher::new(); + framework.required_alignment = BLOCK_LEN; + framework.test_key::(&key3::>(), &mut rng()); + framework.test_key::(&key3::>(), &mut rng()); +} + +#[test] +fn stream_mode_keys_pass_the_stream_cipher_framework() { + let framework = TestFrameworkStreamCipher::new(); + framework.test_key::(&key3::(), &mut rng()); + framework.test_key::(&key3::(), &mut rng()); + framework.test_key::(&key3::(), &mut rng()); +} + +/// A two-key key hands out decryptors for every mode, and they decrypt what the raw two-key +/// permutation encrypted. (Its encryptors do not compile; see `TDES2Internal`.) The CAVP MMT2 +/// vectors in `cavp_mmt_tests.rs` are the real check; this pins the wiring. +#[test] +fn two_key_keys_decrypt() { + let perm = TDES2Internal::new(&material::()).unwrap(); + let plaintext = [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN]]; + + // ECB: block by block. + let mut ct = plaintext; + for block in ct.iter_mut() { + perm.encrypt_block(block); + } + let mut back = ct; + key2::>().create_decryptor().do_decrypt_blocks_inplace(&mut back).unwrap(); + assert_eq!(back, plaintext); + + // CBC: `C_j = CIPH(P_j ^ C_{j-1})`, done by hand. + let iv = [0xA5u8; BLOCK_LEN]; + let mut ct = plaintext; + let mut prev = iv; + for block in ct.iter_mut() { + for (b, p) in block.iter_mut().zip(prev) { + *b ^= p; + } + perm.encrypt_block(block); + prev = *block; + } + let mut back = ct; + key2::>().create_decryptor(&iv).do_decrypt_blocks_inplace(&mut back).unwrap(); + assert_eq!(back, plaintext); + + // CFB64: `C_j = P_j ^ CIPH(C_{j-1})`, so decryption only needs the forward function. + let mut ct = plaintext; + let mut prev = iv; + for block in ct.iter_mut() { + let mut keystream = prev; + perm.encrypt_block(&mut keystream); + for (b, k) in block.iter_mut().zip(keystream) { + *b ^= k; + } + prev = *block; + } + let mut back = ct.as_flattened().to_vec(); + key2::().create_decryptor(&iv).do_decrypt_inplace(&mut back).unwrap(); + assert_eq!(back, plaintext.as_flattened()); + + // CTR: `C_j = P_j ^ CIPH(nonce || j)`, counter from zero. + let nonce = [1u8, 2, 3, 4, 5, 6]; + let mut ct = plaintext; + for (j, block) in ct.iter_mut().enumerate() { + let mut counter_block = [0u8; BLOCK_LEN]; + counter_block[..CTR_NONCE_LEN].copy_from_slice(&nonce); + counter_block[CTR_NONCE_LEN..].copy_from_slice(&(j as u16).to_be_bytes()); + perm.encrypt_block(&mut counter_block); + for (b, k) in block.iter_mut().zip(counter_block) { + *b ^= k; + } + } + let mut back = ct.as_flattened().to_vec(); + key2::().create_decryptor(&nonce).do_decrypt_inplace(&mut back).unwrap(); + assert_eq!(back, plaintext.as_flattened()); + + // ...and the symmetric one-shot on a padded two-key key opens a PKCS7-padded message. + let padded = *b"hello\x03\x03\x03"; + let mut ct = padded; + perm.encrypt_block(&mut ct); + assert_eq!(key2::>().decrypt(&[], &ct).unwrap(), b"hello"); +} + +/// `material::()` with one bit of its last byte flipped. +fn material_flipped(bit: u8) -> KeyMaterial { + let mut bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + bytes[N - 1] ^= bit; + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// Equality is over the expanded schedule, so it follows the material as the engine sees it: the +/// same bytes compare equal, as does a clone, a flipped key bit does not, and a flipped parity bit +/// (the last bit of a key byte, which PC-1 drops) still does. Keys have no `Debug`, hence +/// `assert!` over `==`. +#[test] +fn keys_compare_equal_exactly_when_their_material_does_to_the_engine() { + let key = key3::(); + assert!(key == key.clone()); + assert!(key == key3::()); + assert!( + key != TDES3Key::::from_key_material(&material_flipped::(0x02)).unwrap() + ); + assert!( + key == TDES3Key::::from_key_material(&material_flipped::(0x01)).unwrap() + ); + + let key = key2::(); + assert!(key == key2::()); + assert!( + key != TDES2Key::::from_key_material(&material_flipped::(0x02)) + .unwrap() + ); +} + +/// A key under a MAC mode has no role here, and its `KEY_TYPE` is the `MACKey` the mode asks +/// for: cipher material is refused for its type, and MAC material by the engine, so the +/// constructor never contradicts the constant and `generate` fails the same way. +#[test] +fn a_mac_mode_key_refuses_every_material_consistently() { + fn refused(r: Result) { + assert!(matches!( + r, + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + } + fn mac_material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::MACKey).unwrap() + } + type Mac3 = TDES3Key>; + type Mac2 = TDES2Key>; + assert_eq!(Mac3::KEY_TYPE, KeyType::MACKey); + assert_eq!(Mac2::KEY_TYPE, KeyType::MACKey); + refused(Mac3::from_key_material(&material::())); + refused(Mac3::from_key_material(&mac_material::())); + refused(Mac3::generate(&mut rng())); + refused(Mac2::from_key_material(&material::())); + refused(Mac2::from_key_material(&mac_material::())); + refused(Mac2::generate(&mut rng())); +} + +/// The counter limit, through a key: under `CTR<7, 8>` the counter is one byte, so the 256th +/// block is the last the mode will produce, and the byte after it is refused rather than +/// repeating the keystream (`bouncycastle_cipher::modes::Ctr`'s rule, pinned here on a TDES key +/// so that the key-object path reaches it). +#[test] +fn a_ctr_key_refuses_the_byte_after_the_counter_wraps() { + let key = key3::>(); + let nonce = [0x5au8; 7]; + let (mut enc, got) = key.create_encryptor(&mut FixedSeedRNG::new(nonce)).unwrap(); + assert_eq!(got, nonce); + + let mut data = vec![0u8; 256 * BLOCK_LEN]; + enc.do_encrypt_inplace(&mut data).expect("256 blocks fill a one-byte counter exactly"); + + let mut one = [0u8; 1]; + assert!( + matches!(enc.do_encrypt_inplace(&mut one), Err(SymmetricCipherError::DataLimitExceeded)), + "the 2049th byte must be refused, not encrypted under a repeated counter block" + ); +} diff --git a/crypto/tdes/tests/padded_alias_tests.rs b/crypto/tdes/tests/padded_alias_tests.rs new file mode 100644 index 000000000..96021e943 --- /dev/null +++ b/crypto/tdes/tests/padded_alias_tests.rs @@ -0,0 +1,153 @@ +//! The padded CBC and ECB modes through a three-key TDEA key. +//! +//! The aliases are only type aliases, so what is worth testing is that both parameters actually +//! select: the direction picks the encryptor or the decryptor, and the padding scheme changes the +//! behaviour rather than being decorative. The mode and the padding layer are tested in +//! `bouncycastle-cipher`; this checks the wiring between them and the 8-byte block, through the +//! key that hands the aliases out. Which type each alias resolves to is pinned in +//! `alg_name_tests.rs`. + +use bouncycastle_cipher::modes::CBC; +use bouncycastle_cipher::modes::hazmat::ecb::ECB; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_tdes::hazmat::TDESInternal; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; + +fn material() -> KeyMaterial { + // Three distinct component keys, none of them weak. + let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid key") +} + +fn key_for>() -> TDES3Key { + TDES3Key::from_key_material(&material()).expect("a valid key bundle") +} + +fn rng() -> HashDRBG_SHA512 { + HashDRBG_SHA512::new_from_os() +} + +#[test] +fn round_trips_at_every_alignment() { + fn check(name: &str) + where + M: Mode, + TDES3Key: SymmetricEncryptionKey + + SymmetricDecryptionKey, + { + let key = key_for::(); + for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 63, 64] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (iv, ciphertext) = key.encrypt(&mut rng(), &plaintext).expect("encryption"); + assert_eq!(iv.len(), IV_LEN); + + // PKCS#7 always adds at least one byte, and rounds up to a whole 8-byte block. + assert_eq!( + ciphertext.len(), + (len / BLOCK_LEN + 1) * BLOCK_LEN, + "{name}, len {len}: PKCS7 pads up to the next whole block" + ); + + let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "{name}, len {len}: round trip"); + } + } + + check::>("CBC"); + check::<0, ECB>("ECB"); +} + +#[test] +fn the_padding_parameter_selects_the_scheme() { + fn check(name: &str) + where + Pkcs7Mode: Mode, + NoPadMode: Mode, + TDES3Key: SymmetricEncryptionKey, + TDES3Key: SymmetricEncryptionKey, + { + let pkcs7_key = key_for::(); + let nopad_key = key_for::(); + + // A whole block: both schemes accept it, and they disagree about the length. + let aligned = [0x5Au8; BLOCK_LEN]; + let (_, pkcs7) = pkcs7_key.encrypt(&mut rng(), &aligned).expect("PKCS7 accepts it"); + let (_, nopad) = nopad_key.encrypt(&mut rng(), &aligned).expect("NoPadding accepts it"); + assert_eq!(pkcs7.len(), 16, "{name}: PKCS7 adds a whole block of padding to aligned data"); + assert_eq!(nopad.len(), 8, "{name}: NoPadding adds nothing"); + + // Five bytes: PKCS7 pads it, NoPadding refuses rather than silently padding. + let unaligned = b"hello"; + assert!(pkcs7_key.encrypt(&mut rng(), unaligned).is_ok(), "{name}: PKCS7 pads it"); + assert!( + nopad_key.encrypt(&mut rng(), unaligned).is_err(), + "{name}: NoPadding must refuse a message that is not a whole number of blocks" + ); + } + + check::, CBC>("CBC"); + check::<0, ECB, ECB>("ECB"); +} + +#[test] +fn the_two_schemes_are_not_interchangeable() { + let aligned = [0x5Au8; BLOCK_LEN]; + let (iv, pkcs7) = key_for::>().encrypt(&mut rng(), &aligned).expect("encryption"); + + // NoPadding will hand back the padded block as if it were data, so it "succeeds" with the + // wrong answer -- exactly the silent mismatch the type parameter is there to prevent. + let as_nopad = key_for::>() + .decrypt(&iv, &pkcs7) + .expect("NoPadding cannot tell that the trailing block is padding"); + assert_ne!(as_nopad, aligned, "the recovered data must not match the original"); + assert_eq!(as_nopad.len(), 16, "it keeps the padding block as data"); + + // ...and the matching scheme gets it right. + let correct = key_for::>().decrypt(&iv, &pkcs7).expect("decryption"); + assert_eq!(correct, aligned); +} + +#[test] +fn each_cbc_encryption_gets_a_fresh_iv() { + let key = key_for::>(); + let mut rng = rng(); + let plaintext = [0x77u8; 16]; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let (iv, ct) = key.encrypt(&mut rng, &plaintext).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions"); + let back = key.decrypt(&iv, &ct).unwrap(); + assert_eq!(back, plaintext); + } +} + +#[test] +fn ecb_has_no_iv() { + let key = key_for::>(); + let (no_iv, ciphertext) = key.encrypt(&mut rng(), b"hello").expect("encryption"); + assert_eq!(no_iv, [0u8; 0], "ECB has no IV, so the init data is empty"); + assert_eq!(ciphertext.len(), 8, "five bytes padded to one block, nothing prepended"); + + let recovered = key.decrypt(&no_iv, &ciphertext).unwrap(); + assert_eq!(recovered, b"hello"); +} + +#[test] +fn padding_does_not_hide_the_ecb_codebook_property() { + // Two identical blocks give two identical ciphertext blocks. + let (_, ciphertext) = key_for::>().encrypt(&mut rng(), &[0x5Au8; 16]).unwrap(); + assert_eq!(ciphertext[..8], ciphertext[8..], "ECB is a codebook, padded or not"); + + // ...and encryption is deterministic, there being no IV to vary. + let key = key_for::>(); + let (_, a) = key.encrypt(&mut rng(), b"hello").unwrap(); + let (_, b) = key.encrypt(&mut rng(), b"hello").unwrap(); + assert_eq!(a, b, "the same message encrypts the same way every time"); +} diff --git a/crypto/tdes/tests/tkw_cavp_tests.rs b/crypto/tdes/tests/tkw_cavp_tests.rs new file mode 100644 index 000000000..2e9e862f2 --- /dev/null +++ b/crypto/tdes/tests/tkw_cavp_tests.rs @@ -0,0 +1,118 @@ +//! Known-answer tests against the NIST CAVP sample files for TKW, `TKW_AE.txt` and `TKW_AD.txt` +//! from `kwtestvectors.zip` ("Key Wrap Validation System" sample vectors, CAVS 17.4), read from +//! `bc-test-data/crypto/aes_tdes_vectors/TDES` through the framework's `bc_test_data` loader; +//! without `bc-test-data` beside this repository the tests print a warning and pass. +//! +//! Each file carries five `[PLAINTEXT LENGTH = n]` sections of 100 trials, for 64, 96, 128, 160 +//! and 2048 bits. `TKW_AE` gives `K`, `P` and `C`; `TKW_AD` gives `K`, `C` and either `P` or +//! `FAIL`, 20 forgeries per section. The forward cipher function is the designated one (the +//! `_inv` files of the zip are not used). `K` is `KEY1 || KEY2 || KEY3`, 24 bytes. + +use bouncycastle_cipher::modes::TKW; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_tdes::{KEY_LEN, TDES3Key}; + +const CAVP_DIR: &str = "crypto/aes_tdes_vectors/TDES"; + +/// One trial: `plaintext` is `None` for a `FAIL` line. +#[derive(Default)] +struct Case { + section: String, + count: u32, + key: Vec, + plaintext: Option>, + ciphertext: Vec, +} + +/// Parses a CAVS sample file: `[PLAINTEXT LENGTH = n]` headers, `COUNT = n` starting a case, +/// `K`, `P` and `C` hex fields, and a bare `FAIL` line. +fn parse(text: &str) -> Vec { + let mut cases: Vec = Vec::new(); + let mut section = String::new(); + for line in text.lines().map(str::trim) { + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { + section = name.to_string(); + continue; + } + if line == "FAIL" { + let case = cases.last_mut().expect("a FAIL after a COUNT"); + assert!(case.plaintext.is_none(), "FAIL on a case that has a plaintext"); + continue; + } + let (name, value) = line.split_once(" = ").expect("`NAME = value` line"); + if name == "COUNT" { + cases.push(Case { + section: section.clone(), + count: value.parse().expect("a decimal COUNT"), + ..Default::default() + }); + continue; + } + let case = cases.last_mut().expect("a field before any COUNT"); + let bytes = hex::decode(value).expect("hex field"); + match name { + "K" => case.key = bytes, + "P" => case.plaintext = Some(bytes), + "C" => case.ciphertext = bytes, + other => panic!("unexpected field {other}"), + } + } + cases +} + +fn load(file: &str) -> Option> { + let cases = parse(&bc_test_data(CAVP_DIR, file)?); + assert_eq!(cases.len(), 500, "{file}: five sections of 100 trials"); + println!("{file}: {} cases found under bc-test-data/{CAVP_DIR}", cases.len()); + Some(cases) +} + +fn kek(case: &Case) -> TDES3Key { + let key: [u8; KEY_LEN] = case.key.as_slice().try_into().expect("a 24-byte K"); + let material = KeyMaterial::::from_bytes_as_type(&key, KeyType::SymmetricCipherKey) + .expect("a symmetric cipher key"); + TDES3Key::::from_key_material(&material).unwrap_or_else(|e| { + panic!("[{}] COUNT = {}: the CAVP bundle was refused: {e:?}", case.section, case.count) + }) +} + +#[test] +fn tkw_ae() { + let Some(cases) = load("TKW_AE.txt") else { return }; + for case in &cases { + let where_ = format!("[{}] COUNT = {}", case.section, case.count); + let plaintext = case.plaintext.as_ref().expect("an AE case has a plaintext"); + let kek = kek(case); + assert_eq!(kek.wrap(plaintext).unwrap(), case.ciphertext, "{where_}: wrap"); + assert_eq!(kek.unwrap(&case.ciphertext).unwrap(), *plaintext, "{where_}: unwrap"); + } +} + +#[test] +fn tkw_ad() { + let Some(cases) = load("TKW_AD.txt") else { return }; + let mut forgeries = 0; + for case in &cases { + let where_ = format!("[{}] COUNT = {}", case.section, case.count); + let kek = kek(case); + let result = kek.unwrap(&case.ciphertext); + match &case.plaintext { + Some(plaintext) => assert_eq!(result.unwrap(), *plaintext, "{where_}: unwrap"), + None => { + assert!( + matches!(result, Err(SymmetricCipherError::DecryptionFailed)), + "{where_}: a forgery must fail with DecryptionFailed, got {result:?}" + ); + forgeries += 1; + } + } + } + assert_eq!(forgeries, 100, "20 forgeries in each of the five sections"); +} diff --git a/crypto/tdes/tests/tkw_reference_tests.rs b/crypto/tdes/tests/tkw_reference_tests.rs new file mode 100644 index 000000000..be6f823ca --- /dev/null +++ b/crypto/tdes/tests/tkw_reference_tests.rs @@ -0,0 +1,93 @@ +//! TKW against SP 800-38F Algorithm 7 written out over the raw TDEA permutation, for both key +//! bundles. The CAVP sample files pin the three-key answers in `tkw_cavp_tests.rs`; this file +//! pins the loop itself, with the 32-bit step counter and ICV3, against a direct transcription, +//! and is the only known answer for two-key unwrapping, since no wrapper exists for a two-key +//! bundle and CAVP tests TKW with three keys only. + +use bouncycastle_cipher::modes::TKW; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// TKW-AE (Algorithm 9) as written: `S = ICV3 || P`, then TW (Algorithm 7) with its shift +/// register, `A^t = MSB32(CIPH_K(A^{t-1} || R2^{t-1})) xor [t]32`, `R_i^t = R_{i+1}^{t-1}`, +/// `R_n^t = LSB32(CIPH_K(A^{t-1} || R2^{t-1}))`, for `t = 1 .. 6(n-1)`. +fn tkw_ae_as_written>(perm: &P, plaintext: &[u8]) -> Vec { + let mut a = [0xA6u8; 4]; + let mut r: Vec<[u8; 4]> = plaintext.chunks(4).map(|c| c.try_into().unwrap()).collect(); + let n = r.len() + 1; + for t in 1..=(6 * (n - 1)) as u32 { + let mut block = [0u8; 8]; + block[..4].copy_from_slice(&a); + block[4..].copy_from_slice(&r[0]); + perm.encrypt_block(&mut block); + a.copy_from_slice(&block[..4]); + for (x, y) in a.iter_mut().zip(t.to_be_bytes()) { + *x ^= y; + } + r.rotate_left(1); + r[n - 2].copy_from_slice(&block[4..]); + } + let mut out = a.to_vec(); + out.extend(r.iter().flatten()); + out +} + +#[test] +fn three_key_tkw_matches_algorithm_9_as_written() { + let perm = TDESInternal::new(&material::()).unwrap(); + let kek = TDES3Key::::from_key_material(&material::()).unwrap(); + for len in [8usize, 12, 16, 20, 24, 256] { + let plaintext: Vec = (0..len).map(|i| (i as u8).wrapping_mul(0x9D)).collect(); + let expected = tkw_ae_as_written(&perm, &plaintext); + assert_eq!(kek.wrap(&plaintext).unwrap(), expected, "{len} bytes"); + assert_eq!(kek.unwrap(&expected).unwrap(), plaintext, "{len} bytes"); + } +} + +/// The two-key engine's forward function is not gated (only the key's wrapper is), so a +/// ciphertext can be made from it here and the key's unwrapper must recover the plaintext. +#[test] +fn two_key_tkw_unwraps_what_algorithm_9_wrapped() { + let perm = TDES2Internal::new(&material::()).unwrap(); + let kek = TDES2Key::::from_key_material(&material::()).unwrap(); + for len in [8usize, 16, 20, 256] { + let plaintext: Vec = (0..len).map(|i| (i as u8).wrapping_mul(0x9D)).collect(); + let mut ciphertext = tkw_ae_as_written(&perm, &plaintext); + assert_eq!(kek.unwrap(&ciphertext).unwrap(), plaintext, "{len} bytes"); + ciphertext[len / 2] ^= 0x01; + assert!(kek.unwrap(&ciphertext).is_err(), "{len} bytes, modified"); + } +} + +/// SP 800-38F Table 1: TKW-AD is defined on three or more semiblocks and TKW-AE on two or more. +/// The shared suite's bad lengths (0, one semiblock, one byte short) never reach the 8-byte +/// ciphertext or the 4-byte plaintext, which must be `InvalidInputLength`, not a run of the +/// algorithm ending in `DecryptionFailed`. +#[test] +fn tkw_refuses_a_two_semiblock_ciphertext_and_a_one_semiblock_plaintext() { + use bouncycastle_core::errors::SymmetricCipherError; + use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; + let kek = TDES3Key::::from_key_material(&material::()).unwrap(); + let mut out = [0u8; 16]; + assert!(matches!( + kek.create_unwrapper().unwrap_out(&[0u8; 8], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + kek.create_wrapper().wrap_out(&[0u8; 4], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + let kek2 = TDES2Key::::from_key_material(&material::()).unwrap(); + assert!(matches!( + kek2.create_unwrapper().unwrap_out(&[0u8; 8], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); +} diff --git a/dev_scripts/aes_ct_harness/src/lib.rs b/dev_scripts/aes_ct_harness/src/lib.rs index 8f1670d47..6d76c1873 100644 --- a/dev_scripts/aes_ct_harness/src/lib.rs +++ b/dev_scripts/aes_ct_harness/src/lib.rs @@ -1,14 +1,22 @@ //! The two bodies of `PlaneWord::splat` from `crypto/aes/src/bitslice.rs`, with the barrier the //! crate applies off x86-64 and aarch64 and without it, each out of line so that it lands in the -//! assembly as one symbol. `../check_aes_ct_asm.sh` builds this for the Cortex-M0 and RISC-V 32I -//! targets and checks that the barriered bodies hold no multiply, call or branch, and that the -//! plain ones do show the multiply LLVM folds the shifts and ORs into, which is what the check -//! exists to catch. Keep the bodies the same as the crate's. +//! assembly as one symbol; and the TDES round function, whose `spread` carries the same guard, +//! compiled from the crate's own `des.rs` and `sbox.rs` (included by path, unmodified), with a +//! plain-shift `spread` beside it as the control. `../check_aes_ct_asm.sh` builds this for the +//! Cortex-M0 and RISC-V 32I targets and checks that the barriered bodies hold no multiply, call +//! or branch, and that the plain ones do show the multiply LLVM folds the shifts and ORs into, +//! which is what the check exists to catch. Keep the `splat` bodies the same as the crate's. #![no_std] #![forbid(unsafe_code)] +#![allow(dead_code)] use bouncycastle_utils::ct; +#[path = "../../../crypto/tdes/src/des.rs"] +mod des; +#[path = "../../../crypto/tdes/src/sbox.rs"] +mod sbox; + /// `u32::splat` as the crate compiles it off the fixed-latency targets. #[inline(never)] pub fn splat32_barrier(pattern: u16) -> u32 { @@ -36,3 +44,18 @@ pub fn splat64_plain(pattern: u16) -> u64 { let x = u64::from(pattern); x | (x << 16) | (x << 32) | (x << 48) } + +/// The TDES round function as the crate compiles it: sixteen DEA rounds over a schedule, with +/// `spread` going through the barrier off the fixed-latency targets. +#[inline(never)] +pub fn tdes_rounds(l: u32, r: u32, sk: &des::Subkeys) -> (u32, u32) { + des::rounds(l, r, sk, false) +} + +/// The crate's `spread` without the barrier: the positive control, which LLVM folds into a +/// multiply by 15 on Cortex-M0. +#[inline(never)] +pub fn spread_plain(v: u32) -> u32 { + let v = v & 0x1111_1111; + v | (v << 1) | (v << 2) | (v << 3) +} diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh index 4804d6c86..f845e9cdb 100755 --- a/dev_scripts/check_aes_ct_asm.sh +++ b/dev_scripts/check_aes_ct_asm.sh @@ -1,10 +1,15 @@ #!/usr/bin/env bash -# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) -# compiles to no multiply, call or branch on the targets whose multiply is not fixed-latency. +# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) and +# the TDES round function (`spread` in crypto/tdes/src/des.rs) compile to no multiply, call or +# branch on the targets whose multiply is not fixed-latency. # Builds dev_scripts/aes_ct_harness with the nightly toolchain and `-Zbuild-std=core` (the -# targets have no shipped `core`), emits the assembly and greps the four `splat*` bodies: the -# barriered pair must be clean, and the plain pair must show the multiply the barrier prevents, -# so that a grep that matches nothing is noticed. Targets may be given as arguments. +# targets have no shipped `core`), emits the assembly and greps the checked bodies: the barriered +# `splat` pair and the TDES round function must be clean, every checked body must be found (an +# empty body would pass vacuously), and the unbarriered controls must show the multiply the +# barrier prevents, so that a grep that matches nothing is noticed -- each control on its own +# where the target has a multiplier (thumbv6m), pooled where it has none (rv32i lowers a small +# constant multiply to shifts, and only the 64-bit one calls `__muldi3`). Targets may be given as +# arguments. # # rustup toolchain install nightly --profile minimal --component rust-src # ./dev_scripts/check_aes_ct_asm.sh # thumbv6m-none-eabi riscv32i-unknown-none-elf @@ -48,20 +53,48 @@ for t in $TARGETS; do exit 2 fi - for f in splat32_barrier splat64_barrier; do - hits=$(body "$asm" "$f" | grep -cE "$bad" || true) + # splat is straight-line code, so any call or branch in it is a fault; the TDES round function + # has a loop counter and a buffer clear, which are not secret-dependent, so for it only a + # multiply counts. + for spec in splat32_barrier:all splat64_barrier:all tdes_rounds:mul; do + f=${spec%%:*}; kind=${spec##*:} + if [ "$kind" = all ]; then pat=$bad; what="multiply, call or branch"; else pat=$mul; what="multiply"; fi + hits=$(body "$asm" "$f" | grep -cE "$pat" || true) n=$(body "$asm" "$f" | wc -l) - if [ "$hits" -ne 0 ]; then - echo "FAIL $t $f: $hits multiply/call/branch line(s) in $n instructions" - body "$asm" "$f" | grep -E "$bad" + if [ "$n" -eq 0 ]; then + echo "FAIL $t $f: not found in the assembly (renamed or inlined?), nothing was checked" + status=1 + elif [ "$hits" -ne 0 ]; then + echo "FAIL $t $f: $hits $what line(s) in $n instructions" + body "$asm" "$f" | grep -E "$pat" status=1 else - echo "ok $t $f: no multiply, call or branch in $n instructions" + echo "ok $t $f: no $what in $n instructions" + fi + done + # The controls: the same code with the barrier off must show a multiply, or the grep proves + # nothing. Each control on its own where the target has a multiplier; pooled where it has none. + control=0 + for f in splat32_plain splat64_plain spread_plain; do + n=$(body "$asm" "$f" | wc -l) + hits=$(body "$asm" "$f" | grep -cE "$mul" || true) + if [ "$n" -eq 0 ]; then + echo "FAIL $t $f: control not found in the assembly" + status=1 fi + case "$t" in + thumb*|arm*) + if [ "$hits" -eq 0 ]; then + echo "FAIL $t $f: the unbarriered body shows no multiply, so the grep proves nothing" + status=1 + else + echo "ok $t $f: control shows the multiply ($hits line(s))" + fi ;; + esac + control=$((control + hits)) done - control=$(for f in splat32_plain splat64_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) if [ "$control" -eq 0 ]; then - echo "FAIL $t: the unbarriered bodies show no multiply, so the grep proves nothing" + echo "FAIL $t: no unbarriered body shows a multiply, so the grep proves nothing" status=1 else echo "ok $t control: the unbarriered bodies show the multiply ($control line(s))" diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index b6b045f00..98fa02c4c 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -43,6 +43,10 @@ path = "src/bench_sm2_mem_usage.rs" name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" +[[bin]] +name = "bench_tdes_mem_usage" +path = "src/bench_tdes_mem_usage.rs" + [[bin]] name = "bench_sm3_mem_usage" path = "src/bench_sm3_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs new file mode 100644 index 000000000..2bcc6e404 --- /dev/null +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -0,0 +1,244 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null +//! +//! ms_print massif.out.* +//! ``` +//! +//! or, shoved all into one line: +//! +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! Note: print!() is used to force the compiler not to optimize away the actual code. +//! The important stuff for benchmarking goes to stderr so the junk can be piped to /dev/null. +//! +//! Main is at the bottom, and controls which of these actually runs -- measure one at a time, +//! because massif reports the peak across the whole process. +//! +//! # What to expect, and why massif cannot see it +//! +//! Like AES, TDEA has no interesting stack profile: a call needs a few hundred bytes -- the six +//! input planes, the 128-byte working array of the S-box layer's ANF evaluation and a few words +//! of block state -- which is below what massif resolves, since the process's own start-up +//! reaches about 7.7 kB of stack before `main` runs. So the massif number is the floor, not a +//! measurement. +//! +//! The numbers that do mean something come from the compiler's frame-layout remarks, as for +//! `bench_aes_mem_usage`: build this binary with +//! `RUSTFLAGS="-C remark=prologepilog -C remark=stack-frame-layout"`, with `main` calling every +//! bench so none is dead-stripped, and sum the deepest chain of frames below each `measure` +//! closure, each with its 8-byte return address. The closure is not always the whole cost: +//! `objdump -d` shows the key-expansion closures calling the engine's `new` out of line, which +//! calls the schedule's `expand`, and the one-block closures calling `encrypt_block` / +//! `decrypt_block`; the two- and four-block entry points are inlined into their closures. The +//! engine is built in an `#[inline(never)]` helper so its frame is a sibling of the operation's, +//! and the operation runs in a non-inlined, non-returning closure so nothing crosses back across +//! the boundary. The persistent cost, the packed schedules, is what `print_struct_sizes` prints. +//! A table-driven Triple DES carries 2 KiB of combined S/P tables on top; this one carries none. +//! The two-key engine is decryption-only, so only its decrypting entry points are measured. +//! +//! With rustc 1.97 nightly on x86-64 the frames are: closures 792 B for `TDESInternal::new` and +//! 536 B for `TDES2Internal::new`, and 40, 72 and 104 B for the one-, two- and four-block entry +//! points of either engine; the engines' `new` 440 B and 312 B; `expand` 48 B; the one-block +//! permutation methods 48 B. The crate docs tabulate the chains: `new` 1304 B and 920 B, the +//! one-block calls 104 B, the two- and four-block calls 80 B and 112 B. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each engine, i.e. the persistent cost of holding the schedules. +fn print_struct_sizes() { + use core::mem::size_of; + + // Three DEA keys, sixteen round keys each, two words per round key: 3 * 16 * 2 * 4 = 384. + println!("size_of: {}", size_of::()); + // Two DEA keys: 2 * 16 * 2 * 4 = 256. + println!("size_of: {}", size_of::()); +} + +/// Runs the operation in its own frame. Returns nothing, so no result crosses the boundary and +/// the closure's frame is exactly the operation's. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// The NIST CAVP TECBMMT3.rsp `[ENCRYPT]` COUNT = 0 bundle, so the engine under measurement is a +/// known one; the two-key bundle is its first two components. +const KEY_3: [u8; KEY_LEN] = [ + 0xe9, 0x7c, 0x83, 0x13, 0xba, 0x26, 0x5d, 0x43, 0x25, 0x4c, 0xbf, 0x9e, 0x8f, 0x7c, 0x2a, 0xa8, + 0xa7, 0x54, 0xd6, 0x5e, 0x8a, 0xe9, 0x97, 0xe3, +]; + +/// Wraps a hard-coded key. `#[inline(never)]` so the wrapping is a sibling frame of whatever +/// uses the key, not part of it. +#[inline(never)] +fn key(bytes: &[u8; N]) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey).unwrap() +} + +/// Expands the key into an engine, in its own frame, so the expansion's temporaries are popped +/// before an operation on the engine runs. +#[inline(never)] +fn load_tdes() -> TDESInternal { + TDESInternal::new(&key(&KEY_3)).unwrap() +} +#[inline(never)] +fn load_tdes2() -> TDES2Internal { + let two: [u8; KEY_LEN_2KEY] = KEY_3[..KEY_LEN_2KEY].try_into().unwrap(); + TDES2Internal::new(&key(&two)).unwrap() +} + +// ---- key expansion: the expansion is the operation, so it runs inside `measure` ------------ + +fn bench_tdes_key_expansion() { + eprintln!("TDESInternal::new (key expansion)"); + let key = key(&KEY_3); + measure(|| { + let tdes = TDESInternal::new(&key).unwrap(); + print!("{tdes:?}"); + }); +} + +fn bench_tdes2_key_expansion() { + eprintln!("TDES2Internal::new (key expansion)"); + let two: [u8; KEY_LEN_2KEY] = KEY_3[..KEY_LEN_2KEY].try_into().unwrap(); + let key = key(&two); + measure(|| { + let tdes = TDES2Internal::new(&key).unwrap(); + print!("{tdes:?}"); + }); +} + +// ---- the block entry points ----------------------------------------------------------------- +// +// The engine has no natural unit larger than one block, so the pair and four-block methods are +// single-block loops over the same state. The blocks are stack arrays in the closure, never a +// heap buffer, so massif's --heap=no does not hide them. + +fn bench_tdes_encrypt_block() { + eprintln!("TDESInternal::encrypt_block"); + let tdes = load_tdes(); + measure(|| { + let mut block = [0x11u8; BLOCK_LEN]; + tdes.encrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_tdes_decrypt_block() { + eprintln!("TDESInternal::decrypt_block"); + let tdes = load_tdes(); + measure(|| { + let mut block = [0x11u8; BLOCK_LEN]; + tdes.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_tdes_encrypt_2blocks() { + eprintln!("TDESInternal::encrypt_2blocks"); + let tdes = load_tdes(); + measure(|| { + let mut blocks = [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN]]; + tdes.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_tdes_decrypt_2blocks() { + eprintln!("TDESInternal::decrypt_2blocks"); + let tdes = load_tdes(); + measure(|| { + let mut blocks = [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN]]; + tdes.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_tdes_encrypt_4blocks() { + eprintln!("TDESInternal::encrypt_4blocks"); + let tdes = load_tdes(); + measure(|| { + let mut blocks = + [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN], [0x33u8; BLOCK_LEN], [0x44u8; BLOCK_LEN]]; + tdes.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_tdes_decrypt_4blocks() { + eprintln!("TDESInternal::decrypt_4blocks"); + let tdes = load_tdes(); + measure(|| { + let mut blocks = + [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN], [0x33u8; BLOCK_LEN], [0x44u8; BLOCK_LEN]]; + tdes.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_tdes2_decrypt_block() { + eprintln!("TDES2Internal::decrypt_block"); + let tdes = load_tdes2(); + measure(|| { + let mut block = [0x11u8; BLOCK_LEN]; + tdes.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_tdes2_decrypt_2blocks() { + eprintln!("TDES2Internal::decrypt_2blocks"); + let tdes = load_tdes2(); + measure(|| { + let mut blocks = [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN]]; + tdes.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_tdes2_decrypt_4blocks() { + eprintln!("TDES2Internal::decrypt_4blocks"); + let tdes = load_tdes2(); + measure(|| { + let mut blocks = + [[0x11u8; BLOCK_LEN], [0x22u8; BLOCK_LEN], [0x33u8; BLOCK_LEN], [0x44u8; BLOCK_LEN]]; + tdes.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_tdes_key_expansion() + // bench_tdes2_key_expansion() + // bench_tdes_encrypt_block() + // bench_tdes_decrypt_block() + // bench_tdes_encrypt_2blocks() + // bench_tdes_decrypt_2blocks() + // bench_tdes_encrypt_4blocks() + // bench_tdes_decrypt_4blocks() + // bench_tdes2_decrypt_block() + // bench_tdes2_decrypt_2blocks() + // bench_tdes2_decrypt_4blocks() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index c7017c9d5..a499cb97f 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -11,3 +11,4 @@ mod bench_sha2_mem_usage; mod bench_sha3_mem_usage; mod bench_sm2_mem_usage; mod bench_sm3_mem_usage; +mod bench_tdes_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index 5864f64b4..c1ed4ecb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,3 +20,4 @@ pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; pub use bouncycastle_sm2 as sm2; pub use bouncycastle_sm3 as sm3; +pub use bouncycastle_tdes as tdes;