diff --git a/Cargo.toml b/Cargo.toml index 2de6cf6ec..b629b6b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } bouncycastle-sm3 = { path = "./crypto/sm3" } +bouncycastle-tdes = { path = "./crypto/tdes" } bouncycastle-utils = { path = "./crypto/utils" } @@ -75,3 +76,4 @@ bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true bouncycastle-sm3.workspace = true +bouncycastle-tdes.workspace = true 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/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index 0c40ba5e9..2ea2756e5 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,7 +282,7 @@ 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( mut enc: E, generated_iv: Option<&[u8; INIT_DATA_LEN]>, output_hex: bool, @@ -292,6 +296,7 @@ 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 total = stream_chunks(|data| { // Cannot fail: `out` has room for every whole block of `data` plus the fewer than @@ -302,7 +307,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 +322,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 +357,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 +394,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/main.rs b/cli/src/main.rs index 289bd5b7a..e4b5ffb0a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,6 +18,16 @@ mod rng_cmd; mod sha2_cmd; mod sha3_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; use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; @@ -26,7 +36,7 @@ use crate::rng_cmd::DrbgHash; use crate::sha2_cmd::SHA2Variant; use crate::sha3_cmd::{SHA3Variant, Strength}; 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)] @@ -2359,6 +2369,379 @@ 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 is not supported. + /// + /// 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. + /// + /// 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. + /// + /// 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), 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, as raw bytes exactly as they are (no hex + /// decoding). If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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. + /// + /// 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, as raw bytes exactly as they are (no hex + /// decoding). If both iv and iv_file options are provided, the file will be used. + #[arg(long)] + iv_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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. @@ -2926,6 +3309,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..e79453290 --- /dev/null +++ b/cli/src/tdes_cbc_cmd.rs @@ -0,0 +1,47 @@ +//! 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); the command does not count across invocations. + +use crate::helpers::block_mode_helpers::{ + CipherDirection, decrypt_stream, encrypt_stream, 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::<_, BLOCK_LEN, BLOCK_LEN>(enc, generated_iv.as_ref(), output_hex, None) + } + 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..3582c4f95 --- /dev/null +++ b/cli/src/tdes_cfb8_cmd.rs @@ -0,0 +1,37 @@ +//! 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. +//! +//! # 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::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +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::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs new file mode 100644 index 000000000..63012921d --- /dev/null +++ b/cli/src/tdes_cfb_cmd.rs @@ -0,0 +1,38 @@ +//! 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::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +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::<_, KEY_LEN, BLOCK_LEN>(action, &key, load_iv(iv, iv_file), output_hex) +} diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs new file mode 100644 index 000000000..ecd19310f --- /dev/null +++ b/cli/src/tdes_ctr_cmd.rs @@ -0,0 +1,58 @@ +//! 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::{CipherDirection, load_iv, load_key_object}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +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::<_, KEY_LEN, CTR_NONCE_LEN>(action, &key, nonce, output_hex) +} diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs new file mode 100644 index 000000000..af55e9bd3 --- /dev/null +++ b/cli/src/tdes_ecb_cmd.rs @@ -0,0 +1,53 @@ +//! 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, decrypt_stream, encrypt_stream, 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::<_, 0, BLOCK_LEN>(enc, None, output_hex, Some(MODE)) + } + CipherDirection::Decrypt => decrypt_stream::<_, 0, BLOCK_LEN>( + None, + |no_iv| key.create_decryptor(no_iv), + output_hex, + Some(MODE), + ), + } +} diff --git a/cli/tests/tdes_cli_tests.rs b/cli/tests/tdes_cli_tests.rs new file mode 100644 index 000000000..6571bb69e --- /dev/null +++ b/cli/tests/tdes_cli_tests.rs @@ -0,0 +1,378 @@ +//! 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, PathBuf}; +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, +} + +/// Locates the TDES CAVP directory, or `None` if `bc-test-data` is not checked out. +fn cavp_dir() -> Option { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../bc-test-data/crypto/aes_tdes_vectors/TDES"); + if dir.join("TECBMMT3.rsp").exists() { + return Some(dir); + } + println!( + "WARNING: bc-test-data not found at {}; the CAVP vector tests are skipped", + dir.display() + ); + None +} + +/// `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 = std::fs::read_to_string(cavp_dir()?.join(file)).expect("readable response 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/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index f9931ffc8..296a3db2f 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -158,6 +158,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; let iv = random_iv::(rng)?; Ok((PaddedBlockCipherEncryptor::from_inner(Cbc::from_perm(state, iv)), iv)) } @@ -196,6 +202,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; let iv = random_iv::(rng)?; Ok((Cbc::from_perm(state, iv), iv)) } diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index 36213f44c..e63714a1e 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -196,6 +196,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; // `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 b1797ff8e..24a9d2649 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -103,6 +103,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; BLOCK_LEN]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; // `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 da2c639e9..f0a98bce8 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -171,6 +171,12 @@ where state: &'k Self::KeyState, rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; let nonce = random_iv::(rng)?; Ok((StreamCipher::from_keystream(CtrKeyStream::from_perm(state, nonce)), nonce)) } diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index 5e6bc1430..2efe068e7 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -129,6 +129,12 @@ where P: 'k; fn encrypt_init<'k>(state: &'k Self::KeyState) -> Self::Encryptor<'k> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; Ecb::from_perm(state) } } @@ -165,6 +171,12 @@ where state: &'k Self::KeyState, _rng: &mut dyn RNG, ) -> Result<(Self::Encryptor<'k>, [u8; 0]), SymmetricCipherError> { + const { + assert!( + P::ENCRYPTION_APPROVED, + "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + ) + }; Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state)), [])) } } diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 485550db8..3446a7943 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..d80d8bbd1 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -88,4 +88,17 @@ pub trait ElectronicCodeBook: /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a /// [`SymmetricCipherError::KeyMaterialError`]. fn new(key: &KeyMaterial) -> Result; + + /// Whether this permutation may be used to *apply* cryptographic protection, i.e. as the + /// cipher of an encrypting mode. + /// + /// `true` for every current cipher. `false` marks a permutation kept only to process data that + /// was protected in the past -- two-key TDEA, which NIST SP 800-131A Rev 2 Table 1 lists as + /// "Disallowed" for encryption and "Legacy use" for decryption. A mode checks this in an + /// inline `const` when it makes an encrypting worker, so a key's `create_encryptor` over such + /// a permutation is a compile error at the call site, while its decryptors are unaffected. + /// 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). + const ENCRYPTION_APPROVED: bool = true; } 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..ea81e7099 --- /dev/null +++ b/crypto/tdes/src/des.rs @@ -0,0 +1,391 @@ +//! 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; + +/// 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; + +/// 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 so that +/// nothing depends on the multiplier's timing. +#[inline(always)] +fn spread(v: u32) -> u32 { + 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::TDES`] 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..a455a271a --- /dev/null +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -0,0 +1,270 @@ +//! 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 = ""; +} + +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..10fbf7980 --- /dev/null +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -0,0 +1,194 @@ +//! 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 +/// [`ElectronicCodeBook::ENCRYPTION_APPROVED`] to `false`, and every mode in +/// `bouncycastle_cipher::modes` refuses to make an encrypting worker over it **at compile time**, +/// so a two-key key's `create_encryptor` does not compile: +/// +/// ```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 when they make 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). +/// +/// # 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 }) + } + + /// SP 800-131A Rev 2 Table 1: two-key TDEA encryption is disallowed. + const ENCRYPTION_APPROVED: bool = false; +} + +impl BlockPermutation for TDES2Internal { + /// 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 = ""; +} + +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..9b0d3b8bf --- /dev/null +++ b/crypto/tdes/src/key.rs @@ -0,0 +1,252 @@ +//! 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, Mode, ModeKey, SymmetricDecryptMode, + SymmetricEncryptMode, +}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, 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 at the call site: the +/// modes check [`ElectronicCodeBook::ENCRYPTION_APPROVED`] when 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)?) }) + } +} + +impl> ModeKey for TDESKey { + fn state(&self) -> &M::KeyState { + &self.state + } +} + +// 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) + } +} diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs new file mode 100644 index 000000000..507ae06d6 --- /dev/null +++ b/crypto/tdes/src/lib.rs @@ -0,0 +1,318 @@ +//! 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). The aliases +//! [`TDES_CBC`], [`TDES_CFB`], [`TDES_CFB8`], [`TDES_CTR`] and [`TDES_ECB`] 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 `ElectronicCodeBook::ENCRYPTION_APPROVED` to `false`, so the key's +//! `create_encryptor` refuses to compile under every mode, while its decryptors work. 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: +//! +//! ``` +//! 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: "This signifies that TDEA is no longer an +//! approved block cipher. ... TDEA will continue to be allowed for the decryption, key +//! unwrapping, and verification of Message Authentication Codes (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". 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`. +//! +//! ## 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; + +/// 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}; + +// 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..9fc748485 --- /dev/null +++ b/crypto/tdes/src/schedule.rs @@ -0,0 +1,248 @@ +//! 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 (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, so no other key has the property. The tests +/// enumerate the spec's three lists against this predicate and against that count. +/// +/// 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); + let period4 = (rotl28(c, 4) ^ c) | (rotl28(d, 4) ^ d); + ct_eq_zero_bytes(&period4.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/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs new file mode 100644 index 000000000..3b152b304 --- /dev/null +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -0,0 +1,192 @@ +//! 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::{ElectronicCodeBook, 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, KEY_LEN_2KEY, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, + TDES2_CBC, TDES2_CFB, TDES2_CFB8, TDES2_CTR, 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"); +} + +#[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!(>::ENCRYPTION_APPROVED); +const _: () = + assert!(!>::ENCRYPTION_APPROVED); + +/// 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..8f2daf643 --- /dev/null +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -0,0 +1,379 @@ +//! 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`]). +//! +//! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` +//! relative to the root of this git project, with the response files from NIST's `tdesmmt.zip` +//! (CAVS 18.0) under `crypto/aes_tdes_vectors/TDES/`. If it is absent the tests print a warning and +//! pass, matching the convention used by the AES ACVP and ML-KEM / ML-DSA suites -- `cargo test` +//! must stay green for someone who has only cloned this repository. +//! +//! 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_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}; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/TDES", + "../bc-test-data/crypto/aes_tdes_vectors/TDES", +]; + +/// Locates the TDES CAVP directory, or `None` if `bc-test-data` is not checked out. +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join("TECBMMT3.rsp").exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); CAVP TDES MMT tests will \ + be skipped" + ); + None +} + +/// 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 dir = test_data_dir()?; + let text = fs::read_to_string(dir.join(file)).expect("readable response 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 at {}", cases.len(), dir.display()); + 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..c2f8b13b6 --- /dev/null +++ b/crypto/tdes/tests/key_tests.rs @@ -0,0 +1,332 @@ +//! 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(_)))) +} + +#[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 { + 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); + } + structural.insert(key & PARITY_MASK); + } + } + 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..847a2a380 --- /dev/null +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -0,0 +1,250 @@ +//! 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}; +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::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); +} + +#[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/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index 1e06763dc..7f13aed97 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -35,6 +35,10 @@ path = "src/bench_ccm_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 231a1b730..14dfb7d74 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -6,3 +6,4 @@ mod bench_mlkem_mem_usage; mod bench_sha2_mem_usage; mod bench_sha3_mem_usage; mod bench_sm3_mem_usage; +mod bench_tdes_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index ae250f332..d36001690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,3 +15,4 @@ pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; pub use bouncycastle_sm3 as sm3; +pub use bouncycastle_tdes as tdes;