From 6746a41481b068667eaead6a0ce360ba0749a99b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:00:27 +1000 Subject: [PATCH 01/32] core: KeyMaterialError::WeakKey, for a key an algorithm's specification says to avoid -- a weak or semi-weak key, or a bundle whose component keys are not distinct --- QUALITY_AND_STYLE.md | 8 ++++---- crypto/core/src/errors.rs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 6d3591a51..a21b00ecc 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -83,10 +83,10 @@ All normal rust naming conventions from clippy apply, with one exception: * Where a type, constant or variable corresponds to something a specification (FIPS, RFC, etc) names, keep the specification's spelling and capitalization, and `#[allow(non_camel_case_types)]`, `#[allow(non_snake_case)]` or - `#[allow(non_upper_case_globals)]` the item locally. So the FIPS 204 signature algorithm is `MLDSA65`, not `MlDsa44`, - and it's `AES_CBC_128`, not `AesCbc128`, and if a specification writes `A` for a matrix and `a` for a vector then - `let A = ...; let a = ...;` is the right thing to do for code readability and correspondence with the spec. The point - is that a reviewer with the specification open can match names by eye; that matters more here than rust convention. + `#[allow(non_upper_case_globals)]` the item locally. So the FIPS 197 cipher is `AES_128`, not `AES_128`, its CBC + mode is `AES_CBC_128`, not `AesCbc128`, and if a specification writes `A` for a matrix and `a` for a vector then + `let A = ...; let a = ...;` is the right thing to do. The point is that a reviewer with the specification open can + match names by eye; that matters more here than rust convention. In addition, some library-specific naming conventions: 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), } /// From 4e573e6aa9a217a85e201dfe96e43a0476cafb7c Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:00:28 +1000 Subject: [PATCH 02/32] cli: block-mode plumbing takes the block length as a const parameter and CHUNK_LEN is a fixed 1 KiB, so the shared encrypt/decrypt loops serve block ciphers other than AES --- cli/src/aes_cbc_cmd.rs | 33 ++++++++++++++++----------- cli/src/aes_cfb8_cmd.rs | 22 +++++++++--------- cli/src/aes_cfb_cmd.rs | 22 +++++++++--------- cli/src/aes_ecb_cmd.rs | 12 +++++----- cli/src/helpers/block_mode_helpers.rs | 16 ++++++------- 5 files changed, 55 insertions(+), 50 deletions(-) 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..7205710e4 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 @@ -278,7 +275,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 +289,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 +300,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 +315,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 +350,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 +387,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 From 3ce0987ee71475c42214e6cb8feb169384b91a80 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 03/32] core: ElectronicCodeBook::ENCRYPTION_APPROVED (default true) marks a permutation kept for decrypting legacy data only, so a mode can refuse to encrypt with it at compile time --- crypto/core/src/hazmat/electronic_code_book.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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; } From 7a0e4250473ad619d986beb0fe9ed350bfeb0d19 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 04/32] modes: the Encrypting direction of Ecb, Cbc, Cfb, Cfb8 and Ctr asserts P::ENCRYPTION_APPROVED in an inline const, so building an encryptor over a decryption-only permutation is a compile error --- crypto/cipher/src/modes/cbc.rs | 12 ++++++++++++ crypto/cipher/src/modes/cfb.rs | 6 ++++++ crypto/cipher/src/modes/cfb8.rs | 6 ++++++ crypto/cipher/src/modes/ctr.rs | 6 ++++++ crypto/cipher/src/modes/hazmat/ecb.rs | 12 ++++++++++++ 5 files changed, 42 insertions(+) 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)), [])) } } From 6d1656c257153a4ac42b4c72b52db67b3642384a Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 05/32] tdes: add bouncycastle-tdes, a constant-time table-free TDEA engine (SP 800-67r2) whose S-box layer is an ANF circuit derived from the spec tables at compile time -- three-key TDES with TDES_* mode aliases, decryption-only two-key TDES2Key with TDES2_* decryptors, tdes-*/tdes2-* CLI subcommands, CAVP MMT vectors from bc-test-data --- Cargo.toml | 2 + cli/src/tdes2_cbc_cmd.rs | 27 ++ cli/src/tdes2_cfb8_cmd.rs | 22 + cli/src/tdes2_cfb_cmd.rs | 22 + cli/src/tdes2_ctr_cmd.rs | 22 + cli/src/tdes2_ecb_cmd.rs | 24 ++ cli/src/tdes_cbc_cmd.rs | 43 ++ cli/src/tdes_cfb8_cmd.rs | 36 ++ cli/src/tdes_cfb_cmd.rs | 37 ++ cli/src/tdes_ctr_cmd.rs | 54 +++ cli/src/tdes_ecb_cmd.rs | 47 +++ crypto/tdes/Cargo.toml | 23 ++ crypto/tdes/benches/tdes_benches.rs | 94 +++++ crypto/tdes/src/cbc.rs | 176 ++++++++ crypto/tdes/src/cfb.rs | 79 ++++ crypto/tdes/src/cfb8.rs | 73 ++++ crypto/tdes/src/ctr.rs | 106 +++++ crypto/tdes/src/des.rs | 389 ++++++++++++++++++ crypto/tdes/src/ecb.rs | 145 +++++++ crypto/tdes/src/lib.rs | 276 +++++++++++++ crypto/tdes/src/padded_mode.rs | 64 +++ crypto/tdes/src/sbox.rs | 297 +++++++++++++ crypto/tdes/src/schedule.rs | 248 +++++++++++ crypto/tdes/src/tdes.rs | 252 ++++++++++++ crypto/tdes/src/tdes2.rs | 150 +++++++ crypto/tdes/summary.md | 167 ++++++++ crypto/tdes/tests/cavp_mmt_tests.rs | 375 +++++++++++++++++ crypto/tdes/tests/cbc_alias_tests.rs | 119 ++++++ crypto/tdes/tests/ecb_alias_tests.rs | 112 +++++ .../tdes/tests/electronic_code_book_tests.rs | 26 ++ crypto/tdes/tests/key_tests.rs | 326 +++++++++++++++ mem_usage_benches/Cargo.toml | 4 + mem_usage_benches/src/bench_tdes_mem_usage.rs | 106 +++++ mem_usage_benches/src/lib.rs | 1 + src/lib.rs | 1 + 35 files changed, 3945 insertions(+) create mode 100644 cli/src/tdes2_cbc_cmd.rs create mode 100644 cli/src/tdes2_cfb8_cmd.rs create mode 100644 cli/src/tdes2_cfb_cmd.rs create mode 100644 cli/src/tdes2_ctr_cmd.rs create mode 100644 cli/src/tdes2_ecb_cmd.rs create mode 100644 cli/src/tdes_cbc_cmd.rs create mode 100644 cli/src/tdes_cfb8_cmd.rs create mode 100644 cli/src/tdes_cfb_cmd.rs create mode 100644 cli/src/tdes_ctr_cmd.rs create mode 100644 cli/src/tdes_ecb_cmd.rs create mode 100644 crypto/tdes/Cargo.toml create mode 100644 crypto/tdes/benches/tdes_benches.rs create mode 100644 crypto/tdes/src/cbc.rs create mode 100644 crypto/tdes/src/cfb.rs create mode 100644 crypto/tdes/src/cfb8.rs create mode 100644 crypto/tdes/src/ctr.rs create mode 100644 crypto/tdes/src/des.rs create mode 100644 crypto/tdes/src/ecb.rs create mode 100644 crypto/tdes/src/lib.rs create mode 100644 crypto/tdes/src/padded_mode.rs create mode 100644 crypto/tdes/src/sbox.rs create mode 100644 crypto/tdes/src/schedule.rs create mode 100644 crypto/tdes/src/tdes.rs create mode 100644 crypto/tdes/src/tdes2.rs create mode 100644 crypto/tdes/summary.md create mode 100644 crypto/tdes/tests/cavp_mmt_tests.rs create mode 100644 crypto/tdes/tests/cbc_alias_tests.rs create mode 100644 crypto/tdes/tests/ecb_alias_tests.rs create mode 100644 crypto/tdes/tests/electronic_code_book_tests.rs create mode 100644 crypto/tdes/tests/key_tests.rs create mode 100644 mem_usage_benches/src/bench_tdes_mem_usage.rs diff --git a/Cargo.toml b/Cargo.toml index 16f06a30f..c2b764d20 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/tdes2_cbc_cmd.rs b/cli/src/tdes2_cbc_cmd.rs new file mode 100644 index 000000000..e6eb7eacf --- /dev/null +++ b/cli/src/tdes2_cbc_cmd.rs @@ -0,0 +1,27 @@ +//! 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, +//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cbc decrypt` -- the first 8 bytes +//! of input are the IV, the rest whole 8-byte blocks -- see [`crate::tdes_cbc_cmd`] and +//! [`crate::block_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::block_mode_cmd::{DecryptOnlyAction, decrypt_stream, load_key}; +use bouncycastle::modes::{Cbc, Decrypting}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; + +pub(crate) fn tdes2_cbc_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key::(key, key_file, "two-key TDES"); + decrypt_stream::< + Cbc, + KEY_LEN_2KEY, + BLOCK_LEN, + BLOCK_LEN, + >(&key, output_hex, "CBC") +} diff --git a/cli/src/tdes2_cfb8_cmd.rs b/cli/src/tdes2_cfb8_cmd.rs new file mode 100644 index 000000000..1445015b3 --- /dev/null +++ b/cli/src/tdes2_cfb8_cmd.rs @@ -0,0 +1,22 @@ +//! 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, +//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb8 decrypt` -- the first 8 +//! bytes of input are the IV, the rest any length -- see [`crate::tdes_cfb8_cmd`] for how CFB8 +//! differs from CFB64, and [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; +use crate::stream_mode_cmd::decrypt_stream; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB8}; + +pub(crate) fn tdes2_cfb8_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key::(key, key_file, "two-key TDES"); + decrypt_stream::(&key, output_hex) +} diff --git a/cli/src/tdes2_cfb_cmd.rs b/cli/src/tdes2_cfb_cmd.rs new file mode 100644 index 000000000..8ad55580c --- /dev/null +++ b/cli/src/tdes2_cfb_cmd.rs @@ -0,0 +1,22 @@ +//! 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, +//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb decrypt` -- the first 8 +//! bytes of input are the IV, the rest any length -- see [`crate::tdes_cfb_cmd`] and +//! [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; +use crate::stream_mode_cmd::decrypt_stream; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB}; + +pub(crate) fn tdes2_cfb_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key::(key, key_file, "two-key TDES"); + decrypt_stream::(&key, output_hex) +} diff --git a/cli/src/tdes2_ctr_cmd.rs b/cli/src/tdes2_ctr_cmd.rs new file mode 100644 index 000000000..bed1dc651 --- /dev/null +++ b/cli/src/tdes2_ctr_cmd.rs @@ -0,0 +1,22 @@ +//! 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, +//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-ctr decrypt` -- the first 6 +//! bytes of input are the nonce, the counter starts at zero, at most 512 KiB of data -- see +//! [`crate::tdes_ctr_cmd`] and [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. + +use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; +use crate::stream_mode_cmd::decrypt_stream; +use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN_2KEY, TDES2_CTR}; + +pub(crate) fn tdes2_ctr_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let key = load_key::(key, key_file, "two-key TDES"); + decrypt_stream::(&key, output_hex) +} diff --git a/cli/src/tdes2_ecb_cmd.rs b/cli/src/tdes2_ecb_cmd.rs new file mode 100644 index 000000000..18a87f43f --- /dev/null +++ b/cli/src/tdes2_ecb_cmd.rs @@ -0,0 +1,24 @@ +//! 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, +//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, 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::block_mode_cmd`] for the framing. The key is the 16-byte `KEY1 || KEY2`. + +use crate::block_mode_cmd::{DecryptOnlyAction, decrypt_stream, load_key}; +use bouncycastle::modes::{Decrypting, Ecb}; +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::(key, key_file, "two-key TDES"); + decrypt_stream::, KEY_LEN_2KEY, 0, BLOCK_LEN>( + &key, output_hex, "ECB", + ) +} diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs new file mode 100644 index 000000000..9787ae4f2 --- /dev/null +++ b/cli/src/tdes_cbc_cmd.rs @@ -0,0 +1,43 @@ +//! TDES-CBC encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading, stdin framing and +//! block-alignment enforcement are all in [`crate::block_mode_cmd`], shared with the AES commands +//! and `tdes-ecb`. See that module for the command-line contract. The block, and so the IV, is +//! 8 bytes. +//! +//! 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::block_mode_cmd::{BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES}; + +/// Names the mode in error messages. +const MODE: &str = "CBC"; + +pub(crate) fn tdes_cbc_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key::(key, key_file, "TDES"); + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( + &key, output_hex, MODE, + ) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( + &key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs new file mode 100644 index 000000000..425c637fc --- /dev/null +++ b/cli/src/tdes_cfb8_cmd.rs @@ -0,0 +1,36 @@ +//! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), 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::block_mode_cmd::{BlockModeAction, load_key}; +use crate::stream_mode_cmd::run_stream_mode; +use bouncycastle::modes::{Decrypting, Encrypting}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB8}; + +pub(crate) fn tdes_cfb8_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key::(key, key_file, "TDES"); + run_stream_mode::, TDES_CFB8, KEY_LEN, BLOCK_LEN>( + action, &key, output_hex, + ) +} diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs new file mode 100644 index 000000000..7bb54dcae --- /dev/null +++ b/cli/src/tdes_cfb_cmd.rs @@ -0,0 +1,37 @@ +//! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), 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::block_mode_cmd::{BlockModeAction, load_key}; +use crate::stream_mode_cmd::run_stream_mode; +use bouncycastle::modes::{Decrypting, Encrypting}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB}; + +pub(crate) fn tdes_cfb_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key::(key, key_file, "TDES"); + run_stream_mode::, TDES_CFB, KEY_LEN, BLOCK_LEN>( + action, &key, output_hex, + ) +} diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs new file mode 100644 index 000000000..2b74d704d --- /dev/null +++ b/cli/src/tdes_ctr_cmd.rs @@ -0,0 +1,54 @@ +//! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), 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 and there is no way to +//! supply one, but with only 48 bits of nonce the number of messages under one key bundle must +//! stay small -- 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::block_mode_cmd::{BlockModeAction, load_key}; +use crate::stream_mode_cmd::run_stream_mode; +use bouncycastle::modes::{Decrypting, Encrypting}; +use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN, TDES_CTR}; + +pub(crate) fn tdes_ctr_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key::(key, key_file, "TDES"); + run_stream_mode::, TDES_CTR, KEY_LEN, CTR_NONCE_LEN>( + action, &key, output_hex, + ) +} diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs new file mode 100644 index 000000000..8c9cd5eb0 --- /dev/null +++ b/cli/src/tdes_ecb_cmd.rs @@ -0,0 +1,47 @@ +//! 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::block_mode_cmd`], 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::block_mode_cmd::{BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::modes::{Decrypting, Ecb, Encrypting}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES}; + +/// Names the mode in error messages. +const MODE: &str = "ECB"; + +pub(crate) fn tdes_ecb_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_key::(key, key_file, "TDES"); + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( + &key, output_hex, MODE, + ) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( + &key, output_hex, MODE, + ) + } + } +} diff --git a/crypto/tdes/Cargo.toml b/crypto/tdes/Cargo.toml new file mode 100644 index 000000000..ad5cee15f --- /dev/null +++ b/crypto/tdes/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bouncycastle-tdes" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +# Only for the TDES mode type aliases in `cbc.rs`, `cfb.rs`, `cfb8.rs`, `ctr.rs` and `ecb.rs`; the +# engine itself does not use it. +bouncycastle-modes.workspace = true +# Only for the padded TDES-CBC and TDES-ECB aliases; the engine itself does not use it. +bouncycastle-padding.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..bab00fdef --- /dev/null +++ b/crypto/tdes/benches/tdes_benches.rs @@ -0,0 +1,94 @@ +//! Criterion benchmarks for the constant-time TDES engine. +//! +//! The permutation processes one block per call and does not override the pair or four-block +//! entry points, 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 default +//! 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::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ElectronicCodeBook, RNG}; +use bouncycastle_rng as rng; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES}; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 2048 TDES 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 TDES::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(TDES::new(black_box(&key)).unwrap())) + }); + group.finish(); +} + +fn bench_tdes(c: &mut Criterion) { + let tdes = TDES::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..ba0fc6852 --- /dev/null +++ b/crypto/tdes/src/cbc.rs @@ -0,0 +1,176 @@ +//! Type alias for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), +//! with padding. +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle-padding`'s +//! adapters take five more. This alias pins all of them except the two choices a caller actually +//! makes: the direction and the padding scheme. It adds nothing to the engine -- the permutation +//! still implements none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! ```text +//! TDES_CBC // TDES, CBC, PKCS#7 padded, encrypting +//! TDES_CBC +//! ``` +//! +//! # Why the padding is part of the alias +//! +//! 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 type 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-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. +//! +//! # This is the arbitrary-length API +//! +//! A padded alias implements [`SimpleCipherEncryptor`] / [`SimpleCipherDecryptor`], not the +//! block traits: `encrypt_out` / `decrypt_out` and the streaming `do_update_out` / `do_final`, all +//! taking a `&[u8]` of any length. The block-aligned API, with its compile-time length checks and +//! its in-place data methods, is `bouncycastle_modes::Cbc` itself, which this wraps: +//! +//! ```text +//! bouncycastle_modes::Cbc // block-aligned, in place +//! TDES_CBC // any length, padded +//! ``` +//! +//! # How one alias covers both directions +//! +//! `PaddedEncryptor` and `PaddedDecryptor` are two distinct types, so a plain type alias cannot +//! select between them on a `Dir` parameter. [`PaddedMode`] does it instead: it is implemented for +//! each direction marker and projects to the right adapter, and the alias is written as that +//! projection. The only visible consequence is that `Dir` must be +//! [`Encrypting`](bouncycastle_modes::Encrypting) or +//! [`Decrypting`](bouncycastle_modes::Decrypting), which was already true. + +use crate::padded_mode::PaddedMode; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use bouncycastle_padding::PaddedDecryptor; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +#[allow(unused_imports)] +use bouncycastle_padding::{NoPadding, PKCS7, PaddedEncryptor}; +// end of imports needed for docs + +/// TDES in CBC mode with a padding scheme. +/// +/// `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 generated by encryption and +/// returned; it is never supplied. It is one block: 8 bytes. +/// +/// ``` +/// use bouncycastle_tdes::TDES_CBC; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// use bouncycastle_padding::PKCS7; +/// +/// type Enc = TDES_CBC; +/// type Dec = TDES_CBC; +/// +/// // Three distinct component keys; see `TDES` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// +/// // 5 bytes: PKCS#7 pads it to one block, so the padding does the work CBC cannot. +/// let message = b"hello"; +/// let mut ciphertext = [0u8; 8]; +/// let (iv, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); +/// assert_eq!(written, 8); +/// +/// let mut plaintext = [0u8; 8]; +/// let n = Dec::decrypt_out(&key, &iv, &ciphertext, &mut plaintext).expect("decryption"); +/// assert_eq!(&plaintext[..n], message); +/// ``` +/// +/// With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +/// error at `do_final` rather than something silently padded: +/// +/// ``` +/// use bouncycastle_tdes::TDES_CBC; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// use bouncycastle_padding::NoPadding; +/// +/// type Enc = TDES_CBC; +/// +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// +/// // A whole block is fine, and comes out the same length. +/// let mut out = [0u8; 8]; +/// let (_iv, written) = Enc::encrypt_out(&key, &[0u8; 8], &mut out).expect("aligned"); +/// assert_eq!(written, 8); +/// +/// // Five bytes is not, and is refused rather than padded. +/// let mut out = [0u8; 8]; +/// assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +/// ``` +/// +/// The padding scheme is part of the type, so the two schemes 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; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// use bouncycastle_padding::{NoPadding, PKCS7}; +/// +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// +/// // Built as NoPadding, annotated as PKCS7: mismatched types. +/// let (enc, _iv) = TDES_CBC::::do_encrypt_init(&key).unwrap(); +/// let _mismatched: TDES_CBC = enc; +/// ``` +#[allow(non_camel_case_types)] +pub type TDES_CBC = , + Cbc, + Pad, + BLOCK_LEN, +>>::Mode; + +/// Two-key TDES in CBC mode with a padding scheme, **decryption only**. +/// +/// `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 [`PaddedDecryptor`] alone, and +/// `Cbc` does not compile; see [`TDES2Key`]. The IV is one 8-byte block, +/// supplied by the caller as it was received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2_CBC; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SimpleCipherDecryptor; +/// use bouncycastle_padding::NoPadding; +/// +/// // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let key = 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 iv = [0xf2, 0x9d, 0x4b, 0xa5, 0x17, 0x2f, 0xd2, 0x97]; +/// let ciphertext = [0x1b, 0x38, 0x89, 0xc9, 0xdb, 0xe1, 0x0d, 0x03]; +/// +/// let plaintext = TDES2_CBC::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +/// assert_eq!(plaintext, [0x06, 0x11, 0xa4, 0x74, 0xfb, 0x09, 0x09, 0x78]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CBC = PaddedDecryptor< + Cbc, + Pad, + KEY_LEN_2KEY, + BLOCK_LEN, + BLOCK_LEN, +>; diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs new file mode 100644 index 000000000..e82fb1ab1 --- /dev/null +++ b/crypto/tdes/src/cfb.rs @@ -0,0 +1,79 @@ +//! Type alias for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so +//! callers never spell them out. It adds nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! The segment size is the full block, so 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_modes::Cfb` docs. + +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_modes::{Cfb, Decrypting}; + +/// TDES in CFB64 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// CFB is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly +/// as long as the plaintext. The IV is one block (8 bytes), generated by encryption and returned; +/// it is never supplied. Encryption and decryption work in place. +/// +/// ``` +/// use bouncycastle_tdes::TDES_CFB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// // Three distinct component keys; see `TDES` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// // 47 bytes: a stream cipher does not need a whole number of blocks. +/// let message = [0u8; 47]; +/// let mut data = message; +/// let iv = TDES_CFB::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// TDES_CFB::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, at any byte boundary: +/// let (mut enc, iv) = TDES_CFB::::do_encrypt_init(&key).unwrap(); +/// let mut first = [0u8; 5]; +/// let mut rest = [1u8; 30]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); +/// let mut dec = TDES_CFB::::do_decrypt_init(&key, &iv).unwrap(); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 5]); +/// assert_eq!(rest, [1u8; 30]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES_CFB = Cfb; + +/// Two-key TDES in CFB64 mode, **decryption only** (SP 800-131A Rev 2 Table 1). No direction +/// parameter: this is the `Decrypting` direction, and `Cfb` does not +/// compile; see [`TDES2Key`]. The IV is one 8-byte block, supplied by the caller as received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2_CFB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::StreamCipherDecryptor; +/// +/// // NIST CAVP TCFB64MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let key = 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 iv = [0x6e, 0x37, 0xd1, 0x97, 0x37, 0x6d, 0xb5, 0x95]; +/// let mut data = [0x63, 0xca, 0xd5, 0x22, 0x60, 0xe0, 0xa1, 0xcd]; +/// +/// TDES2_CFB::decrypt(&key, &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 = Cfb; diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs new file mode 100644 index 000000000..5dc065e5c --- /dev/null +++ b/crypto/tdes/src/cfb8.rs @@ -0,0 +1,73 @@ +//! Type alias for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb8` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so +//! callers never spell them out. It adds nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! 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_modes::Cfb8` docs for when that is the right trade. + +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_modes::{Cfb8, Decrypting}; + +/// TDES in CFB8 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// CFB8 is a stream cipher with a one-byte segment, so the data is a `&mut [u8]` of any length and +/// the ciphertext is exactly as long as the plaintext. The IV is one block (8 bytes), generated by +/// encryption and returned; it is never supplied. Encryption and decryption work in place. +/// +/// ``` +/// use bouncycastle_tdes::{TDES_CFB8, TDES_CFB}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// // Three distinct component keys; see `TDES` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// // 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 = TDES_CFB8::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// TDES_CFB8::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // CFB8 and CFB64 are not interchangeable: same key, same IV, different ciphertext. +/// let mut as_cfb8 = message; +/// let iv = TDES_CFB8::::encrypt(&key, &mut as_cfb8).unwrap(); +/// let mut as_cfb64 = as_cfb8; +/// TDES_CFB::::decrypt(&key, &iv, &mut as_cfb64).unwrap(); +/// assert_ne!(as_cfb64, message); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES_CFB8 = Cfb8; + +/// Two-key TDES in CFB8 mode, **decryption only** (SP 800-131A Rev 2 Table 1). No direction +/// parameter: this is the `Decrypting` direction, and `Cfb8` does not +/// compile; see [`TDES2Key`]. The IV is one 8-byte block, supplied by the caller as received. +/// +/// ``` +/// use bouncycastle_tdes::TDES2_CFB8; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::StreamCipherDecryptor; +/// +/// // NIST CAVP TCFB8MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let key = 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 iv = [0x8b, 0x97, 0x57, 0x9e, 0xa5, 0xac, 0x30, 0x0f]; +/// let mut data = [0x05]; +/// +/// TDES2_CFB8::decrypt(&key, &iv, &mut data).expect("decryption"); +/// assert_eq!(data, [0x80]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CFB8 = Cfb8; diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs new file mode 100644 index 000000000..91595b017 --- /dev/null +++ b/crypto/tdes/src/ctr.rs @@ -0,0 +1,106 @@ +//! Type alias for TDES in CTR mode (NIST SP 800-38A Sec 6.5). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ctr` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` / `INIT_DATA_LEN` const parameters. This alias pins +//! the TDES values so callers never spell them out. It adds nothing to the engine: the permutation +//! still implements none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! 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 this alias chooses. Name +//! `Ctr` directly for a different split. + +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_modes::{Ctr, Decrypting}; + +/// 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_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// CTR is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly +/// as long as the plaintext. The nonce is generated by encryption and returned; it is never +/// supplied. Encryption and decryption work in place, and are the same operation. A message longer +/// than 65 536 blocks (512 KiB) is an error, not a repeated counter; see the module docs. +/// +/// ``` +/// use bouncycastle_tdes::TDES_CTR; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// // Three distinct component keys; see `TDES` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// // 47 bytes: a stream cipher does not need a whole number of blocks. +/// let message = [0u8; 47]; +/// let mut data = message; +/// let nonce = TDES_CTR::::encrypt(&key, &mut data).unwrap(); +/// assert_eq!(nonce.len(), 6); +/// assert_ne!(data, message); +/// TDES_CTR::::decrypt(&key, &nonce, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, at any byte boundary: +/// let (mut enc, nonce) = TDES_CTR::::do_encrypt_init(&key).unwrap(); +/// let mut first = [0u8; 5]; +/// let mut rest = [1u8; 30]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); +/// let mut dec = TDES_CTR::::do_decrypt_init(&key, &nonce).unwrap(); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 5]); +/// assert_eq!(rest, [1u8; 30]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES_CTR = Ctr; + +/// 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 `Ctr` +/// does not compile; see [`TDES2Key`]. 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::{TDES2_CTR, TDES2Key}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{ElectronicCodeBook, StreamCipherDecryptor}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = 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]; +/// TDES2Key::new(&key).unwrap().encrypt_block(&mut keystream); +/// let plaintext = *b"8 bytes!"; +/// let mut data: [u8; 8] = core::array::from_fn(|i| plaintext[i] ^ keystream[i]); +/// +/// TDES2_CTR::decrypt(&key, &nonce, &mut data).expect("decryption"); +/// assert_eq!(data, plaintext); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_CTR = Ctr; diff --git a/crypto/tdes/src/des.rs b/crypto/tdes/src/des.rs new file mode 100644 index 000000000..b0366a34a --- /dev/null +++ b/crypto/tdes/src/des.rs @@ -0,0 +1,389 @@ +//! 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::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/ecb.rs b/crypto/tdes/src/ecb.rs new file mode 100644 index 000000000..53047fa8a --- /dev/null +++ b/crypto/tdes/src/ecb.rs @@ -0,0 +1,145 @@ +//! Type alias for TDES in ECB mode (NIST SP 800-38A Sec 6.1; "TECB" in SP 800-38A Appendix E), +//! with padding. +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ecb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle-padding`'s +//! adapters take five more. This alias pins all of them except the two choices a caller actually +//! makes: the direction and the padding scheme. +//! +//! ```text +//! TDES_ECB // TDES, ECB, PKCS#7 padded, encrypting +//! TDES_ECB +//! ``` +//! +//! **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. This alias exists for interoperability with +//! systems that use ECB and for driving test vectors. See the crate docs, "A block permutation is +//! not a cipher". +//! +//! # Why the padding is part of the alias +//! +//! 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. Naming it in the type +//! makes the choice explicit and makes a mismatched pair a compile error. [`PKCS7`] is the usual +//! one (this is Java's `DESede/ECB/PKCS5Padding`); [`NoPadding`] adds nothing and instead rejects a +//! message that is not a whole number of blocks. +//! +//! # This is the arbitrary-length API +//! +//! A padded alias implements [`SimpleCipherEncryptor`] / [`SimpleCipherDecryptor`], not the +//! block traits. The block-aligned API, with compile-time length checks and in-place data methods, +//! is `bouncycastle_modes::Ecb` itself, which this wraps. 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. +//! +//! # How one alias covers both directions +//! +//! See [`PaddedMode`], which is the projection that lets `Dir` select between the encryptor and the +//! decryptor adapter. `Dir` must be [`Encrypting`] or [`Decrypting`]. + +use crate::padded_mode::PaddedMode; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; +use bouncycastle_padding::PaddedDecryptor; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +#[allow(unused_imports)] +use bouncycastle_padding::{NoPadding, PKCS7}; +// end of imports needed for docs + +/// TDES in ECB mode with a padding scheme. +/// +/// `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::TDES_ECB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// use bouncycastle_padding::PKCS7; +/// +/// type Enc = TDES_ECB; +/// type Dec = TDES_ECB; +/// +/// // Three distinct component keys; see `TDES` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// +/// // 5 bytes: PKCS#7 pads it to one 8-byte block. The init data is empty, ECB having no IV. +/// let (no_iv, ciphertext) = Enc::encrypt(&key, b"hello").expect("encryption"); +/// assert_eq!(no_iv, [0u8; 0]); +/// assert_eq!(ciphertext.len(), 8); +/// +/// let recovered = Dec::decrypt(&key, &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::TDES_ECB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// use bouncycastle_padding::NoPadding; +/// +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// +/// // Two identical blocks in... +/// let (_, ciphertext) = +/// TDES_ECB::::encrypt(&key, &[0x5Au8; 16]).expect("encryption"); +/// // ...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 = , + Ecb, + Pad, + 0, +>>::Mode; + +/// Two-key TDES in ECB mode with a padding scheme, **decryption only**. +/// +/// `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 [`PaddedDecryptor`] alone; see +/// [`TDES2Key`]. There is no IV, so decryption takes an empty array. The ECB warning in the module +/// docs applies unchanged. +/// +/// ``` +/// use bouncycastle_tdes::TDES2_ECB; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SimpleCipherDecryptor; +/// use bouncycastle_padding::NoPadding; +/// +/// // NIST CAVP TECBMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). +/// let key = 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 ciphertext = [0x89, 0x32, 0x1b, 0xa7, 0x5b, 0xa5, 0x45, 0xdb]; +/// let plaintext = TDES2_ECB::::decrypt(&key, &[], &ciphertext).expect("decryption"); +/// assert_eq!(plaintext, [0xd8, 0xda, 0x89, 0x29, 0x88, 0x78, 0xed, 0x7d]); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_ECB = PaddedDecryptor< + Ecb, + Pad, + KEY_LEN_2KEY, + 0, + BLOCK_LEN, +>; diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs new file mode 100644 index 000000000..456be3565 --- /dev/null +++ b/crypto/tdes/src/lib.rs @@ -0,0 +1,276 @@ +//! A constant-time, table-free Triple DES block cipher engine (NIST SP 800-67 Rev 2, TDEA). +//! +//! This crate provides the raw three-key TDEA keyed permutation, [`TDES`], and a decryption-only +//! two-key one, [`TDES2Key`], implemented as a Boolean circuit over bit-planes rather than by +//! lookups in S-box tables. That makes them constant-time; see [Design](#design). They are +//! *permutations*, not ciphers you can encrypt data with, and TDEA is a **legacy algorithm**: see +//! [Security Considerations](#security-considerations) before using it for anything new. +//! +//! # Usage Examples +//! +//! ## Encrypting and decrypting a single block +//! +//! ``` +//! use bouncycastle_tdes::TDES; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::ElectronicCodeBook; +//! +//! // 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 = TDES::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]); +//! ``` +//! +//! ## Modes of operation +//! +//! To encrypt more than one block, use a mode of operation from `bouncycastle-modes`. This crate +//! provides aliases that fill in the const parameters, leaving only the choices a caller actually +//! makes: [`TDES_CBC`] for CBC (SP 800-38A Sec 6.2), which takes the direction **and a padding +//! scheme**, and [`TDES_CFB`] for CFB64 (Sec 6.3), which takes only the direction. [`TDES_CFB8`] +//! gives CFB8, the `s = 8` segment size, a different and non-interoperable mode costing one TDES +//! call per byte. [`TDES_CTR`] gives CTR (Sec 6.5) with a 6-byte nonce and a 2-byte counter. +//! [`TDES_ECB`] gives ECB (Sec 6.1), which takes a padding scheme like CBC and has no IV, for +//! interoperability and test vectors only -- see +//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher). +//! +//! 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 alias 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-modes` crate docs for the comparison, and +//! [`TDES_CBC`] for why the scheme is named in the type. +//! +//! ``` +//! use bouncycastle_tdes::TDES_CBC; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Encrypting}; +//! use bouncycastle_padding::PKCS7; +//! +//! // Three distinct component keys; see `TDES::new` 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +//! .expect("a 24-byte symmetric cipher key"); +//! // 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 generated for you and returned; there is no API for supplying one. +//! let (iv, ciphertext) = +//! TDES_CBC::::encrypt(&key, &plaintext).expect("encryption"); +//! assert_eq!(ciphertext.len(), 56, "50 bytes padded out to seven 8-byte blocks"); +//! +//! let recovered = +//! TDES_CBC::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! For the block-aligned API -- whole blocks in place, with the length checked at compile time -- +//! name `bouncycastle_modes::Cbc` directly; that is what these aliases wrap. +//! +//! There is no one-shot static on the permutation, because `TDES::new(&key)?.encrypt_block(..)` +//! already *is* the one shot. Data-level one-shots belong to the modes of operation, which take +//! arbitrary-length input and generate their own initialisation data. +//! +//! ## What a key bundle must satisfy +//! +//! `TDES::new` 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. +//! +//! ## 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. +//! It sets `ElectronicCodeBook::ENCRYPTION_APPROVED` to `false`, so the `Encrypting` direction of +//! every mode refuses to compile over it, while the `Decrypting` direction works. 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::TDES2_CBC; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::SimpleCipherDecryptor; +//! use bouncycastle_padding::NoPadding; +//! +//! // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0. +//! let key = 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 iv = [0xf2, 0x9d, 0x4b, 0xa5, 0x17, 0x2f, 0xd2, 0x97]; +//! let ciphertext = [0x1b, 0x38, 0x89, 0xc9, 0xdb, 0xe1, 0x0d, 0x03]; +//! +//! let plaintext = TDES2_CBC::::decrypt(&key, &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 [`TDES`] 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. +//! +//! # Memory Usage +//! +//! There are no lookup tables and no heap allocation. The only persistent state is the three DEA +//! key schedules: +//! +//! | Type | Key | Rounds | Schedule (persistent) | Tables | +//! |---|---|---|---|---| +//! | [`TDES`] | 24 B | 3 x 16 | 384 B | 0 B | +//! | [`TDES2Key`] | 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. +//! +//! For comparison, the table-driven `DESEngine` carries 2 KiB of combined S/P tables. +//! +//! Measure with `cargo run --release -p mem_usage_benches --bin bench_tdes_mem_usage`. +//! +//! # 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 [`TDES`] 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 +//! +//! [`TDES`] 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. +//! +//! ## 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 type -- 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; +mod ecb; +mod padded_mode; +mod sbox; +mod schedule; +mod tdes; +mod tdes2; + +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 ecb::{TDES_ECB, TDES2_ECB}; +pub use tdes::{BLOCK_LEN, KEY_LEN, TDES}; +pub use tdes2::{KEY_LEN_2KEY, TDES2Key}; diff --git a/crypto/tdes/src/padded_mode.rs b/crypto/tdes/src/padded_mode.rs new file mode 100644 index 000000000..4d656a5cb --- /dev/null +++ b/crypto/tdes/src/padded_mode.rs @@ -0,0 +1,64 @@ +//! The projection that lets a padded mode alias take its direction *and* its padding scheme. +//! +//! `bouncycastle-padding` splits its adapters by direction: [`PaddedEncryptor`] wraps a +//! [`BlockCipherEncryptor`] and [`PaddedDecryptor`] a [`BlockCipherDecryptor`]. They are two +//! distinct types, and a plain type alias cannot choose between two types based on one of its own +//! parameters, so `TDES_CBC` cannot be written directly. +//! +//! [`PaddedMode`] does it instead. It is implemented for each direction marker, and its associated +//! type is the adapter for that direction, so an alias can be written as a projection through it: +//! +//! ```text +//! pub type TDES_CBC = , // what Encrypting resolves to +//! Cbc, // what Decrypting resolves to +//! Pad, 8, +//! >>::Mode; +//! ``` +//! +//! One trait serves both block modes, since it is parameterised by the encryptor and decryptor +//! types rather than by the mode: CBC passes its two directions and `INIT_DATA_LEN = BLOCK_LEN`, +//! ECB passes its two and `INIT_DATA_LEN = 0`. This is the same device as `bouncycastle-aes`'s. + +use crate::{BLOCK_LEN, KEY_LEN}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, Padding}; +use bouncycastle_modes::{Decrypting, Encrypting}; +use bouncycastle_padding::{PaddedDecryptor, PaddedEncryptor}; + +/// Projects a direction marker onto the padded adapter for that direction. +/// +/// Implemented for [`Encrypting`] and [`Decrypting`] and for nothing else, so those remain the only +/// usable values of a `Dir` parameter. See the module docs for why it exists. +/// +/// `Enc` and `Dec` are the two directions of the underlying block mode, `Pad` is the padding +/// scheme, and `INIT_DATA_LEN` is the mode's: the block length for a mode with an IV, 0 for ECB. +pub trait PaddedMode +where + Enc: BlockCipherEncryptor, + Dec: BlockCipherDecryptor, + Pad: Padding, +{ + /// The padded type for this direction: a [`PaddedEncryptor`] over `Enc`, or a + /// [`PaddedDecryptor`] over `Dec`. + type Mode; +} + +impl PaddedMode + for Encrypting +where + Enc: BlockCipherEncryptor, + Dec: BlockCipherDecryptor, + Pad: Padding, +{ + type Mode = PaddedEncryptor; +} + +impl PaddedMode + for Decrypting +where + Enc: BlockCipherEncryptor, + Dec: BlockCipherDecryptor, + Pad: Padding, +{ + type Mode = PaddedDecryptor; +} diff --git a/crypto/tdes/src/sbox.rs b/crypto/tdes/src/sbox.rs new file mode 100644 index 000000000..4be7ca9cb --- /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..e3743eb01 --- /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 i in 0..n { + c = rotl28(c, SHIFTS[i]); + d = rotl28(d, SHIFTS[i]); + } + let cd = ((c as u64) << 28) | d as u64; + let mut k = 0u64; + for &src in PC2.iter() { + k = (k << 1) | cd_bit(cd, src) as u64; + } + k + } + + #[test] + fn test_pc1_selects_no_parity_bit() { + for &n in PC1.iter() { + assert_ne!(n % 8, 0, "PC-1 must not select parity bit {n}"); + } + // ...and selects each of the other 56 bits exactly once. + let mut seen = [false; 65]; + for &n in PC1.iter() { + assert!(!seen[n as usize], "PC-1 selects bit {n} twice"); + seen[n as usize] = true; + } + assert_eq!(seen.iter().filter(|&&s| s).count(), 56); + } + + #[test] + fn test_pc2_selects_48_distinct_bits() { + let mut seen = [false; 57]; + for &n in PC2.iter() { + assert!((1..=56).contains(&n)); + assert!(!seen[n as usize], "PC-2 selects bit {n} twice"); + seen[n as usize] = true; + } + assert_eq!(seen.iter().filter(|&&s| s).count(), 48); + } + + #[test] + fn test_shifts_total_28() { + // Sixteen iterations bring C and D back to C0 and D0: the shifts sum to 28. + assert_eq!(SHIFTS.iter().sum::(), 28); + } + + #[test] + fn test_packed_round_keys_unpack_to_the_reference_schedule() { + let mut seed = 0x0123_4567_89AB_CDEFu64; + for _ in 0..200 { + seed = seed + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + let key = seed; + let mut sk = [0u32; 32]; + expand(key, &mut sk); + for n in 0..16 { + assert_eq!( + unpack(sk[2 * n], sk[2 * n + 1]), + ks_reference(n + 1, key), + "K{}", + n + 1 + ); + // Only bits 0 and 1 of each nibble of `hi` are ever set. + assert_eq!(sk[2 * n + 1] & !0x3333_3333, 0); + } + } + } + + #[test] + fn test_parity_bits_do_not_affect_the_schedule() { + let key = 0x0123_4567_89AB_CDEFu64; + let mut a = [0u32; 32]; + let mut b = [0u32; 32]; + expand(key, &mut a); + expand(key ^ !PARITY_MASK, &mut b); + assert_eq!(a, b); + } +} diff --git a/crypto/tdes/src/tdes.rs b/crypto/tdes/src/tdes.rs new file mode 100644 index 000000000..e1f03af7f --- /dev/null +++ b/crypto/tdes/src/tdes.rs @@ -0,0 +1,252 @@ +//! 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 +//! [`crate::tdes2`] and shares the checks defined here. + +use crate::des::{Subkeys, inverse_ip, ip, rounds}; +use crate::schedule::{PARITY_MASK, expand, is_weak}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_utils::ct::ct_eq_zero_bytes; +use bouncycastle_utils::secret::Secret; + +/// 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; + +pub(crate) type Block = [u8; BLOCK_LEN]; + +/// The three-key TDEA keyed permutation (SP 800-67r2 Sec 3.1), "3TDEA". +/// +/// 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. See `TDES::new` for what is rejected and +/// why; for two-key TDEA, which is decryption-only, see [`crate::TDES2Key`]. See the crate docs +/// for the standing of TDEA today. +pub struct TDES { + schedule: Secret<[Subkeys; 3]>, +} + +/// 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(()) +} + +impl TDES { + /// 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 type, + /// [`crate::TDES2Key`], 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. + pub(crate) 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 }) + } + + /// The TDEA forward cipher operation, `O = F_Key3(I_Key2(F_Key1(d)))` (Sec 3.1), in place. + /// + /// 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`]. + pub(crate) 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. + pub(crate) fn decrypt_block(&self, block: &mut Block) { + let [k1, k2, k3] = &*self.schedule; + inverse(block, k1, k2, k3); + } +} + +/// `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. +#[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 TDES { + 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 TDES { + fn new(key: &KeyMaterial) -> Result { + TDES::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + TDES::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + TDES::decrypt_block(self, block) + } + // The pair and four-block methods keep their defaults: the engine has no natural unit larger + // than one block, so there is nothing to gain from overriding them. +} + +impl core::fmt::Debug for TDES { + /// 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_engine_size_is_exactly_the_three_schedules() { + // The "Memory Usage" table in the crate docs quotes this. No direction flag, no counters: + // three DEA keys of sixteen two-word round keys. + assert_eq!(size_of::(), 3 * 16 * 2 * 4); + assert_eq!(size_of::(), 384); + } + + #[test] + fn test_alg_name_and_strength() { + assert_eq!(::ALG_NAME, "TDES"); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert!(>::ENCRYPTION_APPROVED); + } + + #[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/tdes2.rs b/crypto/tdes/src/tdes2.rs new file mode 100644 index 000000000..e091188fc --- /dev/null +++ b/crypto/tdes/src/tdes2.rs @@ -0,0 +1,150 @@ +//! Two-key TDEA (SP 800-67r2 Sec 3.1, "2TDEA"), for decryption of legacy data only. + +use crate::des::Subkeys; +use crate::schedule::expand; +use crate::tdes::{ + BLOCK_LEN, Block, REPEATED_KEY, any_weak, forward, inverse, same_key, validate_wrapper, +}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_utils::secret::Secret; + +/// 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; + +/// The two-key TDEA keyed permutation (SP 800-67r2 Sec 3.1, "2TDEA"): `Key3 = Key1`, so the bundle +/// is the 16 bytes `Key1 || Key2`. +/// +/// **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 `Encrypting` mode in +/// `bouncycastle-modes` refuses to be built over it **at compile time**: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::{Cbc, Encrypting}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// +/// // Two-key TDEA may not apply protection: this does not compile. +/// let _ = Cbc::::do_encrypt_init(&key); +/// ``` +/// +/// The `Decrypting` direction of every mode works, and this crate provides the aliases +/// [`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) for it. The raw +/// [`ElectronicCodeBook::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`]. +pub struct TDES2Key { + schedule: Secret<[Subkeys; 2]>, +} + +impl TDES2Key { + /// Expands a 16-byte key bundle `Key1 || Key2` into the two DEA key schedules. + /// + /// Parity bits are ignored, as for [`crate::TDES`]. + /// + /// # 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. + pub(crate) 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 }) + } + + /// 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. + pub(crate) 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)))`. + pub(crate) fn decrypt_block(&self, block: &mut Block) { + let [k1, k2] = &*self.schedule; + inverse(block, k1, k2, k1); + } +} + +impl Algorithm for TDES2Key { + 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 TDES2Key { + fn new(key: &KeyMaterial) -> Result { + TDES2Key::new(key) + } + /// SP 800-131A Rev 2 Table 1: two-key TDEA encryption is disallowed. + const ENCRYPTION_APPROVED: bool = false; + fn encrypt_block(&self, block: &mut Block) { + TDES2Key::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + TDES2Key::decrypt_block(self, block) + } +} + +impl core::fmt::Debug for TDES2Key { + /// 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_engine_size_is_exactly_the_two_schedules() { + assert_eq!(size_of::(), 2 * 16 * 2 * 4); + assert_eq!(size_of::(), 256); + } + + #[test] + fn test_alg_name_strength_and_gate() { + assert_eq!(::ALG_NAME, "TDES-2KEY"); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert!(!>::ENCRYPTION_APPROVED); + } +} diff --git a/crypto/tdes/summary.md b/crypto/tdes/summary.md new file mode 100644 index 000000000..387b19692 --- /dev/null +++ b/crypto/tdes/summary.md @@ -0,0 +1,167 @@ +# `crypto/tdes` — implementation summary + +A constant-time, table-free Triple DES (TDEA) block cipher engine, NIST SP 800-67 Rev 2, added on +branch `feature/tdes` following the layout of `crypto/aes`. + +This document is the reviewer's orientation: what was built, why the design is the way it is, what +was verified and how, and the decisions a reviewer may want to revisit. For end-user documentation +see the crate docs in [`src/lib.rs`](src/lib.rs); for the derivation of every constant, see the +module docs in [`src/sbox.rs`](src/sbox.rs), [`src/des.rs`](src/des.rs) and +[`src/schedule.rs`](src/schedule.rs), which are the right place to start reading the source. + +--- + +## 1. What this crate is (and is not) + +It provides the **raw three-key TDEA keyed permutation**, `TDES`, transforming exactly 8 bytes at a +time, plus type aliases (`TDES_CBC`, `TDES_CFB`, `TDES_CFB8`, `TDES_CTR`, `TDES_ECB`) over +`bouncycastle-modes` that fix the key and block lengths; and a **decryption-only two-key** +permutation, `TDES2Key` (16-byte `Key1 || Key2`), with `TDES2_*` decryptor aliases. Consistent +with `crypto/aes`: + +* the permutation implements `core::traits::ElectronicCodeBook` and `Algorithm` only; the + data-encryption traits belong to the modes, +* no factory registration (the factories have no block-cipher slot), +* no `AlgorithmOID`, +* one `bc-rust` subcommand per mode: `tdes-cbc`, `tdes-cfb`, `tdes-cfb8`, `tdes-ctr`, `tdes-ecb`, + and their decrypt-only `tdes2-*` counterparts. + +It is a **legacy algorithm**. SP 800-67 Rev 2 was withdrawn on 1 January 2024 and SP 800-131A Rev 2 +disallows three-key TDEA encryption after 2023 (decryption is "legacy use"). The crate docs say so +prominently; the crate exists for data and protocols that still depend on TDEA. + +--- + +## 2. Design + +### 2.1 The S-box layer is a compile-time-derived Boolean circuit + +The eight S-boxes are viewed as thirty-two 6-to-1-bit functions ("T-boxes", BearSSL's term). Each +has an algebraic normal form, and the 64 ANF coefficient words are computed **at compile time** by a +`const fn` Möbius transform of the S-box tables transcribed from Appendix A. The round function +evaluates all thirty-two ANFs at once by Horner's scheme: 63 ANDs and 63 XORs on `u32` words, with +each S-box's six input bits held one per word and replicated across the four bit positions of its +output nibble. + +Why this rather than Kwan's per-S-box gate circuits (the usual "bitslice DES"): those need many +blocks in parallel to fill a word, whereas this runs all eight S-boxes of *one* block in parallel +across the 32 bit positions; and the only DES-specific constants in the source are the spec's own +tables, laid out as printed, so a reviewer can check them against the document by eye. The runtime +never indexes a table. + +### 2.2 E, P, IP and the key schedule + +* `E` is never materialised. Block `i` of `E(R)` is the `i`-th nibble of `R` plus the last bit of + the nibble above and the first bit of the nibble below, so the six input planes are built with + masks, shifts and two rotations by a whole nibble (`des::planes`). +* `P` is nineteen mask-and-rotate groups generated from Table 3; each line's comment lists the + spec input bits it moves. +* `IP` / `IP^-1` are Outerbridge's five-swap transposition network (via Crypto++ and BearSSL). In + TDEA the `IP^-1` ending one DEA transformation cancels the `IP` starting the next, so each is + applied once around the 48 rounds. No spec-defined intermediate value changes. +* The key schedule iterates the PC-1 and PC-2 tables directly (public indices, public shift + amounts), and stores each 48-bit round key as two words shaped to be XORed straight into the + plane layout: 128 bytes per DEA key, 384 per bundle, no per-round rearrangement. The inverse + transformation reads the same schedule backwards (Sec 2.2), so one `TDES` serves both directions. + +### 2.3 Key-bundle checks + +`TDES::new` takes `KeyMaterial<24>` (`Key1 || Key2 || Key3`) and rejects, in this order: wrong +`KeyType`; wrong length; security strength below 112 bits; component keys that are not pairwise +distinct; any component that is a weak key. Parity bits are ignored throughout (Appendix A says +they "have no effect on the operation of the algorithm"). + +The weak-key test is **structural**, not a table comparison: a DES key is weak, semi-weak or +possibly weak exactly when `C0` and `D0` each have period 4 under rotation. That gives 8 × 8 = 64 +keys, and `tests/key_tests.rs` proves they are precisely the 4 + 12 + 48 keys Sec 3.3.2 lists, by +reconstructing all 64 through inverse PC-1 and comparing sets. The check is two rotations and a +constant-time zero test. + +Rejected errors are `KeyMaterialError::WeakKey(&str)`, a variant added to `core` for this (the enum +is `#[non_exhaustive]`). + +### 2.4 Two-key TDEA is a separate, decryption-only type + +`TDES` rejects `Key1 = Key3`; `TDES2Key` takes the 16-byte `Key1 || Key2` instead. SP 800-67r2 +Sec 3.1 allows 2TDEA "for legacy use only, as defined in SP 800-131A", and SP 800-131Ar2 Table 1 +has it "Disallowed" for encryption, "Legacy use" for decryption. To make that a property of the +type rather than of the documentation, `core::traits::ElectronicCodeBook` gained an associated +`const ENCRYPTION_APPROVED: bool = true`, and every `Encrypting` mode in `bouncycastle-modes` +asserts it in an inline `const` when constructed. `TDES2Key` sets it `false`, so +`Cbc` (and the other four) fail to compile; the `Decrypting` directions +are untouched and the `TDES2_*` aliases name them. The raw `encrypt_block` remains, because CFB and +CTR decryption are built on the forward function. `MAX_SECURITY_STRENGTH` is `None`: 2TDEA is +"<= 80" bits (SP 800-57pt1r5 Table 2), below the lowest modelled level, and there is no honest +higher value. bc-java's `DESedeEngine` accepts 16-byte keys in both directions (and reports 80 bits). + +### 2.5 Decisions a reviewer may want to revisit + +1. **`ENCRYPTION_APPROVED` lives on the permutation trait**, with the gate in the modes. The + alternative -- a decrypt-only trait -- would need every mode's `Decrypting` impl duplicated. +2. **All 64 weak keys are rejected**, where bc-java's `DESedeParameters` rejects the 16 weak and + semi-weak. The spec says all three lists "should be avoided". The structural test makes 64 no + more code than 16. +3. **Component keys are compared with parity masked**, so two keys differing only in parity are a + repeat. They drive the engine identically, so the bundle really would collapse. +4. **`TDES_CTR` uses a 6-byte nonce and a 2-byte counter** (512 KiB per message). The trade-off, + with numbers, is in `src/ctr.rs`. Naming `bouncycastle_modes::Ctr` directly gives another split. +5. **No `encrypt_2blocks` / `encrypt_4blocks` override, and speed was not the goal.** Criterion on + the development machine (`cargo bench -p bouncycastle-tdes`): about 3.8 µs for `TDES::new` + and about 4.1 µs per block in either direction, i.e. roughly 1.9 MiB/s. A table-driven Triple + DES is around an order of magnitude faster; the price here buys the absence of any + secret-indexed memory access. A two-lane `u64` variant would roughly double bulk throughput + for parallel modes at the cost of lane-masked rotations everywhere; not done for a legacy + cipher. +6. **`ALG_NAME` is `"TDES"`** (bc-java: `"DESede"`; NIST: `"TDEA"`). + +--- + +## 3. Verification + +| What | How | +|---|---| +| S-boxes | Exhaustive: 8 × 64 inputs through the circuit vs. table lookup; every row a permutation of 0..15; `ANF[63] == 0` (balancedness) | +| `IP`, `IP^-1`, `P`, `E` | Every unit vector against the spec table (a proof for a bit permutation); `IP^-1 ∘ IP = id` on the tables themselves | +| Key schedule | Packed round keys unpacked and compared with a long-hand `KS(n, KEY)`; PC-1 selects no parity bit and each other bit once; shifts sum to 28 | +| Weak keys | The 64 listed keys rejected in every position with either parity; set equality with the structural 64; one-bit-off keys accepted | +| Whole engine | NIST CAVP `TECBMMT3`, `TCBCMMT3`, `TCFB64MMT3`, `TCFB8MMT3` (20 cases each, both sections, both directions), through the `TDES_*` aliases with a pinned RNG supplying the vector's IV; the `*MMT2` files for `TDES2Key` | +| Trait contract | `TestFrameworkElectronicCodeBook` (incl. the 112-bit security-strength policy) | +| Aliases | Both parameters select; no IV for ECB; schemes not interchangeable; fresh IV per encryption | + +The two-key files `TECBMMT2`, `TCBCMMT2`, `TCFB64MMT2`, `TCFB8MMT2` are checked the same way for +`TDES2Key`, except that CBC/CFB are decrypt-only (both sections decrypted) since the encrypting +modes do not compile over it. + +The CAVP Monte Carlo files were downloaded but not used: their update rule is SP 800-20's and is a +separate piece of work. + +The vectors are NIST's `tdesmmt.zip` response files, added to the `bc-test-data` repository under +`crypto/aes_tdes_vectors/TDES/` (all 22 files); `tests/cavp_mmt_tests.rs` parses the `.rsp` format +and skips with a warning when the repository is absent, as the AES ACVP tests do. + +--- + +## 4. Where the plan changed + +* The first plan was to store the six expanded key planes per round (1152 bytes). The compact + two-word packing costs one spread per plane per round and saves 768 bytes; the middle four + planes' key XOR folds into the data spread, so the cost is four spreads, not six. +* Two-key TDEA was first rejected outright. It came back as `TDES2Key`, decryption-only, once + the `ENCRYPTION_APPROVED` gate gave a way to say "decryption only" in the type system. +* The vectors were first embedded in the crate as a generated Rust module; they moved to + `bc-test-data` (the repository the other CAVP suites already depend on). +* Weak keys were first going to be a 16-entry table, as in bc-java. Working out why the semi-weak + and possibly-weak lists have the shapes they do gave the period-4 characterisation, which covers + all 64 with no table and turned the transcription of the 48 into a *test* of that claim. +* The CLI's `block_mode_cmd` had `BLOCK_LEN = 16` baked in; it is now a const parameter of the + streaming loops and `CHUNK_LEN` is a fixed 1 KiB that both block lengths divide. + +--- + +## 5. Sources consulted + +Downloaded fresh into the session scratchpad, read, and quoted: SP 800-67 Rev 2 (withdrawn), FIPS +46-3 (Appendix 1 cross-check), SP 800-38A (Appendix E), SP 800-131A Rev 2 (Table 1), SP 800-57 +Part 1 Rev 5 (Table 2), SP 800-20 (for the MMT file semantics), CAVP `tdesmmt.zip`. Reference +implementations read for design and behaviour, not copied: BearSSL `des_ct.c` / `des_support.c`, +bc-java `DESEngine`, `DESedeEngine`, `DESParameters`, `DESedeParameters`. diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs new file mode 100644 index 000000000..65cbcdbae --- /dev/null +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -0,0 +1,375 @@ +//! 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 +//! [`TDES`]) 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 `bouncycastle-modes` has no API for a caller-supplied +//! IV. Three-key encryption therefore uses `do_encrypt_init_rng` with 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 `TDES_*` aliases are used, so the aliases' +//! parameter choices (a 24-byte key, an 8-byte block, an 8-byte IV) are what is under test. +//! +//! Two-key TDEA is decryption-only -- the encrypting modes do not compile over [`TDES2Key`] -- so +//! its CBC and CFB vectors are checked by decrypting `CIPHERTEXT` through the `TDES2_*` aliases +//! (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_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, StreamCipherDecryptor, + StreamCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use bouncycastle_tdes::{ + BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES_CFB, TDES_CFB8, TDES2_CFB, TDES2_CFB8, TDES2Key, +}; +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 key3(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 key2(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 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 default 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 = TDES::new(&key3(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 = TDES2Key::new(&key2(case)).expect("a valid CAVP two-key bundle"); + check_ecb("TECBMMT2", case, &tdes); + } +} + +#[test] +fn tcbcmmt3() { + type Enc = Cbc; + type Dec = Cbc; + 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) = Enc::do_encrypt_init_rng(&key, &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(); + for block in buf.as_chunks_mut::().0 { + enc.do_encrypt(block).unwrap(); + } + assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + + let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + for block in buf.as_chunks_mut::().0 { + dec.do_decrypt(block).unwrap(); + } + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + } +} + +#[test] +fn tcbcmmt2_decrypts() { + type Dec = Cbc; + let Some(cases) = load("TCBCMMT2.rsp") else { return }; + + for case in &cases { + let name = label("TCBCMMT2", case); + let mut dec = Dec::do_decrypt_init(&key2(case), &iv(case)).unwrap(); + let mut buf = case.ciphertext.clone(); + for block in buf.as_chunks_mut::().0 { + dec.do_decrypt(block).unwrap(); + } + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + } +} + +/// CFB64 and CFB8 share a shape: stream cipher, 8-byte IV, any length. Three-key: both directions. +fn check_stream3(file: &str) +where + E: StreamCipherEncryptor, + D: StreamCipherDecryptor, +{ + let Some(cases) = load(file) else { return }; + for case in &cases { + let key = key3(case); + let iv = iv(case); + let name = label(file, case); + + let (mut enc, got_iv) = E::do_encrypt_init_rng(&key, &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(&mut buf).unwrap(); + assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + + let mut dec = D::do_decrypt_init(&key, &iv).unwrap(); + dec.do_decrypt(&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, _) = E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::new(iv)).unwrap(); + let mut buf = case.plaintext.clone(); + for byte in buf.iter_mut() { + enc.do_encrypt(core::slice::from_mut(byte)).unwrap(); + } + assert_eq!(buf, case.ciphertext, "{name}: encrypt byte by byte"); + } +} + +/// Two-key: decryption only, whole and byte by byte. +fn check_stream2(file: &str) +where + D: StreamCipherDecryptor, +{ + let Some(cases) = load(file) else { return }; + for case in &cases { + let key = key2(case); + let iv = iv(case); + let name = label(file, case); + + let mut dec = D::do_decrypt_init(&key, &iv).unwrap(); + let mut buf = case.ciphertext.clone(); + dec.do_decrypt(&mut buf).unwrap(); + assert_eq!(buf, case.plaintext, "{name}: decrypt"); + + let mut dec = D::do_decrypt_init(&key, &iv).unwrap(); + let mut buf = case.ciphertext.clone(); + for byte in buf.iter_mut() { + dec.do_decrypt(core::slice::from_mut(byte)).unwrap(); + } + assert_eq!(buf, case.plaintext, "{name}: decrypt byte by byte"); + } +} + +#[test] +fn tcfb64mmt3() { + check_stream3::, TDES_CFB>("TCFB64MMT3.rsp"); +} + +#[test] +fn tcfb8mmt3() { + check_stream3::, TDES_CFB8>("TCFB8MMT3.rsp"); +} + +#[test] +fn tcfb64mmt2_decrypts() { + check_stream2::("TCFB64MMT2.rsp"); +} + +#[test] +fn tcfb8mmt2_decrypts() { + check_stream2::("TCFB8MMT2.rsp"); +} + +#[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/cbc_alias_tests.rs b/crypto/tdes/tests/cbc_alias_tests.rs new file mode 100644 index 000000000..5a1be7be5 --- /dev/null +++ b/crypto/tdes/tests/cbc_alias_tests.rs @@ -0,0 +1,119 @@ +//! Tests for the padded TDES-CBC alias. +//! +//! The alias is only a type alias, so what is worth testing is that it names the *right* types and +//! 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 their own crates; this checks the wiring between them at the 8-byte block. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use bouncycastle_padding::{NoPadding, PKCS7, PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES_CBC}; + +fn key() -> KeyMaterial { + 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") +} + +/// The alias must resolve to exactly the adapters it claims to, at both directions. +#[test] +fn the_alias_names_the_expected_types() { + use core::mem::size_of; + + assert_eq!( + size_of::>(), + size_of::< + PaddedEncryptor< + Cbc, + PKCS7, + KEY_LEN, + BLOCK_LEN, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedDecryptor< + Cbc, + PKCS7, + KEY_LEN, + 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::>()); +} + +/// Round trips at lengths that need padding and lengths that do not, and the IV is one 8-byte block. +#[test] +fn round_trips_at_every_alignment() { + 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) = + TDES_CBC::::encrypt(&key(), &plaintext).expect("encryption"); + assert_eq!(iv.len(), BLOCK_LEN); + assert_eq!( + ciphertext.len(), + (len / 8 + 1) * 8, + "len {len}: PKCS7 pads up to the next whole block" + ); + + let recovered = + TDES_CBC::::decrypt(&key(), &iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// The padding parameter must actually select the scheme, not merely be carried around. +#[test] +fn the_padding_parameter_selects_the_scheme() { + type Pkcs7Enc = TDES_CBC; + type NoPadEnc = TDES_CBC; + + let aligned = [0x5Au8; 8]; + let (_, pkcs7) = Pkcs7Enc::encrypt(&key(), &aligned).expect("PKCS7 accepts aligned data"); + let (_, nopad) = NoPadEnc::encrypt(&key(), &aligned).expect("NoPadding accepts it too"); + assert_eq!(pkcs7.len(), 16, "PKCS7 adds a whole block of padding to aligned data"); + assert_eq!(nopad.len(), 8, "NoPadding adds nothing"); + + let unaligned = b"hello"; + assert!(Pkcs7Enc::encrypt(&key(), unaligned).is_ok(), "PKCS7 pads a partial block"); + assert!(NoPadEnc::encrypt(&key(), unaligned).is_err(), "NoPadding must refuse a partial block"); +} + +/// A ciphertext made under one scheme must not decrypt cleanly under the other. +#[test] +fn the_two_schemes_are_not_interchangeable() { + let aligned = [0x5Au8; 8]; + let (iv, pkcs7) = TDES_CBC::::encrypt(&key(), &aligned).expect("encryption"); + + let as_nopad = TDES_CBC::::decrypt(&key(), &iv, &pkcs7) + .expect("NoPadding cannot tell that the trailing block is padding"); + assert_ne!(as_nopad, aligned); + assert_eq!(as_nopad.len(), 16, "it keeps the padding block as data"); + + let correct = TDES_CBC::::decrypt(&key(), &iv, &pkcs7).expect("decryption"); + assert_eq!(correct, aligned); +} + +/// The IV is generated per encryption, so the same plaintext gives different ciphertext. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let plaintext = [0x77u8; 16]; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let (iv, ct) = TDES_CBC::::encrypt(&key(), &plaintext).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions"); + let back = TDES_CBC::::decrypt(&key(), &iv, &ct).unwrap(); + assert_eq!(back, plaintext); + } +} diff --git a/crypto/tdes/tests/ecb_alias_tests.rs b/crypto/tdes/tests/ecb_alias_tests.rs new file mode 100644 index 000000000..b74651d2b --- /dev/null +++ b/crypto/tdes/tests/ecb_alias_tests.rs @@ -0,0 +1,112 @@ +//! Tests for the padded TDES-ECB alias. +//! +//! As with the CBC alias, this is only a type alias, so what is worth testing is that both +//! parameters select: the direction picks the encryptor or the decryptor, and the padding scheme +//! reaches the behaviour. ECB's own properties are tested in `bouncycastle-modes`; what is specific +//! here is the 8-byte block, and that `INIT_DATA_LEN` is 0 so the projection carries a different +//! value than CBC's. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; +use bouncycastle_padding::{NoPadding, PKCS7, PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES_ECB}; + +fn key() -> KeyMaterial { + 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") +} + +/// The alias must resolve to exactly the adapters it claims to, with `INIT_DATA_LEN = 0`. +#[test] +fn the_alias_names_the_expected_types() { + use core::mem::size_of; + + assert_eq!( + size_of::>(), + size_of::< + PaddedEncryptor< + Ecb, + PKCS7, + KEY_LEN, + 0, + BLOCK_LEN, + >, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedDecryptor< + Ecb, + PKCS7, + KEY_LEN, + 0, + BLOCK_LEN, + >, + >() + ); +} + +/// ECB has no IV, so the init data is an empty array and the ciphertext is exactly the padded +/// plaintext with nothing prepended. +#[test] +fn there_is_no_iv() { + let (no_iv, ciphertext) = + TDES_ECB::::encrypt(&key(), 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 = TDES_ECB::::decrypt(&key(), &no_iv, &ciphertext).unwrap(); + assert_eq!(recovered, b"hello"); +} + +/// Round trips at lengths that need padding and lengths that do not, around the 8-byte block. +#[test] +fn round_trips_at_every_alignment() { + for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 64] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (no_iv, ciphertext) = + TDES_ECB::::encrypt(&key(), &plaintext).expect("encryption"); + assert_eq!(no_iv, [0u8; 0]); + assert_eq!( + ciphertext.len(), + (len / 8 + 1) * 8, + "len {len}: PKCS7 pads up to the next whole block" + ); + + let recovered = TDES_ECB::::decrypt(&key(), &no_iv, &ciphertext) + .expect("decryption"); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// The padding parameter must select the scheme here too. +#[test] +fn the_padding_parameter_selects_the_scheme() { + let aligned = [0x5Au8; 8]; + let (_, pkcs7) = TDES_ECB::::encrypt(&key(), &aligned).expect("PKCS7"); + let (_, nopad) = + TDES_ECB::::encrypt(&key(), &aligned).expect("NoPadding"); + assert_eq!(pkcs7.len(), 16, "PKCS7 adds a whole block to aligned data"); + assert_eq!(nopad.len(), 8, "NoPadding adds nothing"); + + assert!( + TDES_ECB::::encrypt(&key(), b"hello").is_err(), + "NoPadding must refuse a partial block" + ); +} + +/// Padding does not fix ECB: identical plaintext blocks still give identical ciphertext blocks, and +/// the same message under the same key always gives the same ciphertext. +#[test] +fn padding_does_not_hide_the_codebook_property() { + let (_, ciphertext) = + TDES_ECB::::encrypt(&key(), &[0x5Au8; 16]).unwrap(); + assert_eq!(ciphertext[..8], ciphertext[8..], "ECB is a codebook, padded or not"); + + let (_, a) = TDES_ECB::::encrypt(&key(), b"hello").unwrap(); + let (_, b) = TDES_ECB::::encrypt(&key(), b"hello").unwrap(); + assert_eq!(a, b, "the same message encrypts the same way every time"); +} 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..416b790eb --- /dev/null +++ b/crypto/tdes/tests/electronic_code_book_tests.rs @@ -0,0 +1,26 @@ +//! `ElectronicCodeBook` trait conformance, 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::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; + +#[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..d3109dfa8 --- /dev/null +++ b/crypto/tdes/tests/key_tests.rs @@ -0,0 +1,326 @@ +//! Key-bundle handling: what `TDES::new` accepts and rejects, and why. +//! +//! * 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::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; + +/// 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!(TDES::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(TDES::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(TDES::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!(TDES::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(TDES::new(&bundle([a, a, c]))), "Key1 = Key2"); + assert!(is_weak_key_error(TDES::new(&bundle([a, b, b]))), "Key2 = Key3"); + assert!(is_weak_key_error(TDES::new(&bundle([a, b, a]))), "Key1 = Key3 (two-key TDEA)"); + assert!(is_weak_key_error(TDES::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(TDES::new(&bundle([a, a ^ !PARITY_MASK, c]))), + "parity-only difference" + ); +} + +#[test] +fn parity_bits_do_not_change_the_cipher() { + let good = TDES::new(&bundle(GOOD)).unwrap(); + let flipped = TDES::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 = TDES::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 { + continue; // a parity bit: bits 8, 16, ..., 64 in spec numbering are word bits 56, 48, .., 0 + } + let mut k = GOOD; + k[position] ^= 1u64 << bit; + let mut out = block; + TDES::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!( + TDES::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!( + TDES::new(&key), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) + )); + do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::_112bit)) + .unwrap(); + assert!(TDES::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!( + TDES::new(&zero), + Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); +} + +#[test] +fn debug_output_names_the_algorithm_only() { + let tdes = TDES::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 (TDES 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 = TDES2Key::new(&bundle2([a, b])).unwrap(); + let three = TDES::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(TDES2Key::new(&bundle2([a, a]))), "Key1 = Key2 is single DES"); + assert!( + is_weak_key_error2(TDES2Key::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(TDES2Key::new(&bundle2([weak, b]))), "{weak:016x} as Key1"); + assert!(is_weak_key_error2(TDES2Key::new(&bundle2([a, weak]))), "{weak:016x} as Key2"); + } + assert!(TDES2Key::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!( + TDES2Key::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!(TDES2Key::new(&key).is_ok()); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!(::ALG_NAME, "TDES-2KEY"); + assert!(!>::ENCRYPTION_APPROVED); + assert!(>::ENCRYPTION_APPROVED); + assert_eq!(format!("{:?}", TDES2Key::new(&bundle2([a, b])).unwrap()), "TDES-2KEY"); +} 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..570c2a0b2 --- /dev/null +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -0,0 +1,106 @@ +//! 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: +//! +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null +//! +//! ms_print massif.out.835000 +//! +//! or, shoved all into one line: +//! +//! 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 +//! +//! Like AES, TDES has no interesting stack profile: peak usage is a small constant plus the key +//! schedule. The numbers worth recording in the crate docs are the persistent size of the engine +//! (three packed DEA schedules, 384 bytes) and the confirmation that per-block work is a fixed, +//! small amount of stack -- the six input planes and the 128-byte working array of the S-box +//! layer's ANF evaluation. +//! +//! The point of comparison is that a table-driven Triple DES carries 2 KiB of combined S/P tables +//! on top of its schedules; this implementation carries none. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::core::traits::ElectronicCodeBook; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES2Key}; + +/// 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 the engine, i.e. the persistent cost of holding a key schedule. +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::()); +} + +fn key() -> KeyMaterial { + // A fixed pattern whose three 8-byte components are distinct and not weak; an all-zero buffer + // would be tagged KeyType::Zeroized and rejected. + let mut bytes = [0u8; KEY_LEN]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(7).wrapping_add(1); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_tdes_key_expansion() { + eprintln!("TDES::new (key expansion)"); + + let tdes = TDES::new(&key()).unwrap(); + print!("{tdes:?}"); +} + +fn bench_tdes_encrypt_block() { + eprintln!("TDES::encrypt_block"); + + let tdes = TDES::new(&key()).unwrap(); + let mut block = [0x11u8; BLOCK_LEN]; + tdes.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_tdes_decrypt_block() { + eprintln!("TDES::decrypt_block"); + + let tdes = TDES::new(&key()).unwrap(); + let mut block = [0x11u8; BLOCK_LEN]; + tdes.decrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_tdes_encrypt_4blocks() { + eprintln!("TDES::encrypt_4blocks"); + + let tdes = TDES::new(&key()).unwrap(); + let mut blocks = [[0x11u8; BLOCK_LEN]; 4]; + tdes.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_tdes_key_expansion() + // bench_tdes_encrypt_block() + // bench_tdes_decrypt_block() + // bench_tdes_encrypt_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; From b2d327e9f45fa5f0d4a9e53b91da84d438addf37 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 19 Sep 2026 10:14:31 +1000 Subject: [PATCH 06/32] tdes: drop the summary.md reviewer-orientation doc summary.md duplicated the crate docs' content and added a "Where the plan changed" section citing decisions from an internal design session that isn't part of this repo, plus several bc-java class-name references not needed once the doc itself is gone. The crate docs in src/lib.rs already cover the design, verification and the decisions a reviewer would want to revisit. No behavioural change; cargo fmt --check is clean. Assisted-by: Claude:claude-sonnet-5 Co-Authored-By: Claude Sonnet 5 --- crypto/tdes/summary.md | 167 ----------------------------------------- 1 file changed, 167 deletions(-) delete mode 100644 crypto/tdes/summary.md diff --git a/crypto/tdes/summary.md b/crypto/tdes/summary.md deleted file mode 100644 index 387b19692..000000000 --- a/crypto/tdes/summary.md +++ /dev/null @@ -1,167 +0,0 @@ -# `crypto/tdes` — implementation summary - -A constant-time, table-free Triple DES (TDEA) block cipher engine, NIST SP 800-67 Rev 2, added on -branch `feature/tdes` following the layout of `crypto/aes`. - -This document is the reviewer's orientation: what was built, why the design is the way it is, what -was verified and how, and the decisions a reviewer may want to revisit. For end-user documentation -see the crate docs in [`src/lib.rs`](src/lib.rs); for the derivation of every constant, see the -module docs in [`src/sbox.rs`](src/sbox.rs), [`src/des.rs`](src/des.rs) and -[`src/schedule.rs`](src/schedule.rs), which are the right place to start reading the source. - ---- - -## 1. What this crate is (and is not) - -It provides the **raw three-key TDEA keyed permutation**, `TDES`, transforming exactly 8 bytes at a -time, plus type aliases (`TDES_CBC`, `TDES_CFB`, `TDES_CFB8`, `TDES_CTR`, `TDES_ECB`) over -`bouncycastle-modes` that fix the key and block lengths; and a **decryption-only two-key** -permutation, `TDES2Key` (16-byte `Key1 || Key2`), with `TDES2_*` decryptor aliases. Consistent -with `crypto/aes`: - -* the permutation implements `core::traits::ElectronicCodeBook` and `Algorithm` only; the - data-encryption traits belong to the modes, -* no factory registration (the factories have no block-cipher slot), -* no `AlgorithmOID`, -* one `bc-rust` subcommand per mode: `tdes-cbc`, `tdes-cfb`, `tdes-cfb8`, `tdes-ctr`, `tdes-ecb`, - and their decrypt-only `tdes2-*` counterparts. - -It is a **legacy algorithm**. SP 800-67 Rev 2 was withdrawn on 1 January 2024 and SP 800-131A Rev 2 -disallows three-key TDEA encryption after 2023 (decryption is "legacy use"). The crate docs say so -prominently; the crate exists for data and protocols that still depend on TDEA. - ---- - -## 2. Design - -### 2.1 The S-box layer is a compile-time-derived Boolean circuit - -The eight S-boxes are viewed as thirty-two 6-to-1-bit functions ("T-boxes", BearSSL's term). Each -has an algebraic normal form, and the 64 ANF coefficient words are computed **at compile time** by a -`const fn` Möbius transform of the S-box tables transcribed from Appendix A. The round function -evaluates all thirty-two ANFs at once by Horner's scheme: 63 ANDs and 63 XORs on `u32` words, with -each S-box's six input bits held one per word and replicated across the four bit positions of its -output nibble. - -Why this rather than Kwan's per-S-box gate circuits (the usual "bitslice DES"): those need many -blocks in parallel to fill a word, whereas this runs all eight S-boxes of *one* block in parallel -across the 32 bit positions; and the only DES-specific constants in the source are the spec's own -tables, laid out as printed, so a reviewer can check them against the document by eye. The runtime -never indexes a table. - -### 2.2 E, P, IP and the key schedule - -* `E` is never materialised. Block `i` of `E(R)` is the `i`-th nibble of `R` plus the last bit of - the nibble above and the first bit of the nibble below, so the six input planes are built with - masks, shifts and two rotations by a whole nibble (`des::planes`). -* `P` is nineteen mask-and-rotate groups generated from Table 3; each line's comment lists the - spec input bits it moves. -* `IP` / `IP^-1` are Outerbridge's five-swap transposition network (via Crypto++ and BearSSL). In - TDEA the `IP^-1` ending one DEA transformation cancels the `IP` starting the next, so each is - applied once around the 48 rounds. No spec-defined intermediate value changes. -* The key schedule iterates the PC-1 and PC-2 tables directly (public indices, public shift - amounts), and stores each 48-bit round key as two words shaped to be XORed straight into the - plane layout: 128 bytes per DEA key, 384 per bundle, no per-round rearrangement. The inverse - transformation reads the same schedule backwards (Sec 2.2), so one `TDES` serves both directions. - -### 2.3 Key-bundle checks - -`TDES::new` takes `KeyMaterial<24>` (`Key1 || Key2 || Key3`) and rejects, in this order: wrong -`KeyType`; wrong length; security strength below 112 bits; component keys that are not pairwise -distinct; any component that is a weak key. Parity bits are ignored throughout (Appendix A says -they "have no effect on the operation of the algorithm"). - -The weak-key test is **structural**, not a table comparison: a DES key is weak, semi-weak or -possibly weak exactly when `C0` and `D0` each have period 4 under rotation. That gives 8 × 8 = 64 -keys, and `tests/key_tests.rs` proves they are precisely the 4 + 12 + 48 keys Sec 3.3.2 lists, by -reconstructing all 64 through inverse PC-1 and comparing sets. The check is two rotations and a -constant-time zero test. - -Rejected errors are `KeyMaterialError::WeakKey(&str)`, a variant added to `core` for this (the enum -is `#[non_exhaustive]`). - -### 2.4 Two-key TDEA is a separate, decryption-only type - -`TDES` rejects `Key1 = Key3`; `TDES2Key` takes the 16-byte `Key1 || Key2` instead. SP 800-67r2 -Sec 3.1 allows 2TDEA "for legacy use only, as defined in SP 800-131A", and SP 800-131Ar2 Table 1 -has it "Disallowed" for encryption, "Legacy use" for decryption. To make that a property of the -type rather than of the documentation, `core::traits::ElectronicCodeBook` gained an associated -`const ENCRYPTION_APPROVED: bool = true`, and every `Encrypting` mode in `bouncycastle-modes` -asserts it in an inline `const` when constructed. `TDES2Key` sets it `false`, so -`Cbc` (and the other four) fail to compile; the `Decrypting` directions -are untouched and the `TDES2_*` aliases name them. The raw `encrypt_block` remains, because CFB and -CTR decryption are built on the forward function. `MAX_SECURITY_STRENGTH` is `None`: 2TDEA is -"<= 80" bits (SP 800-57pt1r5 Table 2), below the lowest modelled level, and there is no honest -higher value. bc-java's `DESedeEngine` accepts 16-byte keys in both directions (and reports 80 bits). - -### 2.5 Decisions a reviewer may want to revisit - -1. **`ENCRYPTION_APPROVED` lives on the permutation trait**, with the gate in the modes. The - alternative -- a decrypt-only trait -- would need every mode's `Decrypting` impl duplicated. -2. **All 64 weak keys are rejected**, where bc-java's `DESedeParameters` rejects the 16 weak and - semi-weak. The spec says all three lists "should be avoided". The structural test makes 64 no - more code than 16. -3. **Component keys are compared with parity masked**, so two keys differing only in parity are a - repeat. They drive the engine identically, so the bundle really would collapse. -4. **`TDES_CTR` uses a 6-byte nonce and a 2-byte counter** (512 KiB per message). The trade-off, - with numbers, is in `src/ctr.rs`. Naming `bouncycastle_modes::Ctr` directly gives another split. -5. **No `encrypt_2blocks` / `encrypt_4blocks` override, and speed was not the goal.** Criterion on - the development machine (`cargo bench -p bouncycastle-tdes`): about 3.8 µs for `TDES::new` - and about 4.1 µs per block in either direction, i.e. roughly 1.9 MiB/s. A table-driven Triple - DES is around an order of magnitude faster; the price here buys the absence of any - secret-indexed memory access. A two-lane `u64` variant would roughly double bulk throughput - for parallel modes at the cost of lane-masked rotations everywhere; not done for a legacy - cipher. -6. **`ALG_NAME` is `"TDES"`** (bc-java: `"DESede"`; NIST: `"TDEA"`). - ---- - -## 3. Verification - -| What | How | -|---|---| -| S-boxes | Exhaustive: 8 × 64 inputs through the circuit vs. table lookup; every row a permutation of 0..15; `ANF[63] == 0` (balancedness) | -| `IP`, `IP^-1`, `P`, `E` | Every unit vector against the spec table (a proof for a bit permutation); `IP^-1 ∘ IP = id` on the tables themselves | -| Key schedule | Packed round keys unpacked and compared with a long-hand `KS(n, KEY)`; PC-1 selects no parity bit and each other bit once; shifts sum to 28 | -| Weak keys | The 64 listed keys rejected in every position with either parity; set equality with the structural 64; one-bit-off keys accepted | -| Whole engine | NIST CAVP `TECBMMT3`, `TCBCMMT3`, `TCFB64MMT3`, `TCFB8MMT3` (20 cases each, both sections, both directions), through the `TDES_*` aliases with a pinned RNG supplying the vector's IV; the `*MMT2` files for `TDES2Key` | -| Trait contract | `TestFrameworkElectronicCodeBook` (incl. the 112-bit security-strength policy) | -| Aliases | Both parameters select; no IV for ECB; schemes not interchangeable; fresh IV per encryption | - -The two-key files `TECBMMT2`, `TCBCMMT2`, `TCFB64MMT2`, `TCFB8MMT2` are checked the same way for -`TDES2Key`, except that CBC/CFB are decrypt-only (both sections decrypted) since the encrypting -modes do not compile over it. - -The CAVP Monte Carlo files were downloaded but not used: their update rule is SP 800-20's and is a -separate piece of work. - -The vectors are NIST's `tdesmmt.zip` response files, added to the `bc-test-data` repository under -`crypto/aes_tdes_vectors/TDES/` (all 22 files); `tests/cavp_mmt_tests.rs` parses the `.rsp` format -and skips with a warning when the repository is absent, as the AES ACVP tests do. - ---- - -## 4. Where the plan changed - -* The first plan was to store the six expanded key planes per round (1152 bytes). The compact - two-word packing costs one spread per plane per round and saves 768 bytes; the middle four - planes' key XOR folds into the data spread, so the cost is four spreads, not six. -* Two-key TDEA was first rejected outright. It came back as `TDES2Key`, decryption-only, once - the `ENCRYPTION_APPROVED` gate gave a way to say "decryption only" in the type system. -* The vectors were first embedded in the crate as a generated Rust module; they moved to - `bc-test-data` (the repository the other CAVP suites already depend on). -* Weak keys were first going to be a 16-entry table, as in bc-java. Working out why the semi-weak - and possibly-weak lists have the shapes they do gave the period-4 characterisation, which covers - all 64 with no table and turned the transcription of the 48 into a *test* of that claim. -* The CLI's `block_mode_cmd` had `BLOCK_LEN = 16` baked in; it is now a const parameter of the - streaming loops and `CHUNK_LEN` is a fixed 1 KiB that both block lengths divide. - ---- - -## 5. Sources consulted - -Downloaded fresh into the session scratchpad, read, and quoted: SP 800-67 Rev 2 (withdrawn), FIPS -46-3 (Appendix 1 cross-check), SP 800-38A (Appendix E), SP 800-131A Rev 2 (Table 1), SP 800-57 -Part 1 Rev 5 (Table 2), SP 800-20 (for the MMT file semantics), CAVP `tdesmmt.zip`. Reference -implementations read for design and behaviour, not copied: BearSSL `des_ct.c` / `des_support.c`, -bc-java `DESEngine`, `DESedeEngine`, `DESParameters`, `DESedeParameters`. From eeebbf278b6ce0982531fb5206cec58aa5737f34 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 19 Sep 2026 10:47:56 +1000 Subject: [PATCH 07/32] mem_usage_benches: fence bench_tdes_mem_usage's shell recipes as ```text mem_usage_benches/src/lib.rs makes every bench source a module of a lib target, so its //! header is rustdoc'd and an indented block compiles as a Rust doctest by default; the valgrind/ms_print shell one-liners here weren't fenced, so `cargo test --workspace` failed to compile them as Rust (CI on PR #139). Fenced as ```text like the other bench binaries, per CLAUDE.md's note on this exact pitfall. Assisted-by: Claude:claude-sonnet-5 Co-Authored-By: Claude Sonnet 5 --- mem_usage_benches/src/bench_tdes_mem_usage.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index 570c2a0b2..d2c26f53d 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -1,13 +1,17 @@ //! 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: //! -//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null //! -//! ms_print massif.out.835000 +//! ms_print massif.out.* +//! ``` //! //! or, shoved all into one line: //! -//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_tdes_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ```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! //! From b931d49d86c64e31f789c046f73ce552c426c810 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 20 Sep 2026 11:59:36 +1000 Subject: [PATCH 08/32] tdes: fix the three CFB/CFB8/CTR doctests broken by StreamCipherEncryptor::encrypt/encrypt_rng now returning (usize, [u8; INIT_DATA_LEN]) instead of just [u8; INIT_DATA_LEN] -- TDES_CFB, TDES_CFB8 (twice) and TDES_CTR each destructure the tuple instead of binding the old array-only return; no other TDES code is affected since these files are only type aliases over the generic Cfb/Cfb8/Ctr in crypto/modes, not their own trait impls Assisted-by: Claude:claude-sonnet-5 Co-Authored-By: Claude Sonnet 5 --- crypto/tdes/src/cfb.rs | 2 +- crypto/tdes/src/cfb8.rs | 4 ++-- crypto/tdes/src/ctr.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index e82fb1ab1..d72e761eb 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -34,7 +34,7 @@ use bouncycastle_modes::{Cfb, Decrypting}; /// // 47 bytes: a stream cipher does not need a whole number of blocks. /// let message = [0u8; 47]; /// let mut data = message; -/// let iv = TDES_CFB::::encrypt(&key, &mut data).unwrap(); +/// let (_, iv) = TDES_CFB::::encrypt(&key, &mut data).unwrap(); /// assert_ne!(data, message); /// TDES_CFB::::decrypt(&key, &iv, &mut data).unwrap(); /// assert_eq!(data, message); diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index 5dc065e5c..5b4bfc3d7 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -33,14 +33,14 @@ use bouncycastle_modes::{Cfb8, Decrypting}; /// // 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 = TDES_CFB8::::encrypt(&key, &mut data).unwrap(); +/// let (_, iv) = TDES_CFB8::::encrypt(&key, &mut data).unwrap(); /// assert_ne!(data, message); /// TDES_CFB8::::decrypt(&key, &iv, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // CFB8 and CFB64 are not interchangeable: same key, same IV, different ciphertext. /// let mut as_cfb8 = message; -/// let iv = TDES_CFB8::::encrypt(&key, &mut as_cfb8).unwrap(); +/// let (_, iv) = TDES_CFB8::::encrypt(&key, &mut as_cfb8).unwrap(); /// let mut as_cfb64 = as_cfb8; /// TDES_CFB::::decrypt(&key, &iv, &mut as_cfb64).unwrap(); /// assert_ne!(as_cfb64, message); diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index 91595b017..fa91ec173 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -55,7 +55,7 @@ pub const CTR_NONCE_LEN: usize = 6; /// // 47 bytes: a stream cipher does not need a whole number of blocks. /// let message = [0u8; 47]; /// let mut data = message; -/// let nonce = TDES_CTR::::encrypt(&key, &mut data).unwrap(); +/// let (_, nonce) = TDES_CTR::::encrypt(&key, &mut data).unwrap(); /// assert_eq!(nonce.len(), 6); /// assert_ne!(data, message); /// TDES_CTR::::decrypt(&key, &nonce, &mut data).unwrap(); From 84015ab7c52d0a68a61dbc51d226e532d88a9591 Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 28 Sep 2026 20:28:52 +1000 Subject: [PATCH 09/32] tdes: adapt to feature/simple-ciphers -- the API changes this branch absorbed through its merges (SymmetricCipher/BlockCipherPadding renames, the ElectronicCodeBook batch methods, AES*Internal, core::security_strength, PaddedBlockCipher* adapters and the rest), restored in one commit after the linear rebase dropped those merges; the tree is identical to merging d1994a92 with 2d4d0389 Co-Authored-By: Claude Opus 5.5 --- QUALITY_AND_STYLE.md | 8 ++++---- crypto/tdes/benches/tdes_benches.rs | 6 +++--- crypto/tdes/src/cbc.rs | 22 +++++++++++----------- crypto/tdes/src/ecb.rs | 16 ++++++++-------- crypto/tdes/src/lib.rs | 4 ++-- crypto/tdes/src/padded_mode.rs | 22 +++++++++++----------- crypto/tdes/src/tdes.rs | 27 ++++++++++++++++++++++++--- crypto/tdes/src/tdes2.rs | 24 +++++++++++++++++++++++- crypto/tdes/tests/cavp_mmt_tests.rs | 2 +- crypto/tdes/tests/cbc_alias_tests.rs | 10 ++++++---- crypto/tdes/tests/ecb_alias_tests.rs | 10 ++++++---- crypto/tdes/tests/key_tests.rs | 3 ++- 12 files changed, 101 insertions(+), 53 deletions(-) diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index a21b00ecc..6d3591a51 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -83,10 +83,10 @@ All normal rust naming conventions from clippy apply, with one exception: * Where a type, constant or variable corresponds to something a specification (FIPS, RFC, etc) names, keep the specification's spelling and capitalization, and `#[allow(non_camel_case_types)]`, `#[allow(non_snake_case)]` or - `#[allow(non_upper_case_globals)]` the item locally. So the FIPS 197 cipher is `AES_128`, not `AES_128`, its CBC - mode is `AES_CBC_128`, not `AesCbc128`, and if a specification writes `A` for a matrix and `a` for a vector then - `let A = ...; let a = ...;` is the right thing to do. The point is that a reviewer with the specification open can - match names by eye; that matters more here than rust convention. + `#[allow(non_upper_case_globals)]` the item locally. So the FIPS 204 signature algorithm is `MLDSA65`, not `MlDsa44`, + and it's `AES_CBC_128`, not `AesCbc128`, and if a specification writes `A` for a matrix and `a` for a vector then + `let A = ...; let a = ...;` is the right thing to do for code readability and correspondence with the spec. The point + is that a reviewer with the specification open can match names by eye; that matters more here than rust convention. In addition, some library-specific naming conventions: diff --git a/crypto/tdes/benches/tdes_benches.rs b/crypto/tdes/benches/tdes_benches.rs index bab00fdef..790ac3d93 100644 --- a/crypto/tdes/benches/tdes_benches.rs +++ b/crypto/tdes/benches/tdes_benches.rs @@ -1,8 +1,8 @@ //! Criterion benchmarks for the constant-time TDES engine. //! -//! The permutation processes one block per call and does not override the pair or four-block -//! entry points, 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 default +//! 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. diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index ba0fc6852..e44ee9988 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -28,7 +28,7 @@ //! //! # This is the arbitrary-length API //! -//! A padded alias implements [`SimpleCipherEncryptor`] / [`SimpleCipherDecryptor`], not the +//! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the //! block traits: `encrypt_out` / `decrypt_out` and the streaming `do_update_out` / `do_final`, all //! taking a `&[u8]` of any length. The block-aligned API, with its compile-time length checks and //! its in-place data methods, is `bouncycastle_modes::Cbc` itself, which this wraps: @@ -40,7 +40,7 @@ //! //! # How one alias covers both directions //! -//! `PaddedEncryptor` and `PaddedDecryptor` are two distinct types, so a plain type alias cannot +//! `PaddedBlockCipherEncryptor` and `PaddedBlockCipherDecryptor` are two distinct types, so a plain type alias cannot //! select between them on a `Dir` parameter. [`PaddedMode`] does it instead: it is implemented for //! each direction marker and projects to the right adapter, and the alias is written as that //! projection. The only visible consequence is that `Dir` must be @@ -50,13 +50,13 @@ use crate::padded_mode::PaddedMode; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle_padding::PaddedDecryptor; +use bouncycastle_padding::PaddedBlockCipherDecryptor; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; #[allow(unused_imports)] -use bouncycastle_padding::{NoPadding, PKCS7, PaddedEncryptor}; +use bouncycastle_padding::{NoPadding, PKCS7, PaddedBlockCipherEncryptor}; // end of imports needed for docs /// TDES in CBC mode with a padding scheme. @@ -68,7 +68,7 @@ use bouncycastle_padding::{NoPadding, PKCS7, PaddedEncryptor}; /// ``` /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// use bouncycastle_padding::PKCS7; /// @@ -97,7 +97,7 @@ use bouncycastle_padding::{NoPadding, PKCS7, PaddedEncryptor}; /// ``` /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; /// use bouncycastle_modes::Encrypting; /// use bouncycastle_padding::NoPadding; /// @@ -122,7 +122,7 @@ use bouncycastle_padding::{NoPadding, PKCS7, PaddedEncryptor}; /// ```compile_fail /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; /// use bouncycastle_modes::Encrypting; /// use bouncycastle_padding::{NoPadding, PKCS7}; /// @@ -144,14 +144,14 @@ pub type TDES_CBC = ` does not compile; see [`TDES2Key`]. The IV is one 8-byte block, /// supplied by the caller as it was received. /// /// ``` /// use bouncycastle_tdes::TDES2_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SimpleCipherDecryptor; +/// use bouncycastle_core::traits::SymmetricCipherDecryptor; /// use bouncycastle_padding::NoPadding; /// /// // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). @@ -167,7 +167,7 @@ pub type TDES_CBC = = PaddedDecryptor< +pub type TDES2_CBC = PaddedBlockCipherDecryptor< Cbc, Pad, KEY_LEN_2KEY, diff --git a/crypto/tdes/src/ecb.rs b/crypto/tdes/src/ecb.rs index 53047fa8a..7e1b71664 100644 --- a/crypto/tdes/src/ecb.rs +++ b/crypto/tdes/src/ecb.rs @@ -28,7 +28,7 @@ //! //! # This is the arbitrary-length API //! -//! A padded alias implements [`SimpleCipherEncryptor`] / [`SimpleCipherDecryptor`], not the +//! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the //! block traits. The block-aligned API, with compile-time length checks and in-place data methods, //! is `bouncycastle_modes::Ecb` itself, which this wraps. 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 @@ -42,11 +42,11 @@ use crate::padded_mode::PaddedMode; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; -use bouncycastle_padding::PaddedDecryptor; +use bouncycastle_padding::PaddedBlockCipherDecryptor; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; #[allow(unused_imports)] use bouncycastle_padding::{NoPadding, PKCS7}; // end of imports needed for docs @@ -63,7 +63,7 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// ``` /// use bouncycastle_tdes::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// use bouncycastle_padding::PKCS7; /// @@ -90,7 +90,7 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// ``` /// use bouncycastle_tdes::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SimpleCipherEncryptor; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; /// use bouncycastle_modes::Encrypting; /// use bouncycastle_padding::NoPadding; /// @@ -114,14 +114,14 @@ pub type TDES_ECB = = = PaddedDecryptor< +pub type TDES2_ECB = PaddedBlockCipherDecryptor< Ecb, Pad, KEY_LEN_2KEY, diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index 456be3565..8ce4cccc7 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -58,7 +58,7 @@ //! ``` //! use bouncycastle_tdes::TDES_CBC; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; //! use bouncycastle_modes::{Decrypting, Encrypting}; //! use bouncycastle_padding::PKCS7; //! @@ -105,7 +105,7 @@ //! ``` //! use bouncycastle_tdes::TDES2_CBC; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::SimpleCipherDecryptor; +//! use bouncycastle_core::traits::SymmetricCipherDecryptor; //! use bouncycastle_padding::NoPadding; //! //! // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0. diff --git a/crypto/tdes/src/padded_mode.rs b/crypto/tdes/src/padded_mode.rs index 4d656a5cb..667c49bf5 100644 --- a/crypto/tdes/src/padded_mode.rs +++ b/crypto/tdes/src/padded_mode.rs @@ -1,7 +1,7 @@ //! The projection that lets a padded mode alias take its direction *and* its padding scheme. //! -//! `bouncycastle-padding` splits its adapters by direction: [`PaddedEncryptor`] wraps a -//! [`BlockCipherEncryptor`] and [`PaddedDecryptor`] a [`BlockCipherDecryptor`]. They are two +//! `bouncycastle-padding` splits its adapters by direction: [`PaddedBlockCipherEncryptor`] wraps a +//! [`BlockCipherEncryptor`] and [`PaddedBlockCipherDecryptor`] a [`BlockCipherDecryptor`]. They are two //! distinct types, and a plain type alias cannot choose between two types based on one of its own //! parameters, so `TDES_CBC` cannot be written directly. //! @@ -21,9 +21,9 @@ //! ECB passes its two and `INIT_DATA_LEN = 0`. This is the same device as `bouncycastle-aes`'s. use crate::{BLOCK_LEN, KEY_LEN}; -use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, Padding}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding}; use bouncycastle_modes::{Decrypting, Encrypting}; -use bouncycastle_padding::{PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; /// Projects a direction marker onto the padded adapter for that direction. /// @@ -36,10 +36,10 @@ pub trait PaddedMode where Enc: BlockCipherEncryptor, Dec: BlockCipherDecryptor, - Pad: Padding, + Pad: BlockCipherPadding, { - /// The padded type for this direction: a [`PaddedEncryptor`] over `Enc`, or a - /// [`PaddedDecryptor`] over `Dec`. + /// The padded type for this direction: a [`PaddedBlockCipherEncryptor`] over `Enc`, or a + /// [`PaddedBlockCipherDecryptor`] over `Dec`. type Mode; } @@ -48,9 +48,9 @@ impl PaddedMode, Dec: BlockCipherDecryptor, - Pad: Padding, + Pad: BlockCipherPadding, { - type Mode = PaddedEncryptor; + type Mode = PaddedBlockCipherEncryptor; } impl PaddedMode @@ -58,7 +58,7 @@ impl PaddedMode, Dec: BlockCipherDecryptor, - Pad: Padding, + Pad: BlockCipherPadding, { - type Mode = PaddedDecryptor; + type Mode = PaddedBlockCipherDecryptor; } diff --git a/crypto/tdes/src/tdes.rs b/crypto/tdes/src/tdes.rs index e1f03af7f..6c76f921f 100644 --- a/crypto/tdes/src/tdes.rs +++ b/crypto/tdes/src/tdes.rs @@ -6,7 +6,8 @@ use crate::des::{Subkeys, inverse_ip, ip, rounds}; use crate::schedule::{PARITY_MASK, expand, is_weak}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; use bouncycastle_utils::ct::ct_eq_zero_bytes; use bouncycastle_utils::secret::Secret; @@ -204,8 +205,28 @@ impl ElectronicCodeBook for TDES { fn decrypt_block(&self, block: &mut Block) { TDES::decrypt_block(self, block) } - // The pair and four-block methods keep their defaults: the engine has no natural unit larger - // than one block, so there is nothing to gain from overriding them. + // 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() { + TDES::encrypt_block(self, block) + } + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + TDES::decrypt_block(self, block) + } + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + TDES::encrypt_block(self, block) + } + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + TDES::decrypt_block(self, block) + } + } } impl core::fmt::Debug for TDES { diff --git a/crypto/tdes/src/tdes2.rs b/crypto/tdes/src/tdes2.rs index e091188fc..1ee17f8fd 100644 --- a/crypto/tdes/src/tdes2.rs +++ b/crypto/tdes/src/tdes2.rs @@ -7,7 +7,8 @@ use crate::tdes::{ }; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait}; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; use bouncycastle_utils::secret::Secret; /// The length of a two-key TDEA key bundle in bytes: `Key1 || Key2`, with `Key3 = Key1` @@ -122,6 +123,27 @@ impl ElectronicCodeBook for TDES2Key { fn decrypt_block(&self, block: &mut Block) { TDES2Key::decrypt_block(self, block) } + // As for `TDES`: 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() { + TDES2Key::encrypt_block(self, block) + } + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + for block in blocks.iter_mut() { + TDES2Key::decrypt_block(self, block) + } + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + TDES2Key::encrypt_block(self, block) + } + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + for block in blocks.iter_mut() { + TDES2Key::decrypt_block(self, block) + } + } } impl core::fmt::Debug for TDES2Key { diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs index 65cbcdbae..25348e78b 100644 --- a/crypto/tdes/tests/cavp_mmt_tests.rs +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -179,7 +179,7 @@ fn check_ecb>( } assert_eq!(buf, case.plaintext, "{name}: decrypt"); - // The default four-block methods must give the same answers; the longer cases (up to ten + // 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::(); diff --git a/crypto/tdes/tests/cbc_alias_tests.rs b/crypto/tdes/tests/cbc_alias_tests.rs index 5a1be7be5..294f81344 100644 --- a/crypto/tdes/tests/cbc_alias_tests.rs +++ b/crypto/tdes/tests/cbc_alias_tests.rs @@ -6,9 +6,11 @@ //! layer are tested in their own crates; this checks the wiring between them at the 8-byte block. use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle_padding::{NoPadding, PKCS7, PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES_CBC}; fn key() -> KeyMaterial { @@ -25,7 +27,7 @@ fn the_alias_names_the_expected_types() { assert_eq!( size_of::>(), size_of::< - PaddedEncryptor< + PaddedBlockCipherEncryptor< Cbc, PKCS7, KEY_LEN, @@ -37,7 +39,7 @@ fn the_alias_names_the_expected_types() { assert_eq!( size_of::>(), size_of::< - PaddedDecryptor< + PaddedBlockCipherDecryptor< Cbc, PKCS7, KEY_LEN, diff --git a/crypto/tdes/tests/ecb_alias_tests.rs b/crypto/tdes/tests/ecb_alias_tests.rs index b74651d2b..429840cf7 100644 --- a/crypto/tdes/tests/ecb_alias_tests.rs +++ b/crypto/tdes/tests/ecb_alias_tests.rs @@ -7,9 +7,11 @@ //! value than CBC's. use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SimpleCipherDecryptor, SimpleCipherEncryptor}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; -use bouncycastle_padding::{NoPadding, PKCS7, PaddedDecryptor, PaddedEncryptor}; +use bouncycastle_padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES_ECB}; fn key() -> KeyMaterial { @@ -26,7 +28,7 @@ fn the_alias_names_the_expected_types() { assert_eq!( size_of::>(), size_of::< - PaddedEncryptor< + PaddedBlockCipherEncryptor< Ecb, PKCS7, KEY_LEN, @@ -38,7 +40,7 @@ fn the_alias_names_the_expected_types() { assert_eq!( size_of::>(), size_of::< - PaddedDecryptor< + PaddedBlockCipherDecryptor< Ecb, PKCS7, KEY_LEN, diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index d3109dfa8..cc1479689 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -12,7 +12,8 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; /// Sec 3.3.2, "Keys that are considered weak are (in hexadecimal format)". From af64b54f821bf61b3cf5afa2addc40b9401d7734 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 29 Sep 2026 10:36:12 +1000 Subject: [PATCH 10/32] tdes: adapt to feature/simple-ciphers 0fb4e79 -- the TDES_CFB/CFB8/CTR and TDES2_* examples take the renamed in-place one-shots (encrypt_in_place/decrypt_in_place) and import the SymmetricCipher* supertraits their do_*_init calls now come from; and a compile_fail example pins that Ctr is still refused, now through KeyStream::ENCRYPTION_APPROVED (which CtrKeyStream takes from its permutation, carried in the merge) since CTR's encrypting constructor moved into core's StreamCipher adapter Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 --- crypto/tdes/src/cfb.rs | 8 ++++---- crypto/tdes/src/cfb8.rs | 10 +++++----- crypto/tdes/src/ctr.rs | 8 ++++---- crypto/tdes/src/tdes2.rs | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index d72e761eb..7818139fa 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -24,7 +24,7 @@ use bouncycastle_modes::{Cfb, Decrypting}; /// ``` /// use bouncycastle_tdes::TDES_CFB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. @@ -34,9 +34,9 @@ use bouncycastle_modes::{Cfb, Decrypting}; /// // 47 bytes: a stream cipher does not need a whole number of blocks. /// let message = [0u8; 47]; /// let mut data = message; -/// let (_, iv) = TDES_CFB::::encrypt(&key, &mut data).unwrap(); +/// let (_, iv) = TDES_CFB::::encrypt_in_place(&key, &mut data).unwrap(); /// assert_ne!(data, message); -/// TDES_CFB::::decrypt(&key, &iv, &mut data).unwrap(); +/// TDES_CFB::::decrypt_in_place(&key, &iv, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // Streaming, at any byte boundary: @@ -72,7 +72,7 @@ pub type TDES_CFB = Cfb; /// let iv = [0x6e, 0x37, 0xd1, 0x97, 0x37, 0x6d, 0xb5, 0x95]; /// let mut data = [0x63, 0xca, 0xd5, 0x22, 0x60, 0xe0, 0xa1, 0xcd]; /// -/// TDES2_CFB::decrypt(&key, &iv, &mut data).expect("decryption"); +/// TDES2_CFB::decrypt_in_place(&key, &iv, &mut data).expect("decryption"); /// assert_eq!(data, [0xdc, 0xd3, 0xcf, 0x97, 0x46, 0xd6, 0xe4, 0x2b]); /// ``` #[allow(non_camel_case_types)] diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index 5b4bfc3d7..d78c7ecce 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -33,16 +33,16 @@ use bouncycastle_modes::{Cfb8, Decrypting}; /// // 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) = TDES_CFB8::::encrypt(&key, &mut data).unwrap(); +/// let (_, iv) = TDES_CFB8::::encrypt_in_place(&key, &mut data).unwrap(); /// assert_ne!(data, message); -/// TDES_CFB8::::decrypt(&key, &iv, &mut data).unwrap(); +/// TDES_CFB8::::decrypt_in_place(&key, &iv, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // CFB8 and CFB64 are not interchangeable: same key, same IV, different ciphertext. /// let mut as_cfb8 = message; -/// let (_, iv) = TDES_CFB8::::encrypt(&key, &mut as_cfb8).unwrap(); +/// let (_, iv) = TDES_CFB8::::encrypt_in_place(&key, &mut as_cfb8).unwrap(); /// let mut as_cfb64 = as_cfb8; -/// TDES_CFB::::decrypt(&key, &iv, &mut as_cfb64).unwrap(); +/// TDES_CFB::::decrypt_in_place(&key, &iv, &mut as_cfb64).unwrap(); /// assert_ne!(as_cfb64, message); /// ``` #[allow(non_camel_case_types)] @@ -66,7 +66,7 @@ pub type TDES_CFB8 = Cfb8; /// let iv = [0x8b, 0x97, 0x57, 0x9e, 0xa5, 0xac, 0x30, 0x0f]; /// let mut data = [0x05]; /// -/// TDES2_CFB8::decrypt(&key, &iv, &mut data).expect("decryption"); +/// TDES2_CFB8::decrypt_in_place(&key, &iv, &mut data).expect("decryption"); /// assert_eq!(data, [0x80]); /// ``` #[allow(non_camel_case_types)] diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index fa91ec173..81027f4d8 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -45,7 +45,7 @@ pub const CTR_NONCE_LEN: usize = 6; /// ``` /// use bouncycastle_tdes::TDES_CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// use bouncycastle_modes::{Decrypting, Encrypting}; /// /// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. @@ -55,10 +55,10 @@ pub const CTR_NONCE_LEN: usize = 6; /// // 47 bytes: a stream cipher does not need a whole number of blocks. /// let message = [0u8; 47]; /// let mut data = message; -/// let (_, nonce) = TDES_CTR::::encrypt(&key, &mut data).unwrap(); +/// let (_, nonce) = TDES_CTR::::encrypt_in_place(&key, &mut data).unwrap(); /// assert_eq!(nonce.len(), 6); /// assert_ne!(data, message); -/// TDES_CTR::::decrypt(&key, &nonce, &mut data).unwrap(); +/// TDES_CTR::::decrypt_in_place(&key, &nonce, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // Streaming, at any byte boundary: @@ -99,7 +99,7 @@ pub type TDES_CTR = Ctr; /// let plaintext = *b"8 bytes!"; /// let mut data: [u8; 8] = core::array::from_fn(|i| plaintext[i] ^ keystream[i]); /// -/// TDES2_CTR::decrypt(&key, &nonce, &mut data).expect("decryption"); +/// TDES2_CTR::decrypt_in_place(&key, &nonce, &mut data).expect("decryption"); /// assert_eq!(data, plaintext); /// ``` #[allow(non_camel_case_types)] diff --git a/crypto/tdes/src/tdes2.rs b/crypto/tdes/src/tdes2.rs index 1ee17f8fd..cd90e471d 100644 --- a/crypto/tdes/src/tdes2.rs +++ b/crypto/tdes/src/tdes2.rs @@ -37,6 +37,23 @@ pub const KEY_LEN_2KEY: usize = 16; /// let _ = Cbc::::do_encrypt_init(&key); /// ``` /// +/// CTR's encrypting constructor is the generic `StreamCipher` adapter in `bouncycastle-core`, which +/// checks the keystream's `KeyStream::ENCRYPTION_APPROVED`; CTR's keystream takes this type's value, +/// so the refusal holds there too: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// use bouncycastle_modes::{Ctr, Encrypting}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// +/// // Two-key TDEA may not apply protection through CTR either: this does not compile. +/// let _ = Ctr::::do_encrypt_init(&key); +/// ``` +/// /// The `Decrypting` direction of every mode works, and this crate provides the aliases /// [`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) for it. The raw From ae476eaea00b99ef5460144b161c6a561f7d5d00 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 1 Oct 2026 15:28:09 +1000 Subject: [PATCH 11/32] tdes: adopt the hazmat layout (#156 step 5) TDES, the decryption-only TDES2Key and the TDES_ECB / TDES2_ECB aliases move to bouncycastle_tdes::hazmat, with BLOCK_LEN, KEY_LEN and KEY_LEN_2KEY defined at the crate root; the merge ahead of this ported ENCRYPTION_APPROVED onto the moved trait and CtrKeyStream files. No logic change, no mutation run owed. Assisted-by: Claude Code:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- cli/src/tdes2_cbc_cmd.rs | 3 +- cli/src/tdes2_ecb_cmd.rs | 8 +++-- cli/src/tdes_cbc_cmd.rs | 3 +- cli/src/tdes_ecb_cmd.rs | 10 ++++-- crypto/tdes/benches/tdes_benches.rs | 3 +- crypto/tdes/src/cbc.rs | 3 +- crypto/tdes/src/cfb.rs | 3 +- crypto/tdes/src/cfb8.rs | 3 +- crypto/tdes/src/ctr.rs | 6 ++-- crypto/tdes/src/des.rs | 2 +- crypto/tdes/src/{ => hazmat}/ecb.rs | 12 ++++--- crypto/tdes/src/hazmat/mod.rs | 16 ++++++++++ crypto/tdes/src/{ => hazmat}/tdes.rs | 8 +---- crypto/tdes/src/{ => hazmat}/tdes2.rs | 16 ++++------ crypto/tdes/src/lib.rs | 32 +++++++++++++------ crypto/tdes/tests/cavp_mmt_tests.rs | 3 +- crypto/tdes/tests/cbc_alias_tests.rs | 3 +- crypto/tdes/tests/ecb_alias_tests.rs | 3 +- .../tdes/tests/electronic_code_book_tests.rs | 3 +- crypto/tdes/tests/key_tests.rs | 5 +-- mem_usage_benches/src/bench_tdes_mem_usage.rs | 4 +-- 21 files changed, 96 insertions(+), 53 deletions(-) rename crypto/tdes/src/{ => hazmat}/ecb.rs (95%) create mode 100644 crypto/tdes/src/hazmat/mod.rs rename crypto/tdes/src/{ => hazmat}/tdes.rs (97%) rename crypto/tdes/src/{ => hazmat}/tdes2.rs (95%) diff --git a/cli/src/tdes2_cbc_cmd.rs b/cli/src/tdes2_cbc_cmd.rs index e6eb7eacf..c0bdf0048 100644 --- a/cli/src/tdes2_cbc_cmd.rs +++ b/cli/src/tdes2_cbc_cmd.rs @@ -8,7 +8,8 @@ use crate::block_mode_cmd::{DecryptOnlyAction, decrypt_stream, load_key}; use bouncycastle::modes::{Cbc, Decrypting}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; +use bouncycastle::tdes::hazmat::TDES2Key; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; pub(crate) fn tdes2_cbc_cmd( action: &DecryptOnlyAction, diff --git a/cli/src/tdes2_ecb_cmd.rs b/cli/src/tdes2_ecb_cmd.rs index 18a87f43f..409a9359f 100644 --- a/cli/src/tdes2_ecb_cmd.rs +++ b/cli/src/tdes2_ecb_cmd.rs @@ -6,9 +6,11 @@ //! whole number of 8-byte blocks, no unpadding -- see [`crate::tdes_ecb_cmd`] for the ECB warning //! and [`crate::block_mode_cmd`] for the framing. The key is the 16-byte `KEY1 || KEY2`. -use crate::block_mode_cmd::{DecryptOnlyAction, decrypt_stream, load_key}; -use bouncycastle::modes::{Decrypting, Ecb}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, decrypt_stream, load_key}; +use bouncycastle::modes::Decrypting; +use bouncycastle::modes::hazmat::Ecb; +use bouncycastle::tdes::hazmat::TDES2Key; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; pub(crate) fn tdes2_ecb_cmd( action: &DecryptOnlyAction, diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs index 9787ae4f2..d2323523f 100644 --- a/cli/src/tdes_cbc_cmd.rs +++ b/cli/src/tdes_cbc_cmd.rs @@ -16,7 +16,8 @@ use crate::block_mode_cmd::{BlockModeAction, decrypt_stream, encrypt_stream, load_key}; use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES}; +use bouncycastle::tdes::hazmat::TDES; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; /// Names the mode in error messages. const MODE: &str = "CBC"; diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs index 8c9cd5eb0..81996490e 100644 --- a/cli/src/tdes_ecb_cmd.rs +++ b/cli/src/tdes_ecb_cmd.rs @@ -18,9 +18,13 @@ //! 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::block_mode_cmd::{BlockModeAction, decrypt_stream, encrypt_stream, load_key}; -use bouncycastle::modes::{Decrypting, Ecb, Encrypting}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES}; +use crate::helpers::block_mode_helpers::{ + CipherDirection, decrypt_stream, encrypt_stream, load_key, +}; +use bouncycastle::modes::hazmat::Ecb; +use bouncycastle::modes::{Decrypting, Encrypting}; +use bouncycastle::tdes::hazmat::TDES; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; /// Names the mode in error messages. const MODE: &str = "ECB"; diff --git a/crypto/tdes/benches/tdes_benches.rs b/crypto/tdes/benches/tdes_benches.rs index 790ac3d93..2f43a4e90 100644 --- a/crypto/tdes/benches/tdes_benches.rs +++ b/crypto/tdes/benches/tdes_benches.rs @@ -9,7 +9,8 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{ElectronicCodeBook, RNG}; use bouncycastle_rng as rng; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES}; +use bouncycastle_tdes::hazmat::TDES; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index e44ee9988..cd144a4a4 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -47,8 +47,9 @@ //! [`Encrypting`](bouncycastle_modes::Encrypting) or //! [`Decrypting`](bouncycastle_modes::Decrypting), which was already true. +use crate::hazmat::{TDES, TDES2Key}; use crate::padded_mode::PaddedMode; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; use bouncycastle_padding::PaddedBlockCipherDecryptor; diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index 7818139fa..bbc4a4703 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -11,7 +11,8 @@ //! non-interoperable mode with its own alias, [`TDES_CFB8`](crate::TDES_CFB8); `s = 1` is not //! implemented. See the `bouncycastle_modes::Cfb` docs. -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use crate::hazmat::{TDES, TDES2Key}; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; use bouncycastle_modes::{Cfb, Decrypting}; /// TDES in CFB64 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index d78c7ecce..d4e69440e 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -10,7 +10,8 @@ //! 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_modes::Cfb8` docs for when that is the right trade. -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use crate::hazmat::{TDES, TDES2Key}; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; use bouncycastle_modes::{Cfb8, Decrypting}; /// TDES in CFB8 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index 81027f4d8..aa095e76c 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -28,7 +28,8 @@ //! whose counter can still reach a useful message size, and it is what this alias chooses. Name //! `Ctr` directly for a different split. -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use crate::hazmat::{TDES, TDES2Key}; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; use bouncycastle_modes::{Ctr, Decrypting}; /// The nonce length the [`TDES_CTR`] alias uses, leaving a 2-byte counter. See the module docs. @@ -85,7 +86,8 @@ pub type TDES_CTR = Ctr; /// instead: the first keystream block is the forward cipher function of `nonce || 0x0000`. /// /// ``` -/// use bouncycastle_tdes::{TDES2_CTR, TDES2Key}; +/// use bouncycastle_tdes::hazmat::TDES2Key; +/// use bouncycastle_tdes::TDES2_CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{ElectronicCodeBook, StreamCipherDecryptor}; /// diff --git a/crypto/tdes/src/des.rs b/crypto/tdes/src/des.rs index b0366a34a..2aa8153f6 100644 --- a/crypto/tdes/src/des.rs +++ b/crypto/tdes/src/des.rs @@ -173,7 +173,7 @@ fn f(r: u32, lo: u32, hi: u32) -> u32 { /// /// `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::tdes::TDES`] applies each permutation once around all forty-eight rounds. Every +/// [`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) { diff --git a/crypto/tdes/src/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs similarity index 95% rename from crypto/tdes/src/ecb.rs rename to crypto/tdes/src/hazmat/ecb.rs index 7e1b71664..91b0a310b 100644 --- a/crypto/tdes/src/ecb.rs +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -39,9 +39,11 @@ //! See [`PaddedMode`], which is the projection that lets `Dir` select between the encryptor and the //! decryptor adapter. `Dir` must be [`Encrypting`] or [`Decrypting`]. +use crate::hazmat::{TDES, TDES2Key}; use crate::padded_mode::PaddedMode; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; -use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; +use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +use bouncycastle_modes::hazmat::Ecb; +use bouncycastle_modes::{Decrypting, Encrypting}; use bouncycastle_padding::PaddedBlockCipherDecryptor; // Imports needed for docs @@ -61,7 +63,7 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// does not make it safe. /// /// ``` -/// use bouncycastle_tdes::TDES_ECB; +/// use bouncycastle_tdes::hazmat::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; /// use bouncycastle_modes::{Decrypting, Encrypting}; @@ -88,7 +90,7 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// plaintext blocks still give two identical ciphertext blocks. /// /// ``` -/// use bouncycastle_tdes::TDES_ECB; +/// use bouncycastle_tdes::hazmat::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherEncryptor; /// use bouncycastle_modes::Encrypting; @@ -119,7 +121,7 @@ pub type TDES_ECB = KeyMaterial { let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); diff --git a/crypto/tdes/tests/ecb_alias_tests.rs b/crypto/tdes/tests/ecb_alias_tests.rs index 429840cf7..5b0dfbfa4 100644 --- a/crypto/tdes/tests/ecb_alias_tests.rs +++ b/crypto/tdes/tests/ecb_alias_tests.rs @@ -12,7 +12,8 @@ use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; use bouncycastle_padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES_ECB}; +use bouncycastle_tdes::hazmat::{TDES, TDES_ECB}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; fn key() -> KeyMaterial { let bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); diff --git a/crypto/tdes/tests/electronic_code_book_tests.rs b/crypto/tdes/tests/electronic_code_book_tests.rs index 416b790eb..3decce11b 100644 --- a/crypto/tdes/tests/electronic_code_book_tests.rs +++ b/crypto/tdes/tests/electronic_code_book_tests.rs @@ -13,7 +13,8 @@ //! strength the framework tries must be accepted; the framework checks that too. use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_tdes::hazmat::{TDES, TDES2Key}; +use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; #[test] fn tdes_conforms_to_electronic_code_book() { diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index cc1479689..3a94c96a9 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -13,8 +13,9 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES, TDES2Key}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_tdes::hazmat::{TDES, TDES2Key}; +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] = diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index d2c26f53d..681043c3e 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -36,8 +36,8 @@ #![allow(unused_imports)] use bouncycastle::core::key_material::{KeyMaterial, KeyType}; -use bouncycastle::core::traits::ElectronicCodeBook; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES, TDES2Key}; +use bouncycastle::tdes::hazmat::{TDES, TDES2Key}; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; /// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. fn bench_do_nothing() { From 27a5856766b1da217ade31c430d2b5f5122b72da Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 1 Oct 2026 16:20:32 +1000 Subject: [PATCH 12/32] tdes: project TDES_CBC and TDES_ECB through core's sealed Direction::Select Follows aes 160ac180; the crate's own PaddedMode copy goes with it. Type aliases only, no behaviour change, no mutation run owed. Assisted-by: Claude Code:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 --- crypto/tdes/src/cbc.rs | 34 +++++++++++------- crypto/tdes/src/hazmat/ecb.rs | 28 ++++++++++----- crypto/tdes/src/lib.rs | 1 - crypto/tdes/src/padded_mode.rs | 64 ---------------------------------- 4 files changed, 41 insertions(+), 86 deletions(-) delete mode 100644 crypto/tdes/src/padded_mode.rs diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index cd144a4a4..da344b74f 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -41,23 +41,23 @@ //! # How one alias covers both directions //! //! `PaddedBlockCipherEncryptor` and `PaddedBlockCipherDecryptor` are two distinct types, so a plain type alias cannot -//! select between them on a `Dir` parameter. [`PaddedMode`] does it instead: it is implemented for -//! each direction marker and projects to the right adapter, and the alias is written as that -//! projection. The only visible consequence is that `Dir` must be +//! select between them on a `Dir` parameter. [`Direction::Select`](bouncycastle_core::stream_cipher::Direction) does it instead: the +//! sealed projection in core picks the adapter for the direction, and the alias is written as +//! that projection. The only visible consequence is that `Dir` must be //! [`Encrypting`](bouncycastle_modes::Encrypting) or //! [`Decrypting`](bouncycastle_modes::Decrypting), which was already true. use crate::hazmat::{TDES, TDES2Key}; -use crate::padded_mode::PaddedMode; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +use bouncycastle_core::stream_cipher::Direction; use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle_padding::PaddedBlockCipherDecryptor; +use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; // Imports needed for docs #[allow(unused_imports)] use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; #[allow(unused_imports)] -use bouncycastle_padding::{NoPadding, PKCS7, PaddedBlockCipherEncryptor}; +use bouncycastle_padding::{NoPadding, PKCS7}; // end of imports needed for docs /// TDES in CBC mode with a padding scheme. @@ -135,12 +135,22 @@ use bouncycastle_padding::{NoPadding, PKCS7, PaddedBlockCipherEncryptor}; /// let _mismatched: TDES_CBC = enc; /// ``` #[allow(non_camel_case_types)] -pub type TDES_CBC = , - Cbc, - Pad, - BLOCK_LEN, ->>::Mode; +pub type TDES_CBC = ::Select< + PaddedBlockCipherEncryptor< + Cbc, + Pad, + KEY_LEN, + BLOCK_LEN, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc, + Pad, + KEY_LEN, + BLOCK_LEN, + BLOCK_LEN, + >, +>; /// Two-key TDES in CBC mode with a padding scheme, **decryption only**. /// diff --git a/crypto/tdes/src/hazmat/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs index 91b0a310b..efbb63ebd 100644 --- a/crypto/tdes/src/hazmat/ecb.rs +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -36,15 +36,15 @@ //! //! # How one alias covers both directions //! -//! See [`PaddedMode`], which is the projection that lets `Dir` select between the encryptor and the +//! See [`Direction::Select`](bouncycastle_core::stream_cipher::Direction), which is the projection that lets `Dir` select between the encryptor and the //! decryptor adapter. `Dir` must be [`Encrypting`] or [`Decrypting`]. use crate::hazmat::{TDES, TDES2Key}; -use crate::padded_mode::PaddedMode; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +use bouncycastle_core::stream_cipher::Direction; use bouncycastle_modes::hazmat::Ecb; use bouncycastle_modes::{Decrypting, Encrypting}; -use bouncycastle_padding::PaddedBlockCipherDecryptor; +use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; // Imports needed for docs #[allow(unused_imports)] @@ -106,12 +106,22 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// assert_eq!(ciphertext[..8], ciphertext[8..]); /// ``` #[allow(non_camel_case_types)] -pub type TDES_ECB = , - Ecb, - Pad, - 0, ->>::Mode; +pub type TDES_ECB = ::Select< + PaddedBlockCipherEncryptor< + Ecb, + Pad, + KEY_LEN, + 0, + BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Ecb, + Pad, + KEY_LEN, + 0, + BLOCK_LEN, + >, +>; /// Two-key TDES in ECB mode with a padding scheme, **decryption only**. /// diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index c457e2511..eeaa6a05e 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -264,7 +264,6 @@ mod cfb8; mod ctr; mod des; pub mod hazmat; -mod padded_mode; mod sbox; mod schedule; diff --git a/crypto/tdes/src/padded_mode.rs b/crypto/tdes/src/padded_mode.rs deleted file mode 100644 index 667c49bf5..000000000 --- a/crypto/tdes/src/padded_mode.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! The projection that lets a padded mode alias take its direction *and* its padding scheme. -//! -//! `bouncycastle-padding` splits its adapters by direction: [`PaddedBlockCipherEncryptor`] wraps a -//! [`BlockCipherEncryptor`] and [`PaddedBlockCipherDecryptor`] a [`BlockCipherDecryptor`]. They are two -//! distinct types, and a plain type alias cannot choose between two types based on one of its own -//! parameters, so `TDES_CBC` cannot be written directly. -//! -//! [`PaddedMode`] does it instead. It is implemented for each direction marker, and its associated -//! type is the adapter for that direction, so an alias can be written as a projection through it: -//! -//! ```text -//! pub type TDES_CBC = , // what Encrypting resolves to -//! Cbc, // what Decrypting resolves to -//! Pad, 8, -//! >>::Mode; -//! ``` -//! -//! One trait serves both block modes, since it is parameterised by the encryptor and decryptor -//! types rather than by the mode: CBC passes its two directions and `INIT_DATA_LEN = BLOCK_LEN`, -//! ECB passes its two and `INIT_DATA_LEN = 0`. This is the same device as `bouncycastle-aes`'s. - -use crate::{BLOCK_LEN, KEY_LEN}; -use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding}; -use bouncycastle_modes::{Decrypting, Encrypting}; -use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; - -/// Projects a direction marker onto the padded adapter for that direction. -/// -/// Implemented for [`Encrypting`] and [`Decrypting`] and for nothing else, so those remain the only -/// usable values of a `Dir` parameter. See the module docs for why it exists. -/// -/// `Enc` and `Dec` are the two directions of the underlying block mode, `Pad` is the padding -/// scheme, and `INIT_DATA_LEN` is the mode's: the block length for a mode with an IV, 0 for ECB. -pub trait PaddedMode -where - Enc: BlockCipherEncryptor, - Dec: BlockCipherDecryptor, - Pad: BlockCipherPadding, -{ - /// The padded type for this direction: a [`PaddedBlockCipherEncryptor`] over `Enc`, or a - /// [`PaddedBlockCipherDecryptor`] over `Dec`. - type Mode; -} - -impl PaddedMode - for Encrypting -where - Enc: BlockCipherEncryptor, - Dec: BlockCipherDecryptor, - Pad: BlockCipherPadding, -{ - type Mode = PaddedBlockCipherEncryptor; -} - -impl PaddedMode - for Decrypting -where - Enc: BlockCipherEncryptor, - Dec: BlockCipherDecryptor, - Pad: BlockCipherPadding, -{ - type Mode = PaddedBlockCipherDecryptor; -} From 7d74ee33e43363e02eb6b459741700330a99f644 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 24 Oct 2026 21:43:44 +1100 Subject: [PATCH 13/32] tdes: the adaptations the dropped merges carried -- bouncycastle-cipher paths for the modes and padding, core::hazmat::ElectronicCodeBook The linear replay of this branch onto feature/key-rework-memory drops its merges from feature/simple-ciphers, and the move of bouncycastle-modes and bouncycastle-padding into bouncycastle-cipher, with Direction and ElectronicCodeBook's new homes, was resolved only in those merges. This is the tdes crate, its CLI commands and its memory bench exactly as the branch tip 51972290 had them. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/tdes2_cbc_cmd.rs | 7 ++-- cli/src/tdes2_cfb8_cmd.rs | 6 +-- cli/src/tdes2_cfb_cmd.rs | 6 +-- cli/src/tdes2_ctr_cmd.rs | 6 +-- cli/src/tdes2_ecb_cmd.rs | 6 +-- cli/src/tdes_cbc_cmd.rs | 15 ++++--- cli/src/tdes_cfb8_cmd.rs | 10 ++--- cli/src/tdes_cfb_cmd.rs | 10 ++--- cli/src/tdes_ctr_cmd.rs | 10 ++--- cli/src/tdes_ecb_cmd.rs | 12 +++--- crypto/tdes/Cargo.toml | 3 +- crypto/tdes/benches/tdes_benches.rs | 3 +- crypto/tdes/src/cbc.rs | 41 ++++++++++--------- crypto/tdes/src/cfb.rs | 13 +++--- crypto/tdes/src/cfb8.rs | 13 +++--- crypto/tdes/src/ctr.rs | 14 ++++--- crypto/tdes/src/hazmat/ecb.rs | 30 +++++++------- crypto/tdes/src/hazmat/tdes.rs | 3 +- crypto/tdes/src/hazmat/tdes2.rs | 11 +++-- crypto/tdes/src/lib.rs | 14 +++---- crypto/tdes/tests/cavp_mmt_tests.rs | 9 ++-- crypto/tdes/tests/cbc_alias_tests.rs | 9 ++-- crypto/tdes/tests/ecb_alias_tests.rs | 11 ++--- crypto/tdes/tests/key_tests.rs | 6 +-- mem_usage_benches/src/bench_tdes_mem_usage.rs | 1 + 25 files changed, 143 insertions(+), 126 deletions(-) diff --git a/cli/src/tdes2_cbc_cmd.rs b/cli/src/tdes2_cbc_cmd.rs index c0bdf0048..7643f0be8 100644 --- a/cli/src/tdes2_cbc_cmd.rs +++ b/cli/src/tdes2_cbc_cmd.rs @@ -4,10 +4,11 @@ //! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and //! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cbc decrypt` -- the first 8 bytes //! of input are the IV, the rest whole 8-byte blocks -- see [`crate::tdes_cbc_cmd`] and -//! [`crate::block_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. +//! [`crate::helpers::block_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. -use crate::block_mode_cmd::{DecryptOnlyAction, decrypt_stream, load_key}; -use bouncycastle::modes::{Cbc, Decrypting}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, decrypt_stream, load_key}; +use bouncycastle::cipher::Decrypting; +use bouncycastle::cipher::modes::Cbc; use bouncycastle::tdes::hazmat::TDES2Key; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; diff --git a/cli/src/tdes2_cfb8_cmd.rs b/cli/src/tdes2_cfb8_cmd.rs index 1445015b3..c7e862e1d 100644 --- a/cli/src/tdes2_cfb8_cmd.rs +++ b/cli/src/tdes2_cfb8_cmd.rs @@ -4,10 +4,10 @@ //! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and //! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb8 decrypt` -- the first 8 //! bytes of input are the IV, the rest any length -- see [`crate::tdes_cfb8_cmd`] for how CFB8 -//! differs from CFB64, and [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. +//! differs from CFB64, and [`crate::helpers::stream_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. -use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; -use crate::stream_mode_cmd::decrypt_stream; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key}; +use crate::helpers::stream_mode_helpers::decrypt_stream; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB8}; pub(crate) fn tdes2_cfb8_cmd( diff --git a/cli/src/tdes2_cfb_cmd.rs b/cli/src/tdes2_cfb_cmd.rs index 8ad55580c..df85e840b 100644 --- a/cli/src/tdes2_cfb_cmd.rs +++ b/cli/src/tdes2_cfb_cmd.rs @@ -4,10 +4,10 @@ //! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and //! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-cfb decrypt` -- the first 8 //! bytes of input are the IV, the rest any length -- see [`crate::tdes_cfb_cmd`] and -//! [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. +//! [`crate::helpers::stream_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. -use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; -use crate::stream_mode_cmd::decrypt_stream; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key}; +use crate::helpers::stream_mode_helpers::decrypt_stream; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB}; pub(crate) fn tdes2_cfb_cmd( diff --git a/cli/src/tdes2_ctr_cmd.rs b/cli/src/tdes2_ctr_cmd.rs index bed1dc651..11eacb36f 100644 --- a/cli/src/tdes2_ctr_cmd.rs +++ b/cli/src/tdes2_ctr_cmd.rs @@ -4,10 +4,10 @@ //! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and //! [`DecryptOnlyAction`] has no `encrypt`. Everything else is `tdes-ctr decrypt` -- the first 6 //! bytes of input are the nonce, the counter starts at zero, at most 512 KiB of data -- see -//! [`crate::tdes_ctr_cmd`] and [`crate::stream_mode_cmd`]. The key is the 16-byte `KEY1 || KEY2`. +//! [`crate::tdes_ctr_cmd`] and [`crate::helpers::stream_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. -use crate::block_mode_cmd::{DecryptOnlyAction, load_key}; -use crate::stream_mode_cmd::decrypt_stream; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key}; +use crate::helpers::stream_mode_helpers::decrypt_stream; use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN_2KEY, TDES2_CTR}; pub(crate) fn tdes2_ctr_cmd( diff --git a/cli/src/tdes2_ecb_cmd.rs b/cli/src/tdes2_ecb_cmd.rs index 409a9359f..f03634d30 100644 --- a/cli/src/tdes2_ecb_cmd.rs +++ b/cli/src/tdes2_ecb_cmd.rs @@ -4,11 +4,11 @@ //! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, 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::block_mode_cmd`] for the framing. The key is the 16-byte `KEY1 || KEY2`. +//! 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}; -use bouncycastle::modes::Decrypting; -use bouncycastle::modes::hazmat::Ecb; +use bouncycastle::cipher::Decrypting; +use bouncycastle::cipher::modes::hazmat::Ecb; use bouncycastle::tdes::hazmat::TDES2Key; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs index d2323523f..bf5f96f50 100644 --- a/cli/src/tdes_cbc_cmd.rs +++ b/cli/src/tdes_cbc_cmd.rs @@ -1,7 +1,7 @@ //! TDES-CBC encryption and decryption, streaming stdin to stdout. //! //! Only the mode wiring lives here: the IV convention, key loading, stdin framing and -//! block-alignment enforcement are all in [`crate::block_mode_cmd`], shared with the AES commands +//! block-alignment enforcement 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. //! @@ -14,8 +14,11 @@ //! 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::block_mode_cmd::{BlockModeAction, decrypt_stream, encrypt_stream, load_key}; -use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; +use crate::helpers::block_mode_helpers::{ + CipherDirection, decrypt_stream, encrypt_stream, load_key, +}; +use bouncycastle::cipher::modes::Cbc; +use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::tdes::hazmat::TDES; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; @@ -23,19 +26,19 @@ use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; const MODE: &str = "CBC"; pub(crate) fn tdes_cbc_cmd( - action: &BlockModeAction, + action: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { let key = load_key::(key, key_file, "TDES"); match action { - BlockModeAction::Encrypt => { + CipherDirection::Encrypt => { encrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( &key, output_hex, MODE, ) } - BlockModeAction::Decrypt => { + CipherDirection::Decrypt => { decrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( &key, output_hex, MODE, ) diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs index 425c637fc..3c96a4175 100644 --- a/cli/src/tdes_cfb8_cmd.rs +++ b/cli/src/tdes_cfb8_cmd.rs @@ -1,7 +1,7 @@ //! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), shared with the +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), 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. //! @@ -18,13 +18,13 @@ //! 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::block_mode_cmd::{BlockModeAction, load_key}; -use crate::stream_mode_cmd::run_stream_mode; -use bouncycastle::modes::{Decrypting, Encrypting}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB8}; pub(crate) fn tdes_cfb8_cmd( - action: &BlockModeAction, + action: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs index 7bb54dcae..ca448bdd9 100644 --- a/cli/src/tdes_cfb_cmd.rs +++ b/cli/src/tdes_cfb_cmd.rs @@ -1,7 +1,7 @@ //! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), shared with the +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), 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`. @@ -19,13 +19,13 @@ //! 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::block_mode_cmd::{BlockModeAction, load_key}; -use crate::stream_mode_cmd::run_stream_mode; -use bouncycastle::modes::{Decrypting, Encrypting}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB}; pub(crate) fn tdes_cfb_cmd( - action: &BlockModeAction, + action: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs index 2b74d704d..f3c47150a 100644 --- a/cli/src/tdes_ctr_cmd.rs +++ b/cli/src/tdes_ctr_cmd.rs @@ -1,7 +1,7 @@ //! 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::stream_mode_cmd`] (and [`crate::block_mode_cmd`] for the key loader), shared with the +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the //! AES stream commands and `tdes-cfb` / `tdes-cfb8`. See those modules for the command-line //! contract. //! @@ -36,13 +36,13 @@ //! 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::block_mode_cmd::{BlockModeAction, load_key}; -use crate::stream_mode_cmd::run_stream_mode; -use bouncycastle::modes::{Decrypting, Encrypting}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN, TDES_CTR}; pub(crate) fn tdes_ctr_cmd( - action: &BlockModeAction, + action: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs index 81996490e..026a93057 100644 --- a/cli/src/tdes_ecb_cmd.rs +++ b/cli/src/tdes_ecb_cmd.rs @@ -1,7 +1,7 @@ //! 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::block_mode_cmd`], shared with the AES commands and `tdes-cbc`. See that module +//! 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. @@ -21,8 +21,8 @@ use crate::helpers::block_mode_helpers::{ CipherDirection, decrypt_stream, encrypt_stream, load_key, }; -use bouncycastle::modes::hazmat::Ecb; -use bouncycastle::modes::{Decrypting, Encrypting}; +use bouncycastle::cipher::modes::hazmat::Ecb; +use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::tdes::hazmat::TDES; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; @@ -30,19 +30,19 @@ use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; const MODE: &str = "ECB"; pub(crate) fn tdes_ecb_cmd( - action: &BlockModeAction, + action: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { let key = load_key::(key, key_file, "TDES"); match action { - BlockModeAction::Encrypt => { + CipherDirection::Encrypt => { encrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( &key, output_hex, MODE, ) } - BlockModeAction::Decrypt => { + CipherDirection::Decrypt => { decrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( &key, output_hex, MODE, ) diff --git a/crypto/tdes/Cargo.toml b/crypto/tdes/Cargo.toml index ad5cee15f..94d0e4a0a 100644 --- a/crypto/tdes/Cargo.toml +++ b/crypto/tdes/Cargo.toml @@ -8,9 +8,8 @@ bouncycastle-core.workspace = true bouncycastle-utils.workspace = true # Only for the TDES mode type aliases in `cbc.rs`, `cfb.rs`, `cfb8.rs`, `ctr.rs` and `ecb.rs`; the # engine itself does not use it. -bouncycastle-modes.workspace = true +bouncycastle-cipher.workspace = true # Only for the padded TDES-CBC and TDES-ECB aliases; the engine itself does not use it. -bouncycastle-padding.workspace = true [dev-dependencies] bouncycastle-core-test-framework.workspace = true diff --git a/crypto/tdes/benches/tdes_benches.rs b/crypto/tdes/benches/tdes_benches.rs index 2f43a4e90..d3ce28688 100644 --- a/crypto/tdes/benches/tdes_benches.rs +++ b/crypto/tdes/benches/tdes_benches.rs @@ -6,8 +6,9 @@ //! 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::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ElectronicCodeBook, RNG}; +use bouncycastle_core::traits::RNG; use bouncycastle_rng as rng; use bouncycastle_tdes::hazmat::TDES; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index da344b74f..f1b6b2467 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -1,8 +1,8 @@ //! Type alias for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), //! with padding. //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle-padding`'s +//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle_cipher::padding`'s //! adapters take five more. This alias pins all of them except the two choices a caller actually //! makes: the direction and the padding scheme. It adds nothing to the engine -- the permutation //! still implements none of the data-encryption traits itself (see the crate docs), the mode does. @@ -21,7 +21,7 @@ //! every use, and makes a mismatched pair a compile error rather than a decryption that returns //! plausible-looking rubbish. //! -//! The two schemes `bouncycastle-padding` provides are [`PKCS7`], which is what almost everyone +//! 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. @@ -31,33 +31,34 @@ //! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the //! block traits: `encrypt_out` / `decrypt_out` and the streaming `do_update_out` / `do_final`, all //! taking a `&[u8]` of any length. The block-aligned API, with its compile-time length checks and -//! its in-place data methods, is `bouncycastle_modes::Cbc` itself, which this wraps: +//! its in-place data methods, is `bouncycastle_cipher::modes::Cbc` itself, which this wraps: //! //! ```text -//! bouncycastle_modes::Cbc // block-aligned, in place +//! bouncycastle_cipher::modes::Cbc // block-aligned, in place //! TDES_CBC // any length, padded //! ``` //! //! # How one alias covers both directions //! //! `PaddedBlockCipherEncryptor` and `PaddedBlockCipherDecryptor` are two distinct types, so a plain type alias cannot -//! select between them on a `Dir` parameter. [`Direction::Select`](bouncycastle_core::stream_cipher::Direction) does it instead: the +//! select between them on a `Dir` parameter. [`Direction::Select`](bouncycastle_cipher::Direction) does it instead: the //! sealed projection in core picks the adapter for the direction, and the alias is written as //! that projection. The only visible consequence is that `Dir` must be -//! [`Encrypting`](bouncycastle_modes::Encrypting) or -//! [`Decrypting`](bouncycastle_modes::Decrypting), which was already true. +//! [`Encrypting`](bouncycastle_cipher::Encrypting) or +//! [`Decrypting`](bouncycastle_cipher::Decrypting), which was already true. use crate::hazmat::{TDES, TDES2Key}; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; -use bouncycastle_core::stream_cipher::Direction; -use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +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_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_padding::{NoPadding, PKCS7}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; // end of imports needed for docs /// TDES in CBC mode with a padding scheme. @@ -70,8 +71,8 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_modes::{Decrypting, Encrypting}; -/// use bouncycastle_padding::PKCS7; +/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_cipher::padding::PKCS7; /// /// type Enc = TDES_CBC; /// type Dec = TDES_CBC; @@ -99,8 +100,8 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_modes::Encrypting; -/// use bouncycastle_padding::NoPadding; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::padding::NoPadding; /// /// type Enc = TDES_CBC; /// @@ -124,8 +125,8 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// use bouncycastle_tdes::TDES_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_modes::Encrypting; -/// use bouncycastle_padding::{NoPadding, PKCS7}; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::padding::{NoPadding, PKCS7}; /// /// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); /// let key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); @@ -163,7 +164,7 @@ pub type TDES_CBC = ::Select< /// use bouncycastle_tdes::TDES2_CBC; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherDecryptor; -/// use bouncycastle_padding::NoPadding; +/// use bouncycastle_cipher::padding::NoPadding; /// /// // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). /// let key = KeyMaterial::<16>::from_bytes_as_type( diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index bbc4a4703..defb6c593 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -1,6 +1,6 @@ //! Type alias for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the +//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the //! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so //! callers never spell them out. It adds nothing to the engine: the permutation still implements //! none of the data-encryption traits itself (see the crate docs), the mode does. @@ -9,14 +9,15 @@ //! 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_modes::Cfb` docs. +//! implemented. See the `bouncycastle_cipher::modes::Cfb` docs. use crate::hazmat::{TDES, TDES2Key}; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; -use bouncycastle_modes::{Cfb, Decrypting}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Cfb; -/// TDES in CFB64 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or -/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// TDES in CFB64 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime check. /// /// CFB is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly /// as long as the plaintext. The IV is one block (8 bytes), generated by encryption and returned; @@ -26,7 +27,7 @@ use bouncycastle_modes::{Cfb, Decrypting}; /// use bouncycastle_tdes::TDES_CFB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// use bouncycastle_cipher::{Decrypting, Encrypting}; /// /// // Three distinct component keys; see `TDES` 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)); diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index d4e69440e..cdc423d79 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -1,6 +1,6 @@ //! Type alias for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb8` takes the permutation, the +//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cfb8` takes the permutation, the //! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so //! callers never spell them out. It adds nothing to the engine: the permutation still implements //! none of the data-encryption traits itself (see the crate docs), the mode does. @@ -8,14 +8,15 @@ //! 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_modes::Cfb8` docs for when that is the right trade. +//! Appendix E. See the `bouncycastle_cipher::modes::Cfb8` docs for when that is the right trade. use crate::hazmat::{TDES, TDES2Key}; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; -use bouncycastle_modes::{Cfb8, Decrypting}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Cfb8; -/// TDES in CFB8 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or -/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// TDES in CFB8 mode. `Dir` is [`bouncycastle_cipher::Encrypting`] or +/// [`bouncycastle_cipher::Decrypting`]; the wrong direction is a compile error, not a runtime check. /// /// CFB8 is a stream cipher with a one-byte segment, so the data is a `&mut [u8]` of any length and /// the ciphertext is exactly as long as the plaintext. The IV is one block (8 bytes), generated by @@ -25,7 +26,7 @@ use bouncycastle_modes::{Cfb8, Decrypting}; /// use bouncycastle_tdes::{TDES_CFB8, TDES_CFB}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// use bouncycastle_cipher::{Decrypting, Encrypting}; /// /// // Three distinct component keys; see `TDES` 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)); diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index aa095e76c..3190dabaf 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -1,6 +1,6 @@ //! Type alias for TDES in CTR mode (NIST SP 800-38A Sec 6.5). //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ctr` takes the permutation, the +//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Ctr` takes the permutation, the //! direction, and the `KEY_LEN` / `BLOCK_LEN` / `INIT_DATA_LEN` const parameters. This alias pins //! the TDES values so callers never spell them out. It adds nothing to the engine: the permutation //! still implements none of the data-encryption traits itself (see the crate docs), the mode does. @@ -30,13 +30,14 @@ use crate::hazmat::{TDES, TDES2Key}; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; -use bouncycastle_modes::{Ctr, Decrypting}; +use bouncycastle_cipher::Decrypting; +use bouncycastle_cipher::modes::Ctr; /// 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_modes::Encrypting`] or -/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// 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. /// /// CTR is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly /// as long as the plaintext. The nonce is generated by encryption and returned; it is never @@ -47,7 +48,7 @@ pub const CTR_NONCE_LEN: usize = 6; /// use bouncycastle_tdes::TDES_CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// use bouncycastle_cipher::{Decrypting, Encrypting}; /// /// // Three distinct component keys; see `TDES` 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)); @@ -89,7 +90,8 @@ pub type TDES_CTR = Ctr; /// use bouncycastle_tdes::hazmat::TDES2Key; /// use bouncycastle_tdes::TDES2_CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{ElectronicCodeBook, StreamCipherDecryptor}; +/// use bouncycastle_core::hazmat::ElectronicCodeBook; +/// use bouncycastle_core::traits::StreamCipherDecryptor; /// /// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); /// let key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); diff --git a/crypto/tdes/src/hazmat/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs index efbb63ebd..970214da0 100644 --- a/crypto/tdes/src/hazmat/ecb.rs +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -1,8 +1,8 @@ //! Type alias for TDES in ECB mode (NIST SP 800-38A Sec 6.1; "TECB" in SP 800-38A Appendix E), //! with padding. //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ecb` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle-padding`'s +//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Ecb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle_cipher::padding`'s //! adapters take five more. This alias pins all of them except the two choices a caller actually //! makes: the direction and the padding scheme. //! @@ -30,27 +30,27 @@ //! //! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the //! block traits. The block-aligned API, with compile-time length checks and in-place data methods, -//! is `bouncycastle_modes::Ecb` itself, which this wraps. ECB has no IV, so `INIT_DATA_LEN` is 0: +//! is `bouncycastle_cipher::modes::Ecb` itself, which this wraps. 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. //! //! # How one alias covers both directions //! -//! See [`Direction::Select`](bouncycastle_core::stream_cipher::Direction), which is the projection that lets `Dir` select between the encryptor and the +//! See [`Direction::Select`](bouncycastle_cipher::Direction), which is the projection that lets `Dir` select between the encryptor and the //! decryptor adapter. `Dir` must be [`Encrypting`] or [`Decrypting`]. use crate::hazmat::{TDES, TDES2Key}; use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; -use bouncycastle_core::stream_cipher::Direction; -use bouncycastle_modes::hazmat::Ecb; -use bouncycastle_modes::{Decrypting, Encrypting}; -use bouncycastle_padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; // Imports needed for docs #[allow(unused_imports)] -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_padding::{NoPadding, PKCS7}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; // end of imports needed for docs /// TDES in ECB mode with a padding scheme. @@ -66,8 +66,8 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// use bouncycastle_tdes::hazmat::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_modes::{Decrypting, Encrypting}; -/// use bouncycastle_padding::PKCS7; +/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_cipher::padding::PKCS7; /// /// type Enc = TDES_ECB; /// type Dec = TDES_ECB; @@ -93,8 +93,8 @@ use bouncycastle_padding::{NoPadding, PKCS7}; /// use bouncycastle_tdes::hazmat::TDES_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_modes::Encrypting; -/// use bouncycastle_padding::NoPadding; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::padding::NoPadding; /// /// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); /// let key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); @@ -134,7 +134,7 @@ pub type TDES_ECB = ::Select< /// use bouncycastle_tdes::hazmat::TDES2_ECB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherDecryptor; -/// use bouncycastle_padding::NoPadding; +/// use bouncycastle_cipher::padding::NoPadding; /// /// // NIST CAVP TECBMMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). /// let key = KeyMaterial::<16>::from_bytes_as_type( diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs index 81e7fbca3..f6e39aa35 100644 --- a/crypto/tdes/src/hazmat/tdes.rs +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -6,9 +6,10 @@ use crate::des::{Subkeys, inverse_ip, ip, rounds}; use crate::schedule::{PARITY_MASK, expand, is_weak}; use crate::{BLOCK_LEN, KEY_LEN}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; +use bouncycastle_core::traits::Algorithm; use bouncycastle_utils::ct::ct_eq_zero_bytes; use bouncycastle_utils::secret::Secret; diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index 61a606afe..ad60ce746 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -8,9 +8,10 @@ use crate::hazmat::tdes::{ }; use crate::schedule::expand; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook}; +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 @@ -20,13 +21,14 @@ use bouncycastle_utils::secret::Secret; /// 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 `Encrypting` mode in -/// `bouncycastle-modes` refuses to be built over it **at compile time**: +/// `bouncycastle_cipher::modes` refuses to be built over it **at compile time**: /// /// ```compile_fail /// use bouncycastle_tdes::hazmat::TDES2Key; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::BlockCipherEncryptor; -/// use bouncycastle_modes::{Cbc, Encrypting}; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::modes::Cbc; /// /// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); /// let key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); @@ -43,7 +45,8 @@ use bouncycastle_utils::secret::Secret; /// use bouncycastle_tdes::hazmat::TDES2Key; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; /// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_modes::{Ctr, Encrypting}; +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::modes::Ctr; /// /// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); /// let key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index eeaa6a05e..452e301b2 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -14,7 +14,7 @@ //! ``` //! use bouncycastle_tdes::hazmat::TDES; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::ElectronicCodeBook; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! //! // NIST CAVP TECBMMT3.rsp, [ENCRYPT] COUNT = 0: KEY1 || KEY2 || KEY3. //! let key = KeyMaterial::<24>::from_bytes_as_type( @@ -37,7 +37,7 @@ //! //! ## Modes of operation //! -//! To encrypt more than one block, use a mode of operation from `bouncycastle-modes`. This crate +//! To encrypt more than one block, use a mode of operation from `bouncycastle_cipher::modes`. This crate //! provides aliases that fill in the const parameters, leaving only the choices a caller actually //! makes: [`TDES_CBC`] for CBC (SP 800-38A Sec 6.2), which takes the direction **and a padding //! scheme**, and [`TDES_CFB`] for CFB64 (Sec 6.3), which takes only the direction. [`TDES_CFB8`] @@ -53,15 +53,15 @@ //! //! CBC is a block cipher, so it is defined only on whole blocks and the alias 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-modes` crate docs for the comparison, and +//! 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::TDES_CBC; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_modes::{Decrypting, Encrypting}; -//! use bouncycastle_padding::PKCS7; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_cipher::padding::PKCS7; //! //! // Three distinct component keys; see `TDES::new` 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)); @@ -81,7 +81,7 @@ //! ``` //! //! For the block-aligned API -- whole blocks in place, with the length checked at compile time -- -//! name `bouncycastle_modes::Cbc` directly; that is what these aliases wrap. +//! name `bouncycastle_cipher::modes::Cbc` directly; that is what these aliases wrap. //! //! There is no one-shot static on the permutation, because `TDES::new(&key)?.encrypt_block(..)` //! already *is* the one shot. Data-level one-shots belong to the modes of operation, which take @@ -107,7 +107,7 @@ //! use bouncycastle_tdes::TDES2_CBC; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::SymmetricCipherDecryptor; -//! use bouncycastle_padding::NoPadding; +//! use bouncycastle_cipher::padding::NoPadding; //! //! // NIST CAVP TCBCMMT2.rsp, [DECRYPT] COUNT = 0. //! let key = KeyMaterial::<16>::from_bytes_as_type( diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs index 91762d230..c2a62cf85 100644 --- a/crypto/tdes/tests/cavp_mmt_tests.rs +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -19,7 +19,7 @@ //! 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 `bouncycastle-modes` has no API for a caller-supplied +//! The CBC and CFB vectors carry an IV, and `bouncycastle_cipher::modes` has no API for a caller-supplied //! IV. Three-key encryption therefore uses `do_encrypt_init_rng` with 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 `TDES_*` aliases are used, so the aliases' @@ -33,14 +33,15 @@ //! 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::Cbc; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, StreamCipherDecryptor, - StreamCipherEncryptor, + BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; -use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; use bouncycastle_tdes::hazmat::{TDES, TDES2Key}; use bouncycastle_tdes::{ BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES_CFB, TDES_CFB8, TDES2_CFB, TDES2_CFB8, diff --git a/crypto/tdes/tests/cbc_alias_tests.rs b/crypto/tdes/tests/cbc_alias_tests.rs index 8ed05393e..f07667149 100644 --- a/crypto/tdes/tests/cbc_alias_tests.rs +++ b/crypto/tdes/tests/cbc_alias_tests.rs @@ -5,12 +5,13 @@ //! the padding scheme changes the behaviour rather than being decorative. The mode and the padding //! layer are tested in their own crates; this checks the wiring between them at the 8-byte block. -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; -use bouncycastle_padding::{ +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_tdes::hazmat::TDES; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES_CBC}; diff --git a/crypto/tdes/tests/ecb_alias_tests.rs b/crypto/tdes/tests/ecb_alias_tests.rs index 5b0dfbfa4..158e7cfec 100644 --- a/crypto/tdes/tests/ecb_alias_tests.rs +++ b/crypto/tdes/tests/ecb_alias_tests.rs @@ -2,16 +2,17 @@ //! //! As with the CBC alias, this is only a type alias, so what is worth testing is that both //! parameters select: the direction picks the encryptor or the decryptor, and the padding scheme -//! reaches the behaviour. ECB's own properties are tested in `bouncycastle-modes`; what is specific +//! reaches the behaviour. ECB's own properties are tested in `bouncycastle_cipher::modes`; what is specific //! here is the 8-byte block, and that `INIT_DATA_LEN` is 0 so the projection carries a different //! value than CBC's. -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; -use bouncycastle_padding::{ +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; use bouncycastle_tdes::hazmat::{TDES, TDES_ECB}; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index 3a94c96a9..59d1b20bf 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -9,9 +9,9 @@ //! * The `KeyMaterial` checks: type, length and security strength. use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::Algorithm; use bouncycastle_tdes::hazmat::{TDES, TDES2Key}; diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index 681043c3e..8dd9f0c1e 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -35,6 +35,7 @@ #![allow(dead_code)] #![allow(unused_imports)] +use bouncycastle::core::hazmat::ElectronicCodeBook; use bouncycastle::core::key_material::{KeyMaterial, KeyType}; use bouncycastle::tdes::hazmat::{TDES, TDES2Key}; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; From cb5707911768ce14a0f0cd21106e95b4b8274f12 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 24 Oct 2026 22:00:22 +1100 Subject: [PATCH 14/32] tdes: onto the key objects -- TDESKey hands out the mode workers, the engines become TDESInternal / TDES2Internal The engines are renamed for what they are under the key objects of feature/key-rework-memory: TDES -> TDESInternal and TDES2Key -> TDES2Internal, each Clone, implementing ElectronicCodeBook (new, ENCRYPTION_APPROVED) and BlockPermutation (the block methods) separately, and ModeNames for the five modes offered (the AEAD and MAC names are empty). TDESKey> mirrors AESKey: the key holds the mode's key state, FromKeyMaterial<24> / <16> build it per engine, and the six role impls (symmetric, block and ECB, each direction) forward to the mode traits. TDES3Key and TDES2Key name the engines. A TDES2Key carries the same roles; its create_encryptor fails to compile through the const assert the modes make on ENCRYPTION_APPROVED, which the compile_fail doctests on TDES2Internal now express at the key. The aliases take the key-state lifetime (TDES_CBC<'k, Dir, Pad>, ...), and the docs and examples build a key and call encrypt / decrypt on it. CTR has no usable default for an 8-byte block, so its key is TDES3Key>. Tests: cbc_alias_tests and ecb_alias_tests become padded_alias_tests and alg_name_tests on the key; mode_key_tests runs the FromKeyMaterial, block, symmetric and stream frameworks over every mode and checks the two-key decryptors against the raw permutation; cavp_mmt_tests drives the CBC and CFB vectors through the keys and the ECB ones through the engines. CLI: the ten tdes commands load a TDES3Key / TDES2Key under NoPadding and keep their contract -- whole blocks, the IV in band, no --iv -- and the tdes2-* commands take DecryptOnlyAction. Checked against the TECBMMT3, TCBCMMT2, TECBMMT2, TCFB64MMT2 and TCFB8MMT2 COUNT = 0 vectors. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/helpers/block_mode_helpers.rs | 7 + cli/src/main.rs | 415 +++++++++++++++++- cli/src/tdes2_cbc_cmd.rs | 27 +- cli/src/tdes2_cfb8_cmd.rs | 11 +- cli/src/tdes2_cfb_cmd.rs | 11 +- cli/src/tdes2_ctr_cmd.rs | 13 +- cli/src/tdes2_ecb_cmd.rs | 22 +- cli/src/tdes_cbc_cmd.rs | 32 +- cli/src/tdes_cfb8_cmd.rs | 12 +- cli/src/tdes_cfb_cmd.rs | 12 +- cli/src/tdes_ctr_cmd.rs | 13 +- cli/src/tdes_ecb_cmd.rs | 29 +- crypto/tdes/benches/tdes_benches.rs | 14 +- crypto/tdes/src/cbc.rs | 174 ++++---- crypto/tdes/src/cfb.rs | 73 +-- crypto/tdes/src/cfb8.rs | 66 +-- crypto/tdes/src/ctr.rs | 91 ++-- crypto/tdes/src/hazmat/ecb.rs | 139 +++--- crypto/tdes/src/hazmat/mod.rs | 15 +- crypto/tdes/src/hazmat/tdes.rs | 166 +++---- crypto/tdes/src/hazmat/tdes2.rs | 133 +++--- crypto/tdes/src/key.rs | 234 ++++++++++ crypto/tdes/src/lib.rs | 180 ++++---- crypto/tdes/tests/alg_name_tests.rs | 177 ++++++++ crypto/tdes/tests/cavp_mmt_tests.rs | 178 ++++---- crypto/tdes/tests/cbc_alias_tests.rs | 123 ------ crypto/tdes/tests/ecb_alias_tests.rs | 116 ----- .../tdes/tests/electronic_code_book_tests.rs | 9 +- crypto/tdes/tests/key_tests.rs | 82 ++-- crypto/tdes/tests/mode_key_tests.rs | 168 +++++++ crypto/tdes/tests/padded_alias_tests.rs | 153 +++++++ mem_usage_benches/src/bench_tdes_mem_usage.rs | 18 +- 32 files changed, 1930 insertions(+), 983 deletions(-) create mode 100644 crypto/tdes/src/key.rs create mode 100644 crypto/tdes/tests/alg_name_tests.rs delete mode 100644 crypto/tdes/tests/cbc_alias_tests.rs delete mode 100644 crypto/tdes/tests/ecb_alias_tests.rs create mode 100644 crypto/tdes/tests/mode_key_tests.rs create mode 100644 crypto/tdes/tests/padded_alias_tests.rs diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index 7205710e4..2ea2756e5 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -84,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. /// 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 index 7643f0be8..e70dbab60 100644 --- a/cli/src/tdes2_cbc_cmd.rs +++ b/cli/src/tdes2_cbc_cmd.rs @@ -1,16 +1,16 @@ //! 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, -//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! `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, the rest whole 8-byte blocks -- 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_key}; -use bouncycastle::cipher::Decrypting; -use bouncycastle::cipher::modes::Cbc; -use bouncycastle::tdes::hazmat::TDES2Key; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, decrypt_stream, load_key_object}; +use bouncycastle::cipher::modes::CBC; +use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::core::symmetric_key::SymmetricDecryptionKey; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; pub(crate) fn tdes2_cbc_cmd( action: &DecryptOnlyAction, @@ -19,11 +19,12 @@ pub(crate) fn tdes2_cbc_cmd( output_hex: bool, ) { let DecryptOnlyAction::Decrypt = action; - let key = load_key::(key, key_file, "two-key TDES"); - decrypt_stream::< - Cbc, - KEY_LEN_2KEY, - BLOCK_LEN, - BLOCK_LEN, - >(&key, output_hex, "CBC") + let key = + load_key_object::>, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( + None, + |iv| key.create_decryptor(iv), + output_hex, + Some("CBC"), + ) } diff --git a/cli/src/tdes2_cfb8_cmd.rs b/cli/src/tdes2_cfb8_cmd.rs index c7e862e1d..bdcad835b 100644 --- a/cli/src/tdes2_cfb8_cmd.rs +++ b/cli/src/tdes2_cfb8_cmd.rs @@ -1,14 +1,15 @@ //! 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, -//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! `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, 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_key}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key_object}; use crate::helpers::stream_mode_helpers::decrypt_stream; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB8}; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; pub(crate) fn tdes2_cfb8_cmd( action: &DecryptOnlyAction, @@ -17,6 +18,6 @@ pub(crate) fn tdes2_cfb8_cmd( output_hex: bool, ) { let DecryptOnlyAction::Decrypt = action; - let key = load_key::(key, key_file, "two-key TDES"); - decrypt_stream::(&key, output_hex) + let key = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, KEY_LEN_2KEY, BLOCK_LEN>(&key, None, output_hex) } diff --git a/cli/src/tdes2_cfb_cmd.rs b/cli/src/tdes2_cfb_cmd.rs index df85e840b..9467a47af 100644 --- a/cli/src/tdes2_cfb_cmd.rs +++ b/cli/src/tdes2_cfb_cmd.rs @@ -1,14 +1,15 @@ //! 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, -//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! `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, 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_key}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key_object}; use crate::helpers::stream_mode_helpers::decrypt_stream; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2_CFB}; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; pub(crate) fn tdes2_cfb_cmd( action: &DecryptOnlyAction, @@ -17,6 +18,6 @@ pub(crate) fn tdes2_cfb_cmd( output_hex: bool, ) { let DecryptOnlyAction::Decrypt = action; - let key = load_key::(key, key_file, "two-key TDES"); - decrypt_stream::(&key, output_hex) + let key = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + decrypt_stream::<_, KEY_LEN_2KEY, BLOCK_LEN>(&key, None, output_hex) } diff --git a/cli/src/tdes2_ctr_cmd.rs b/cli/src/tdes2_ctr_cmd.rs index 11eacb36f..da1cefdb5 100644 --- a/cli/src/tdes2_ctr_cmd.rs +++ b/cli/src/tdes2_ctr_cmd.rs @@ -1,14 +1,15 @@ //! 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, -//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! `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, 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_key}; +use crate::helpers::block_mode_helpers::{DecryptOnlyAction, load_key_object}; use crate::helpers::stream_mode_helpers::decrypt_stream; -use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN_2KEY, TDES2_CTR}; +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, @@ -17,6 +18,8 @@ pub(crate) fn tdes2_ctr_cmd( output_hex: bool, ) { let DecryptOnlyAction::Decrypt = action; - let key = load_key::(key, key_file, "two-key TDES"); - decrypt_stream::(&key, output_hex) + let key = load_key_object::>, KEY_LEN_2KEY>( + key, key_file, "two-key TDES", + ); + decrypt_stream::<_, KEY_LEN_2KEY, CTR_NONCE_LEN>(&key, None, output_hex) } diff --git a/cli/src/tdes2_ecb_cmd.rs b/cli/src/tdes2_ecb_cmd.rs index f03634d30..815cde98d 100644 --- a/cli/src/tdes2_ecb_cmd.rs +++ b/cli/src/tdes2_ecb_cmd.rs @@ -1,16 +1,16 @@ //! 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, -//! `bouncycastle-tdes` will not compile an encrypting mode over `TDES2Key`, and +//! `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}; -use bouncycastle::cipher::Decrypting; -use bouncycastle::cipher::modes::hazmat::Ecb; -use bouncycastle::tdes::hazmat::TDES2Key; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY}; +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, @@ -19,8 +19,12 @@ pub(crate) fn tdes2_ecb_cmd( output_hex: bool, ) { let DecryptOnlyAction::Decrypt = action; - let key = load_key::(key, key_file, "two-key TDES"); - decrypt_stream::, KEY_LEN_2KEY, 0, BLOCK_LEN>( - &key, output_hex, "ECB", + 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 index bf5f96f50..66596940a 100644 --- a/cli/src/tdes_cbc_cmd.rs +++ b/cli/src/tdes_cbc_cmd.rs @@ -15,12 +15,12 @@ //! 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_key, + CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, }; -use bouncycastle::cipher::modes::Cbc; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::tdes::hazmat::TDES; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; +use bouncycastle::cipher::modes::CBC; +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 = "CBC"; @@ -31,17 +31,23 @@ pub(crate) fn tdes_cbc_cmd( key_file: &Option, output_hex: bool, ) { - let key = load_key::(key, key_file, "TDES"); + // `NoPadding`: whole blocks only, as the command's contract says. + let key = load_key_object::>, KEY_LEN>(key, key_file, "TDES"); match action { CipherDirection::Encrypt => { - encrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( - &key, output_hex, MODE, - ) - } - CipherDirection::Decrypt => { - decrypt_stream::, KEY_LEN, BLOCK_LEN, BLOCK_LEN>( - &key, output_hex, MODE, + let (enc, generated_iv) = start_encryptor(None, |rng| key.create_encryptor(rng)); + encrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( + enc, + generated_iv.as_ref(), + output_hex, + Some(MODE), ) } + CipherDirection::Decrypt => decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( + None, + |iv| key.create_decryptor(iv), + output_hex, + Some(MODE), + ), } } diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs index 3c96a4175..600d75a53 100644 --- a/cli/src/tdes_cfb8_cmd.rs +++ b/cli/src/tdes_cfb8_cmd.rs @@ -18,10 +18,10 @@ //! 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_key}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB8}; +use bouncycastle::cipher::modes::CFB8; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; pub(crate) fn tdes_cfb8_cmd( action: &CipherDirection, @@ -29,8 +29,6 @@ pub(crate) fn tdes_cfb8_cmd( key_file: &Option, output_hex: bool, ) { - let key = load_key::(key, key_file, "TDES"); - run_stream_mode::, TDES_CFB8, KEY_LEN, BLOCK_LEN>( - action, &key, output_hex, - ) + let key = load_key_object::, KEY_LEN>(key, key_file, "TDES"); + run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, None, output_hex) } diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs index ca448bdd9..a699b3a27 100644 --- a/cli/src/tdes_cfb_cmd.rs +++ b/cli/src/tdes_cfb_cmd.rs @@ -19,10 +19,10 @@ //! 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_key}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES_CFB}; +use bouncycastle::cipher::modes::CFB; +use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; pub(crate) fn tdes_cfb_cmd( action: &CipherDirection, @@ -30,8 +30,6 @@ pub(crate) fn tdes_cfb_cmd( key_file: &Option, output_hex: bool, ) { - let key = load_key::(key, key_file, "TDES"); - run_stream_mode::, TDES_CFB, KEY_LEN, BLOCK_LEN>( - action, &key, output_hex, - ) + let key = load_key_object::, KEY_LEN>(key, key_file, "TDES"); + run_stream_mode::<_, KEY_LEN, BLOCK_LEN>(action, &key, None, output_hex) } diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs index f3c47150a..81dbd11c8 100644 --- a/cli/src/tdes_ctr_cmd.rs +++ b/cli/src/tdes_ctr_cmd.rs @@ -36,10 +36,10 @@ //! 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_key}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::tdes::{CTR_NONCE_LEN, KEY_LEN, TDES_CTR}; +use bouncycastle::cipher::modes::CTR; +use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES3Key}; pub(crate) fn tdes_ctr_cmd( action: &CipherDirection, @@ -47,8 +47,7 @@ pub(crate) fn tdes_ctr_cmd( key_file: &Option, output_hex: bool, ) { - let key = load_key::(key, key_file, "TDES"); - run_stream_mode::, TDES_CTR, KEY_LEN, CTR_NONCE_LEN>( - action, &key, output_hex, - ) + let key = + load_key_object::>, KEY_LEN>(key, key_file, "TDES"); + run_stream_mode::<_, KEY_LEN, CTR_NONCE_LEN>(action, &key, None, output_hex) } diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs index 026a93057..f96fb45dc 100644 --- a/cli/src/tdes_ecb_cmd.rs +++ b/cli/src/tdes_ecb_cmd.rs @@ -19,12 +19,12 @@ //! the `bouncycastle-tdes` crate docs. use crate::helpers::block_mode_helpers::{ - CipherDirection, decrypt_stream, encrypt_stream, load_key, + CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, }; -use bouncycastle::cipher::modes::hazmat::Ecb; -use bouncycastle::cipher::{Decrypting, Encrypting}; -use bouncycastle::tdes::hazmat::TDES; -use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; +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"; @@ -35,17 +35,18 @@ pub(crate) fn tdes_ecb_cmd( key_file: &Option, output_hex: bool, ) { - let key = load_key::(key, key_file, "TDES"); + let key = load_key_object::>, KEY_LEN>(key, key_file, "TDES"); match action { CipherDirection::Encrypt => { - encrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( - &key, output_hex, MODE, - ) - } - CipherDirection::Decrypt => { - decrypt_stream::, KEY_LEN, 0, BLOCK_LEN>( - &key, output_hex, MODE, - ) + // 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/crypto/tdes/benches/tdes_benches.rs b/crypto/tdes/benches/tdes_benches.rs index d3ce28688..e69339a91 100644 --- a/crypto/tdes/benches/tdes_benches.rs +++ b/crypto/tdes/benches/tdes_benches.rs @@ -1,4 +1,4 @@ -//! Criterion benchmarks for the constant-time TDES engine. +//! 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 @@ -6,16 +6,16 @@ //! 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::ElectronicCodeBook; +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::TDES; +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 TDES blocks. +/// 16 KiB of data, i.e. 2048 TDESInternal blocks. const NUM_BLOCKS: usize = 2048; const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; @@ -37,7 +37,7 @@ fn key() -> KeyMaterial { generator.next_bytes_out(&mut bytes).unwrap(); let key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) .unwrap(); - if TDES::new(&key).is_ok() { + if TDESInternal::new(&key).is_ok() { return key; } } @@ -47,13 +47,13 @@ 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(TDES::new(black_box(&key)).unwrap())) + b.iter(|| black_box(TDESInternal::new(black_box(&key)).unwrap())) }); group.finish(); } fn bench_tdes(c: &mut Criterion) { - let tdes = TDES::new(&key()).unwrap(); + let tdes = TDESInternal::new(&key()).unwrap(); let blocks = random_blocks(); let mut group = c.benchmark_group("tdes::TDES"); diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index f1b6b2467..2a4a28036 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -1,54 +1,30 @@ -//! Type alias for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), +//! Type aliases for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), //! with padding. //! -//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle_cipher::padding`'s -//! adapters take five more. This alias pins all of them except the two choices a caller actually -//! makes: the direction and the padding scheme. It adds nothing to the engine -- the permutation -//! still implements none of the data-encryption traits itself (see the crate docs), the mode does. +//! 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. //! -//! ```text -//! TDES_CBC // TDES, CBC, PKCS#7 padded, encrypting -//! TDES_CBC -//! ``` -//! -//! # Why the padding is part of the alias +//! # 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 type 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. -//! -//! # This is the arbitrary-length API -//! -//! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the -//! block traits: `encrypt_out` / `decrypt_out` and the streaming `do_update_out` / `do_final`, all -//! taking a `&[u8]` of any length. The block-aligned API, with its compile-time length checks and -//! its in-place data methods, is `bouncycastle_cipher::modes::Cbc` itself, which this wraps: -//! -//! ```text -//! bouncycastle_cipher::modes::Cbc // block-aligned, in place -//! TDES_CBC // any length, padded -//! ``` +//! 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. //! -//! # How one alias covers both directions -//! -//! `PaddedBlockCipherEncryptor` and `PaddedBlockCipherDecryptor` are two distinct types, so a plain type alias cannot -//! select between them on a `Dir` parameter. [`Direction::Select`](bouncycastle_cipher::Direction) does it instead: the -//! sealed projection in core picks the adapter for the direction, and the alias is written as -//! that projection. The only visible consequence is that `Dir` must be -//! [`Encrypting`](bouncycastle_cipher::Encrypting) or -//! [`Decrypting`](bouncycastle_cipher::Decrypting), which was already true. +//! 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. -use crate::hazmat::{TDES, TDES2Key}; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; use bouncycastle_cipher::Direction; use bouncycastle_cipher::modes::Cbc; use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; @@ -58,131 +34,133 @@ use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; // end of imports needed for docs -/// TDES in CBC mode with a padding scheme. +/// 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 generated by encryption and -/// returned; it is never supplied. It is one block: 8 bytes. +/// 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::TDES_CBC; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// 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, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// type Enc = TDES_CBC; -/// type Dec = TDES_CBC; -/// -/// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// 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 message = b"hello"; -/// let mut ciphertext = [0u8; 8]; -/// let (iv, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); -/// assert_eq!(written, 8); +/// let (iv, ciphertext) = key.encrypt(&mut rng, b"hello").expect("encryption"); +/// assert_eq!(ciphertext.len(), 8); /// -/// let mut plaintext = [0u8; 8]; -/// let n = Dec::decrypt_out(&key, &iv, &ciphertext, &mut plaintext).expect("decryption"); -/// assert_eq!(&plaintext[..n], message); +/// 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_final` rather than something silently padded: +/// error at `do_encrypt_final` rather than something silently padded: /// /// ``` -/// use bouncycastle_tdes::TDES_CBC; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::Encrypting; +/// 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; /// -/// type Enc = TDES_CBC; -/// +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// 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 (_iv, written) = Enc::encrypt_out(&key, &[0u8; 8], &mut out).expect("aligned"); +/// 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. -/// let mut out = [0u8; 8]; -/// assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +/// assert!(key.encrypt_out(&mut rng, b"hello", &mut out).is_err()); /// ``` /// -/// The padding scheme is part of the type, so the two schemes are different types and cannot be -/// interchanged. A value built with one will not satisfy a binding annotated with the other: +/// 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; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// let key = TDES3Key::>::from_key_material(&material).unwrap(); /// -/// // Built as NoPadding, annotated as PKCS7: mismatched types. -/// let (enc, _iv) = TDES_CBC::::do_encrypt_init(&key).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; /// ``` #[allow(non_camel_case_types)] -pub type TDES_CBC = ::Select< +pub type TDES_CBC<'k, Dir, Pad> = ::Select< PaddedBlockCipherEncryptor< - Cbc, + Cbc<'k, TDESInternal, Encrypting, BLOCK_LEN>, Pad, - KEY_LEN, BLOCK_LEN, BLOCK_LEN, >, PaddedBlockCipherDecryptor< - Cbc, + Cbc<'k, TDESInternal, Decrypting, BLOCK_LEN>, Pad, - KEY_LEN, BLOCK_LEN, BLOCK_LEN, >, >; -/// Two-key TDES in CBC mode with a padding scheme, **decryption only**. +/// 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 -/// `Cbc` does not compile; see [`TDES2Key`]. The IV is one 8-byte block, -/// supplied by the caller as it was received. +/// 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::TDES2_CBC; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherDecryptor; +/// 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 key = KeyMaterial::<16>::from_bytes_as_type( +/// 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 = TDES2_CBC::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +/// 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 = PaddedBlockCipherDecryptor< - Cbc, +pub type TDES2_CBC<'k, Pad> = PaddedBlockCipherDecryptor< + Cbc<'k, TDES2Internal, Decrypting, BLOCK_LEN>, Pad, - KEY_LEN_2KEY, BLOCK_LEN, BLOCK_LEN, >; diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index defb6c593..2656d9a4f 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -1,9 +1,10 @@ -//! Type alias for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. +//! Type aliases for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. //! -//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so -//! callers never spell them out. It adds nothing to the engine: the permutation still implements -//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! 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 @@ -11,71 +12,79 @@ //! non-interoperable mode with its own alias, [`TDES_CFB8`](crate::TDES_CFB8); `s = 1` is not //! implemented. See the `bouncycastle_cipher::modes::Cfb` docs. -use crate::hazmat::{TDES, TDES2Key}; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +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. /// -/// CFB is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly -/// as long as the plaintext. The IV is one block (8 bytes), generated by encryption and returned; -/// it is never supplied. Encryption and decryption work in place. -/// /// ``` -/// use bouncycastle_tdes::TDES_CFB; +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::CFB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// 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) = TDES_CFB::::encrypt_in_place(&key, &mut data).unwrap(); +/// let (_, iv) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); /// assert_ne!(data, message); -/// TDES_CFB::::decrypt_in_place(&key, &iv, &mut data).unwrap(); +/// key.decrypt_inplace(&iv, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // Streaming, at any byte boundary: -/// let (mut enc, iv) = TDES_CFB::::do_encrypt_init(&key).unwrap(); +/// let (mut enc, iv) = key.create_encryptor(&mut rng).unwrap(); /// let mut first = [0u8; 5]; /// let mut rest = [1u8; 30]; -/// enc.do_encrypt(&mut first).unwrap(); -/// enc.do_encrypt(&mut rest).unwrap(); -/// let mut dec = TDES_CFB::::do_decrypt_init(&key, &iv).unwrap(); -/// dec.do_decrypt(&mut first).unwrap(); -/// dec.do_decrypt(&mut rest).unwrap(); +/// 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]); /// ``` #[allow(non_camel_case_types)] -pub type TDES_CFB = Cfb; +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 `Cfb` does not -/// compile; see [`TDES2Key`]. The IV is one 8-byte block, supplied by the caller as received. +/// 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::TDES2_CFB; +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CFB; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::StreamCipherDecryptor; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; /// /// // NIST CAVP TCFB64MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). -/// let key = KeyMaterial::<16>::from_bytes_as_type( +/// 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]; /// -/// TDES2_CFB::decrypt_in_place(&key, &iv, &mut data).expect("decryption"); +/// 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 = Cfb; +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 index cdc423d79..18218a8c4 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -1,75 +1,83 @@ -//! Type alias for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). +//! Type aliases for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). //! -//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Cfb8` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. This alias pins the TDES values so -//! callers never spell them out. It adds nothing to the engine: the permutation still implements -//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! 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. -use crate::hazmat::{TDES, TDES2Key}; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +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. /// -/// CFB8 is a stream cipher with a one-byte segment, so the data is a `&mut [u8]` of any length and -/// the ciphertext is exactly as long as the plaintext. The IV is one block (8 bytes), generated by -/// encryption and returned; it is never supplied. Encryption and decryption work in place. -/// /// ``` -/// use bouncycastle_tdes::{TDES_CFB8, TDES_CFB}; +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::{CFB, CFB8}; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; -/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// 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) = TDES_CFB8::::encrypt_in_place(&key, &mut data).unwrap(); +/// let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut data).unwrap(); /// assert_ne!(data, message); -/// TDES_CFB8::::decrypt_in_place(&key, &iv, &mut data).unwrap(); +/// cfb8_key.decrypt_inplace(&iv, &mut data).unwrap(); /// assert_eq!(data, message); /// -/// // CFB8 and CFB64 are not interchangeable: same key, same IV, different ciphertext. +/// // 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) = TDES_CFB8::::encrypt_in_place(&key, &mut as_cfb8).unwrap(); +/// let (_, iv) = cfb8_key.encrypt_inplace(&mut rng, &mut as_cfb8).unwrap(); /// let mut as_cfb64 = as_cfb8; -/// TDES_CFB::::decrypt_in_place(&key, &iv, &mut as_cfb64).unwrap(); +/// cfb64_key.decrypt_inplace(&iv, &mut as_cfb64).unwrap(); /// assert_ne!(as_cfb64, message); /// ``` #[allow(non_camel_case_types)] -pub type TDES_CFB8 = Cfb8; +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 `Cfb8` does not -/// compile; see [`TDES2Key`]. The IV is one 8-byte block, supplied by the caller as received. +/// 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::TDES2_CFB8; +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CFB8; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::StreamCipherDecryptor; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; /// /// // NIST CAVP TCFB8MMT2.rsp, [DECRYPT] COUNT = 0: KEY1 || KEY2 (KEY3 = KEY1). -/// let key = KeyMaterial::<16>::from_bytes_as_type( +/// 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]; /// -/// TDES2_CFB8::decrypt_in_place(&key, &iv, &mut data).expect("decryption"); +/// key.decrypt_inplace(&iv, &mut data).expect("decryption"); /// assert_eq!(data, [0x80]); /// ``` #[allow(non_camel_case_types)] -pub type TDES2_CFB8 = Cfb8; +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 index 3190dabaf..ad3f8b03c 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -1,9 +1,13 @@ -//! Type alias for TDES in CTR mode (NIST SP 800-38A Sec 6.5). +//! Type aliases for TDES in CTR mode (NIST SP 800-38A Sec 6.5). //! -//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Ctr` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` / `INIT_DATA_LEN` const parameters. This alias pins -//! the TDES values so callers never spell them out. It adds nothing to the engine: the permutation -//! still implements none of the data-encryption traits itself (see the crate docs), the mode does. +//! 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 @@ -25,86 +29,97 @@ //! 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 this alias chooses. Name -//! `Ctr` directly for a different split. +//! 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. -use crate::hazmat::{TDES, TDES2Key}; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +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. /// -/// CTR is a stream cipher, so the data is a `&mut [u8]` of any length and the ciphertext is exactly -/// as long as the plaintext. The nonce is generated by encryption and returned; it is never -/// supplied. Encryption and decryption work in place, and are the same operation. A message longer -/// than 65 536 blocks (512 KiB) is an error, not a repeated counter; see the module docs. +/// A message longer than 65 536 blocks (512 KiB) is an error, not a repeated counter; see the +/// module docs. /// /// ``` -/// use bouncycastle_tdes::TDES_CTR; +/// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES3Key}; +/// use bouncycastle_cipher::modes::CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +/// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// 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) = TDES_CTR::::encrypt_in_place(&key, &mut data).unwrap(); +/// let (_, nonce) = key.encrypt_inplace(&mut rng, &mut data).unwrap(); /// assert_eq!(nonce.len(), 6); /// assert_ne!(data, message); -/// TDES_CTR::::decrypt_in_place(&key, &nonce, &mut data).unwrap(); +/// key.decrypt_inplace(&nonce, &mut data).unwrap(); /// assert_eq!(data, message); /// /// // Streaming, at any byte boundary: -/// let (mut enc, nonce) = TDES_CTR::::do_encrypt_init(&key).unwrap(); +/// let (mut enc, nonce) = key.create_encryptor(&mut rng).unwrap(); /// let mut first = [0u8; 5]; /// let mut rest = [1u8; 30]; -/// enc.do_encrypt(&mut first).unwrap(); -/// enc.do_encrypt(&mut rest).unwrap(); -/// let mut dec = TDES_CTR::::do_decrypt_init(&key, &nonce).unwrap(); -/// dec.do_decrypt(&mut first).unwrap(); -/// dec.do_decrypt(&mut rest).unwrap(); +/// 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]); /// ``` #[allow(non_camel_case_types)] -pub type TDES_CTR = Ctr; +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 `Ctr` -/// does not compile; see [`TDES2Key`]. 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`]. +/// 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::TDES2Key; -/// use bouncycastle_tdes::TDES2_CTR; +/// 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::hazmat::ElectronicCodeBook; -/// use bouncycastle_core::traits::StreamCipherDecryptor; +/// 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 key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// 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]; -/// TDES2Key::new(&key).unwrap().encrypt_block(&mut keystream); +/// 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]); /// -/// TDES2_CTR::decrypt_in_place(&key, &nonce, &mut data).expect("decryption"); +/// 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 = Ctr; +pub type TDES2_CTR<'k> = Ctr<'k, TDES2Internal, Decrypting, BLOCK_LEN, CTR_NONCE_LEN>; diff --git a/crypto/tdes/src/hazmat/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs index 970214da0..4fe3b660a 100644 --- a/crypto/tdes/src/hazmat/ecb.rs +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -1,48 +1,29 @@ -//! Type alias for TDES in ECB mode (NIST SP 800-38A Sec 6.1; "TECB" in SP 800-38A Appendix E), +//! Type aliases for TDES in ECB mode (NIST SP 800-38A Sec 6.1; "TECB" in SP 800-38A Appendix E), //! with padding. //! -//! `bouncycastle_cipher::modes` is deliberately cipher-agnostic, so `Ecb` takes the permutation, the -//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters, and `bouncycastle_cipher::padding`'s -//! adapters take five more. This alias pins all of them except the two choices a caller actually -//! makes: the direction and the padding scheme. -//! -//! ```text -//! TDES_ECB // TDES, ECB, PKCS#7 padded, encrypting -//! TDES_ECB -//! ``` +//! 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. This alias exists for interoperability with +//! 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". //! -//! # Why the padding is part of the alias -//! //! 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. Naming it in the type -//! makes the choice explicit and makes a mismatched pair a compile error. [`PKCS7`] is the usual -//! one (this is Java's `DESede/ECB/PKCS5Padding`); [`NoPadding`] adds nothing and instead rejects a -//! message that is not a whole number of blocks. -//! -//! # This is the arbitrary-length API -//! -//! A padded alias implements [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], not the -//! block traits. The block-aligned API, with compile-time length checks and in-place data methods, -//! is `bouncycastle_cipher::modes::Ecb` itself, which this wraps. 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. -//! -//! # How one alias covers both directions -//! -//! See [`Direction::Select`](bouncycastle_cipher::Direction), which is the projection that lets `Dir` select between the encryptor and the -//! decryptor adapter. `Dir` must be [`Encrypting`] or [`Decrypting`]. +//! 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::hazmat::{TDES, TDES2Key}; -use crate::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY}; +use crate::BLOCK_LEN; +use crate::hazmat::{TDES2Internal, TDESInternal}; use bouncycastle_cipher::Direction; -use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::modes::hazmat::ecb::Ecb; use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; @@ -50,10 +31,13 @@ use bouncycastle_cipher::{Decrypting, Encrypting}; #[allow(unused_imports)] use bouncycastle_cipher::padding::{NoPadding, PKCS7}; #[allow(unused_imports)] -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +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. +/// 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 @@ -63,26 +47,26 @@ use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncrypt /// does not make it safe. /// /// ``` -/// use bouncycastle_tdes::hazmat::TDES_ECB; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -/// use bouncycastle_cipher::{Decrypting, Encrypting}; +/// 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, SymmetricEncryptionKey}; +/// use bouncycastle_rng::HashDRBG_SHA512; /// -/// type Enc = TDES_ECB; -/// type Dec = TDES_ECB; -/// -/// // Three distinct component keys; see `TDES` for what a key bundle must satisfy. +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// 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) = Enc::encrypt(&key, b"hello").expect("encryption"); +/// 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 = Dec::decrypt(&key, &no_iv, &ciphertext).expect("decryption"); +/// let recovered = key.decrypt(&no_iv, &ciphertext).expect("decryption"); /// assert_eq!(recovered, b"hello"); /// ``` /// @@ -90,68 +74,55 @@ use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncrypt /// plaintext blocks still give two identical ciphertext blocks. /// /// ``` -/// use bouncycastle_tdes::hazmat::TDES_ECB; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::Encrypting; +/// 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// 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) = -/// TDES_ECB::::encrypt(&key, &[0x5Au8; 16]).expect("encryption"); +/// 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 = ::Select< - PaddedBlockCipherEncryptor< - Ecb, - Pad, - KEY_LEN, - 0, - BLOCK_LEN, - >, - PaddedBlockCipherDecryptor< - Ecb, - Pad, - KEY_LEN, - 0, - BLOCK_LEN, - >, +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**. +/// 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 -/// [`TDES2Key`]. There is no IV, so decryption takes an empty array. The ECB warning in the module -/// docs applies unchanged. +/// 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::hazmat::TDES2_ECB; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherDecryptor; +/// 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 key = KeyMaterial::<16>::from_bytes_as_type( +/// 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 = TDES2_ECB::::decrypt(&key, &[], &ciphertext).expect("decryption"); +/// 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 = PaddedBlockCipherDecryptor< - Ecb, - Pad, - KEY_LEN_2KEY, - 0, - BLOCK_LEN, ->; +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 index 0c4bf10d1..f452f8488 100644 --- a/crypto/tdes/src/hazmat/mod.rs +++ b/crypto/tdes/src/hazmat/mod.rs @@ -1,16 +1,17 @@ //! The raw TDEA permutations and the ECB aliases; see [`bouncycastle_core::hazmat`] for what the //! path means and the supported uses. //! -//! [`TDES`] and the decryption-only [`TDES2Key`] transform exactly one block and are the primitive -//! under the modes in this crate, 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 [`TDES_CBC`](crate::TDES_CBC) and the -//! other mode aliases at the crate root. +//! [`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::TDES; -pub use tdes2::TDES2Key; +pub use tdes::TDESInternal; +pub use tdes2::TDES2Internal; diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs index f6e39aa35..d57ad71d4 100644 --- a/crypto/tdes/src/hazmat/tdes.rs +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -1,12 +1,13 @@ //! 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 -//! [`crate::tdes2`] and shares the checks defined here. +//! [`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::ElectronicCodeBook; +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; @@ -15,17 +16,19 @@ 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 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. See `TDES::new` for what is rejected and -/// why; for two-key TDEA, which is decryption-only, see [`crate::TDES2Key`]. See the crate docs -/// for the standing of TDEA today. -pub struct TDES { +/// 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]>, } @@ -87,66 +90,12 @@ pub(crate) fn any_weak(keys: &[u64]) -> Result<(), SymmetricCipherError> { Ok(()) } -impl TDES { - /// 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 type, - /// [`crate::TDES2Key`], 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. - pub(crate) 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 }) - } - - /// The TDEA forward cipher operation, `O = F_Key3(I_Key2(F_Key1(d)))` (Sec 3.1), in place. - /// - /// 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`]. - pub(crate) 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. - pub(crate) fn decrypt_block(&self, block: &mut Block) { - let [k1, k2, k3] = &*self.schedule; - inverse(block, k1, k2, k3); - } -} - /// `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); @@ -184,47 +133,104 @@ fn join(block: &mut Block, (l, r): (u32, u32)) { block[4..].copy_from_slice(&r.to_be_bytes()); } -impl Algorithm for TDES { +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 TDES { +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 { - TDES::new(key) + 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) { - TDES::encrypt_block(self, 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) { - TDES::decrypt_block(self, 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() { - TDES::encrypt_block(self, block) + self.encrypt_block(block) } } fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { for block in blocks.iter_mut() { - TDES::decrypt_block(self, block) + self.decrypt_block(block) } } fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { for block in blocks.iter_mut() { - TDES::encrypt_block(self, block) + self.encrypt_block(block) } } fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { for block in blocks.iter_mut() { - TDES::decrypt_block(self, block) + self.decrypt_block(block) } } } -impl core::fmt::Debug for TDES { +/// 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) @@ -239,15 +245,15 @@ mod tests { fn test_engine_size_is_exactly_the_three_schedules() { // The "Memory Usage" table in the crate docs quotes this. No direction flag, no counters: // three DEA keys of sixteen two-word round keys. - assert_eq!(size_of::(), 3 * 16 * 2 * 4); - assert_eq!(size_of::(), 384); + assert_eq!(size_of::(), 3 * 16 * 2 * 4); + assert_eq!(size_of::(), 384); } #[test] fn test_alg_name_and_strength() { - assert_eq!(::ALG_NAME, "TDES"); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); - assert!(>::ENCRYPTION_APPROVED); + assert_eq!(::ALG_NAME, "TDES"); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert!(>::ENCRYPTION_APPROVED); } #[test] diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index ad60ce746..d907875ad 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -7,58 +7,62 @@ 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::ElectronicCodeBook; +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`. +/// 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 `Encrypting` mode in -/// `bouncycastle_cipher::modes` refuses to be built over it **at compile time**: +/// [`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::hazmat::TDES2Key; +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::CBC; +/// use bouncycastle_cipher::padding::PKCS7; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::BlockCipherEncryptor; -/// use bouncycastle_cipher::Encrypting; -/// use bouncycastle_cipher::modes::Cbc; +/// 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 key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// 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 _ = Cbc::::do_encrypt_init(&key); +/// let _ = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()); /// ``` /// -/// CTR's encrypting constructor is the generic `StreamCipher` adapter in `bouncycastle-core`, which -/// checks the keystream's `KeyStream::ENCRYPTION_APPROVED`; CTR's keystream takes this type's value, -/// so the refusal holds there too: +/// 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::hazmat::TDES2Key; +/// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES2Key}; +/// use bouncycastle_cipher::modes::CTR; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::SymmetricCipherEncryptor; -/// use bouncycastle_cipher::Encrypting; -/// use bouncycastle_cipher::modes::Ctr; +/// 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 key = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap(); +/// 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 _ = Ctr::::do_encrypt_init(&key); +/// let _ = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()); /// ``` /// -/// The `Decrypting` direction of every mode works, and this crate provides the aliases +/// `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) for it. The raw -/// [`ElectronicCodeBook::encrypt_block`] is still present, because CFB and CTR decryption are +/// [`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). /// @@ -70,14 +74,22 @@ use bouncycastle_utils::secret::Secret; /// no honest higher value to demand. /// /// The persistent state is the two expanded DEA keys, 256 bytes, in a [`Secret`]. -pub struct TDES2Key { +#[derive(Clone)] +pub struct TDES2Internal { schedule: Secret<[Subkeys; 2]>, } -impl TDES2Key { +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 [`crate::TDES`]. + /// Parity bits are ignored, as for [`TDESInternal`](super::TDESInternal). /// /// # Errors /// * [`KeyMaterialError::InvalidKeyType`] if the key is not `KeyType::SymmetricCipherKey`. @@ -87,7 +99,7 @@ impl TDES2Key { /// 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. - pub(crate) fn new(key: &KeyMaterial) -> Result { + 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)?; @@ -106,65 +118,62 @@ impl TDES2Key { 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. - pub(crate) fn encrypt_block(&self, block: &mut Block) { + 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)))`. - pub(crate) fn decrypt_block(&self, block: &mut Block) { + fn decrypt_block(&self, block: &mut Block) { let [k1, k2] = &*self.schedule; inverse(block, k1, k2, k1); } -} - -impl Algorithm for TDES2Key { - 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 TDES2Key { - fn new(key: &KeyMaterial) -> Result { - TDES2Key::new(key) - } - /// SP 800-131A Rev 2 Table 1: two-key TDEA encryption is disallowed. - const ENCRYPTION_APPROVED: bool = false; - fn encrypt_block(&self, block: &mut Block) { - TDES2Key::encrypt_block(self, block) - } - fn decrypt_block(&self, block: &mut Block) { - TDES2Key::decrypt_block(self, block) - } - // As for `TDES`: no unit larger than one block, so the batch methods are single-block loops. + // 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() { - TDES2Key::encrypt_block(self, block) + self.encrypt_block(block) } } fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { for block in blocks.iter_mut() { - TDES2Key::decrypt_block(self, block) + self.decrypt_block(block) } } fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { for block in blocks.iter_mut() { - TDES2Key::encrypt_block(self, block) + self.encrypt_block(block) } } fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { for block in blocks.iter_mut() { - TDES2Key::decrypt_block(self, block) + self.decrypt_block(block) } } } -impl core::fmt::Debug for TDES2Key { +/// 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) @@ -177,14 +186,16 @@ mod tests { #[test] fn test_engine_size_is_exactly_the_two_schedules() { - assert_eq!(size_of::(), 2 * 16 * 2 * 4); - assert_eq!(size_of::(), 256); + assert_eq!(size_of::(), 2 * 16 * 2 * 4); + assert_eq!(size_of::(), 256); } #[test] fn test_alg_name_strength_and_gate() { - assert_eq!(::ALG_NAME, "TDES-2KEY"); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); - assert!(!>::ENCRYPTION_APPROVED); + assert_eq!(::ALG_NAME, "TDES-2KEY"); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert!( + !>::ENCRYPTION_APPROVED + ); } } diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs new file mode 100644 index 000000000..24a1b04f0 --- /dev/null +++ b/crypto/tdes/src/key.rs @@ -0,0 +1,234 @@ +//! The TDEA key objects: a key bundle checked and expanded once, under a mode, handing out that +//! mode's workers through the role traits. +//! +//! [`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`]. +//! +//! [`ElectronicCodeBook::ENCRYPTION_APPROVED`]: bouncycastle_core::hazmat::ElectronicCodeBook::ENCRYPTION_APPROVED + +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. +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() } + } +} + +/// Every TDEA mode is a cipher mode, so the material must be a `SymmetricCipherKey`; the engine's +/// constructor checks that too, but this is the error the role contract names. +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. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`]. + /// * [`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. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`]. + /// * [`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 index 452e301b2..c8ff28118 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -1,124 +1,130 @@ -//! A constant-time, table-free Triple DES block cipher engine (NIST SP 800-67 Rev 2, TDEA). -//! -//! This crate provides the raw three-key TDEA keyed permutation, [`TDES`], and a decryption-only -//! two-key one, [`TDES2Key`], implemented as a Boolean circuit over bit-planes rather than by -//! lookups in S-box tables. That makes them constant-time; see [Design](#design). They are +//! 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 //! -//! ## Encrypting and decrypting a single block -//! -//! ``` -//! use bouncycastle_tdes::hazmat::TDES; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; -//! -//! // 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 = TDES::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]); -//! ``` -//! //! ## Modes of operation //! -//! To encrypt more than one block, use a mode of operation from `bouncycastle_cipher::modes`. This crate -//! provides aliases that fill in the const parameters, leaving only the choices a caller actually -//! makes: [`TDES_CBC`] for CBC (SP 800-38A Sec 6.2), which takes the direction **and a padding -//! scheme**, and [`TDES_CFB`] for CFB64 (Sec 6.3), which takes only the direction. [`TDES_CFB8`] -//! gives CFB8, the `s = 8` segment size, a different and non-interoperable mode costing one TDES -//! call per byte. [`TDES_CTR`] gives CTR (Sec 6.5) with a 6-byte nonce and a 2-byte counter. -//! [`TDES_ECB`] gives ECB (Sec 6.1), which takes a padding scheme like CBC and has no IV, for -//! interoperability and test vectors only -- see -//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher). +//! 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 alias 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. +//! 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::TDES_CBC; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! 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, SymmetricEncryptionKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let mut rng = HashDRBG_SHA512::new_from_os(); // one RNG, held across messages //! -//! // Three distinct component keys; see `TDES::new` for what a key bundle must satisfy. +//! // 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 key = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +//! 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 generated for you and returned; there is no API for supplying one. -//! let (iv, ciphertext) = -//! TDES_CBC::::encrypt(&key, &plaintext).expect("encryption"); +//! // 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 = -//! TDES_CBC::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! let recovered = key.decrypt(&iv, &ciphertext).expect("decryption"); //! assert_eq!(recovered, plaintext); //! ``` //! -//! For the block-aligned API -- whole blocks in place, with the length checked at compile time -- -//! name `bouncycastle_cipher::modes::Cbc` directly; that is what these aliases wrap. +//! 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. //! -//! There is no one-shot static on the permutation, because `TDES::new(&key)?.encrypt_block(..)` -//! already *is* the one shot. Data-level one-shots belong to the modes of operation, which take -//! arbitrary-length input and generate their own initialisation data. +//! ## 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 //! -//! `TDES::new` 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, +//! 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. +//! 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. -//! It sets `ElectronicCodeBook::ENCRYPTION_APPROVED` to `false`, so the `Encrypting` direction of -//! every mode refuses to compile over it, while the `Decrypting` direction works. The aliases +//! 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::TDES2_CBC; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::SymmetricCipherDecryptor; +//! 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 key = KeyMaterial::<16>::from_bytes_as_type( +//! 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 = TDES2_CBC::::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! let plaintext = key.decrypt(&iv, &ciphertext).expect("decryption"); //! assert_eq!(plaintext, [0x06, 0x11, 0xa4, 0x74, 0xfb, 0x09, 0x09, 0x78]); //! ``` //! @@ -152,24 +158,32 @@ //! //! 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 [`TDES`] 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. +//! 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 three DEA -//! key schedules: +//! 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 | //! |---|---|---|---|---| -//! | [`TDES`] | 24 B | 3 x 16 | 384 B | 0 B | -//! | [`TDES2Key`] | 16 B | 3 x 16 | 256 B | 0 B | +//! | [`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. +//! compiler keeps in registers. A worker adds the mode's own state (an IV or counter block) and a +//! reference to the key. //! //! For comparison, the table-driven `DESEngine` carries 2 KiB of combined S/P tables. //! @@ -185,7 +199,7 @@ //! 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 [`TDES`] reports as its maximum strength. +//! 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`. @@ -203,7 +217,7 @@ //! //! ## A block permutation is not a cipher //! -//! [`TDES`] transforms exactly 8 bytes. Using it directly on data means ECB, which is not +//! [`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. @@ -249,7 +263,7 @@ //! * 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 type -- the crate docs say so and cite the SP 800-67r2 and SP 800-131A Rev 2 +//! 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. @@ -264,6 +278,7 @@ mod cfb8; mod ctr; mod des; pub mod hazmat; +mod key; mod sbox; mod schedule; @@ -282,8 +297,9 @@ 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, TDES_ECB, TDES2_ECB, TDES2Key}; +use hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; // end of imports needed for docs diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs new file mode 100644 index 000000000..169643edd --- /dev/null +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -0,0 +1,177 @@ +//! 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::Mode; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{ + FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_tdes::hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{ + BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, 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); +} + +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 index c2a62cf85..504173f4c 100644 --- a/crypto/tdes/tests/cavp_mmt_tests.rs +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -1,6 +1,6 @@ //! 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 -//! [`TDES`]) and `TECBMMT2`, `TCBCMMT2`, `TCFB64MMT2`, `TCFB8MMT2` (two-key, for [`TDES2Key`]). +//! [`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` @@ -19,33 +19,34 @@ //! 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 `bouncycastle_cipher::modes` has no API for a caller-supplied -//! IV. Three-key encryption therefore uses `do_encrypt_init_rng` with 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 `TDES_*` aliases are used, so the aliases' -//! parameter choices (a 24-byte key, an 8-byte block, an 8-byte IV) are what is under test. +//! 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 -- the encrypting modes do not compile over [`TDES2Key`] -- so -//! its CBC and CFB vectors are checked by decrypting `CIPHERTEXT` through the `TDES2_*` aliases -//! (both sections), and the forward direction is checked on the ECB vectors through the raw -//! permutation, which CFB decryption is built on anyway. +//! 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::Cbc; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8}; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{ + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, + SymmetricEncryptionKey, +}; use bouncycastle_core::traits::{ BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; -use bouncycastle_tdes::hazmat::{TDES, TDES2Key}; -use bouncycastle_tdes::{ - BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES_CFB, TDES_CFB8, TDES2_CFB, TDES2_CFB8, -}; +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}; @@ -138,7 +139,7 @@ fn load(file: &str) -> Option> { Some(cases) } -fn key3(case: &MmtCase) -> KeyMaterial { +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) @@ -146,13 +147,21 @@ fn key3(case: &MmtCase) -> KeyMaterial { } /// The two-key files carry `KEY3 = KEY1`; the engine takes `KEY1 || KEY2`. -fn key2(case: &MmtCase) -> KeyMaterial { +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") } @@ -162,11 +171,7 @@ fn label(file: &str, case: &MmtCase) -> String { } /// ECB through a raw permutation: single blocks, then the four-block path. -fn check_ecb>( - file: &str, - case: &MmtCase, - perm: &P, -) { +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"); @@ -211,7 +216,7 @@ fn check_ecb>( fn tecbmmt3() { let Some(cases) = load("TECBMMT3.rsp") else { return }; for case in &cases { - let tdes = TDES::new(&key3(case)).expect("a valid CAVP key bundle"); + let tdes = TDESInternal::new(&material3(case)).expect("a valid CAVP key bundle"); check_ecb("TECBMMT3", case, &tdes); } } @@ -220,129 +225,126 @@ fn tecbmmt3() { fn tecbmmt2() { let Some(cases) = load("TECBMMT2.rsp") else { return }; for case in &cases { - let tdes = TDES2Key::new(&key2(case)).expect("a valid CAVP two-key bundle"); + let tdes = TDES2Internal::new(&material2(case)).expect("a valid CAVP two-key bundle"); check_ecb("TECBMMT2", case, &tdes); } } #[test] fn tcbcmmt3() { - type Enc = Cbc; - type Dec = Cbc; let Some(cases) = load("TCBCMMT3.rsp") else { return }; for case in &cases { - let key = key3(case); + let key = key3::>(case); let iv = iv(case); let name = label("TCBCMMT3", case); - let (mut enc, got_iv) = Enc::do_encrypt_init_rng(&key, &mut FixedSeedRNG::new(iv)).unwrap(); + 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(); - for block in buf.as_chunks_mut::().0 { - enc.do_encrypt(block).unwrap(); - } + enc.do_encrypt_blocks_inplace(buf.as_chunks_mut::().0).unwrap(); assert_eq!(buf, case.ciphertext, "{name}: encrypt"); - let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); - for block in buf.as_chunks_mut::().0 { - dec.do_decrypt(block).unwrap(); - } + 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() { - type Dec = Cbc; let Some(cases) = load("TCBCMMT2.rsp") else { return }; for case in &cases { let name = label("TCBCMMT2", case); - let mut dec = Dec::do_decrypt_init(&key2(case), &iv(case)).unwrap(); + let key = key2::>(case); + let mut dec = key.create_decryptor(&iv(case)); let mut buf = case.ciphertext.clone(); - for block in buf.as_chunks_mut::().0 { - dec.do_decrypt(block).unwrap(); - } + dec.do_decrypt_blocks_inplace(buf.as_chunks_mut::().0).unwrap(); assert_eq!(buf, case.plaintext, "{name}: decrypt"); } } -/// CFB64 and CFB8 share a shape: stream cipher, 8-byte IV, any length. Three-key: both directions. -fn check_stream3(file: &str) +/// 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 - E: StreamCipherEncryptor, - D: StreamCipherDecryptor, + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { - let Some(cases) = load(file) else { return }; - for case in &cases { - let key = key3(case); - let iv = iv(case); - let name = label(file, case); + let iv = iv(case); - let (mut enc, got_iv) = E::do_encrypt_init_rng(&key, &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(&mut buf).unwrap(); - assert_eq!(buf, case.ciphertext, "{name}: encrypt"); + 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 = D::do_decrypt_init(&key, &iv).unwrap(); - dec.do_decrypt(&mut buf).unwrap(); - assert_eq!(buf, case.plaintext, "{name}: decrypt"); + 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, _) = E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::new(iv)).unwrap(); - let mut buf = case.plaintext.clone(); - for byte in buf.iter_mut() { - enc.do_encrypt(core::slice::from_mut(byte)).unwrap(); - } - assert_eq!(buf, case.ciphertext, "{name}: encrypt byte by byte"); + // 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"); } -/// Two-key: decryption only, whole and byte by byte. -fn check_stream2(file: &str) +/// 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 - D: StreamCipherDecryptor, + K: SymmetricDecryptionKey, + K::Decryptor<'k>: StreamCipherDecryptor, { - let Some(cases) = load(file) else { return }; - for case in &cases { - let key = key2(case); - let iv = iv(case); - let name = label(file, case); + let iv = iv(case); - let mut dec = D::do_decrypt_init(&key, &iv).unwrap(); - let mut buf = case.ciphertext.clone(); - dec.do_decrypt(&mut buf).unwrap(); - assert_eq!(buf, case.plaintext, "{name}: decrypt"); + 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 = D::do_decrypt_init(&key, &iv).unwrap(); - let mut buf = case.ciphertext.clone(); - for byte in buf.iter_mut() { - dec.do_decrypt(core::slice::from_mut(byte)).unwrap(); - } - assert_eq!(buf, case.plaintext, "{name}: decrypt byte by byte"); + 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() { - check_stream3::, TDES_CFB>("TCFB64MMT3.rsp"); + let Some(cases) = load("TCFB64MMT3.rsp") else { return }; + for case in &cases { + check_stream3_case(&label("TCFB64MMT3", case), &key3::(case), case); + } } #[test] fn tcfb8mmt3() { - check_stream3::, TDES_CFB8>("TCFB8MMT3.rsp"); + 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() { - check_stream2::("TCFB64MMT2.rsp"); + 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() { - check_stream2::("TCFB8MMT2.rsp"); + let Some(cases) = load("TCFB8MMT2.rsp") else { return }; + for case in &cases { + check_stream2_case(&label("TCFB8MMT2", case), &key2::(case), case); + } } #[test] diff --git a/crypto/tdes/tests/cbc_alias_tests.rs b/crypto/tdes/tests/cbc_alias_tests.rs deleted file mode 100644 index f07667149..000000000 --- a/crypto/tdes/tests/cbc_alias_tests.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Tests for the padded TDES-CBC alias. -//! -//! The alias is only a type alias, so what is worth testing is that it names the *right* types and -//! 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 their own crates; this checks the wiring between them at the 8-byte block. - -use bouncycastle_cipher::modes::Cbc; -use bouncycastle_cipher::padding::{ - NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, -}; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle_tdes::hazmat::TDES; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, TDES_CBC}; - -fn key() -> KeyMaterial { - 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") -} - -/// The alias must resolve to exactly the adapters it claims to, at both directions. -#[test] -fn the_alias_names_the_expected_types() { - use core::mem::size_of; - - assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherEncryptor< - Cbc, - PKCS7, - KEY_LEN, - BLOCK_LEN, - BLOCK_LEN, - >, - >() - ); - assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherDecryptor< - Cbc, - PKCS7, - KEY_LEN, - 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::>()); -} - -/// Round trips at lengths that need padding and lengths that do not, and the IV is one 8-byte block. -#[test] -fn round_trips_at_every_alignment() { - 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) = - TDES_CBC::::encrypt(&key(), &plaintext).expect("encryption"); - assert_eq!(iv.len(), BLOCK_LEN); - assert_eq!( - ciphertext.len(), - (len / 8 + 1) * 8, - "len {len}: PKCS7 pads up to the next whole block" - ); - - let recovered = - TDES_CBC::::decrypt(&key(), &iv, &ciphertext).expect("decryption"); - assert_eq!(recovered, plaintext, "len {len}: round trip"); - } -} - -/// The padding parameter must actually select the scheme, not merely be carried around. -#[test] -fn the_padding_parameter_selects_the_scheme() { - type Pkcs7Enc = TDES_CBC; - type NoPadEnc = TDES_CBC; - - let aligned = [0x5Au8; 8]; - let (_, pkcs7) = Pkcs7Enc::encrypt(&key(), &aligned).expect("PKCS7 accepts aligned data"); - let (_, nopad) = NoPadEnc::encrypt(&key(), &aligned).expect("NoPadding accepts it too"); - assert_eq!(pkcs7.len(), 16, "PKCS7 adds a whole block of padding to aligned data"); - assert_eq!(nopad.len(), 8, "NoPadding adds nothing"); - - let unaligned = b"hello"; - assert!(Pkcs7Enc::encrypt(&key(), unaligned).is_ok(), "PKCS7 pads a partial block"); - assert!(NoPadEnc::encrypt(&key(), unaligned).is_err(), "NoPadding must refuse a partial block"); -} - -/// A ciphertext made under one scheme must not decrypt cleanly under the other. -#[test] -fn the_two_schemes_are_not_interchangeable() { - let aligned = [0x5Au8; 8]; - let (iv, pkcs7) = TDES_CBC::::encrypt(&key(), &aligned).expect("encryption"); - - let as_nopad = TDES_CBC::::decrypt(&key(), &iv, &pkcs7) - .expect("NoPadding cannot tell that the trailing block is padding"); - assert_ne!(as_nopad, aligned); - assert_eq!(as_nopad.len(), 16, "it keeps the padding block as data"); - - let correct = TDES_CBC::::decrypt(&key(), &iv, &pkcs7).expect("decryption"); - assert_eq!(correct, aligned); -} - -/// The IV is generated per encryption, so the same plaintext gives different ciphertext. -#[test] -fn each_encryption_gets_a_fresh_iv() { - let plaintext = [0x77u8; 16]; - let mut seen = std::collections::BTreeSet::new(); - for _ in 0..16 { - let (iv, ct) = TDES_CBC::::encrypt(&key(), &plaintext).unwrap(); - assert!(seen.insert(iv), "IV repeated across encryptions"); - let back = TDES_CBC::::decrypt(&key(), &iv, &ct).unwrap(); - assert_eq!(back, plaintext); - } -} diff --git a/crypto/tdes/tests/ecb_alias_tests.rs b/crypto/tdes/tests/ecb_alias_tests.rs deleted file mode 100644 index 158e7cfec..000000000 --- a/crypto/tdes/tests/ecb_alias_tests.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Tests for the padded TDES-ECB alias. -//! -//! As with the CBC alias, this is only a type alias, so what is worth testing is that both -//! parameters select: the direction picks the encryptor or the decryptor, and the padding scheme -//! reaches the behaviour. ECB's own properties are tested in `bouncycastle_cipher::modes`; what is specific -//! here is the 8-byte block, and that `INIT_DATA_LEN` is 0 so the projection carries a different -//! value than CBC's. - -use bouncycastle_cipher::modes::hazmat::Ecb; -use bouncycastle_cipher::padding::{ - NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, -}; -use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle_tdes::hazmat::{TDES, TDES_ECB}; -use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN}; - -fn key() -> KeyMaterial { - 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") -} - -/// The alias must resolve to exactly the adapters it claims to, with `INIT_DATA_LEN = 0`. -#[test] -fn the_alias_names_the_expected_types() { - use core::mem::size_of; - - assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherEncryptor< - Ecb, - PKCS7, - KEY_LEN, - 0, - BLOCK_LEN, - >, - >() - ); - assert_eq!( - size_of::>(), - size_of::< - PaddedBlockCipherDecryptor< - Ecb, - PKCS7, - KEY_LEN, - 0, - BLOCK_LEN, - >, - >() - ); -} - -/// ECB has no IV, so the init data is an empty array and the ciphertext is exactly the padded -/// plaintext with nothing prepended. -#[test] -fn there_is_no_iv() { - let (no_iv, ciphertext) = - TDES_ECB::::encrypt(&key(), 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 = TDES_ECB::::decrypt(&key(), &no_iv, &ciphertext).unwrap(); - assert_eq!(recovered, b"hello"); -} - -/// Round trips at lengths that need padding and lengths that do not, around the 8-byte block. -#[test] -fn round_trips_at_every_alignment() { - for len in [0usize, 1, 7, 8, 9, 15, 16, 17, 64] { - let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); - let (no_iv, ciphertext) = - TDES_ECB::::encrypt(&key(), &plaintext).expect("encryption"); - assert_eq!(no_iv, [0u8; 0]); - assert_eq!( - ciphertext.len(), - (len / 8 + 1) * 8, - "len {len}: PKCS7 pads up to the next whole block" - ); - - let recovered = TDES_ECB::::decrypt(&key(), &no_iv, &ciphertext) - .expect("decryption"); - assert_eq!(recovered, plaintext, "len {len}: round trip"); - } -} - -/// The padding parameter must select the scheme here too. -#[test] -fn the_padding_parameter_selects_the_scheme() { - let aligned = [0x5Au8; 8]; - let (_, pkcs7) = TDES_ECB::::encrypt(&key(), &aligned).expect("PKCS7"); - let (_, nopad) = - TDES_ECB::::encrypt(&key(), &aligned).expect("NoPadding"); - assert_eq!(pkcs7.len(), 16, "PKCS7 adds a whole block to aligned data"); - assert_eq!(nopad.len(), 8, "NoPadding adds nothing"); - - assert!( - TDES_ECB::::encrypt(&key(), b"hello").is_err(), - "NoPadding must refuse a partial block" - ); -} - -/// Padding does not fix ECB: identical plaintext blocks still give identical ciphertext blocks, and -/// the same message under the same key always gives the same ciphertext. -#[test] -fn padding_does_not_hide_the_codebook_property() { - let (_, ciphertext) = - TDES_ECB::::encrypt(&key(), &[0x5Au8; 16]).unwrap(); - assert_eq!(ciphertext[..8], ciphertext[8..], "ECB is a codebook, padded or not"); - - let (_, a) = TDES_ECB::::encrypt(&key(), b"hello").unwrap(); - let (_, b) = TDES_ECB::::encrypt(&key(), b"hello").unwrap(); - assert_eq!(a, b, "the same message encrypts the same way every time"); -} diff --git a/crypto/tdes/tests/electronic_code_book_tests.rs b/crypto/tdes/tests/electronic_code_book_tests.rs index 3decce11b..2972acf91 100644 --- a/crypto/tdes/tests/electronic_code_book_tests.rs +++ b/crypto/tdes/tests/electronic_code_book_tests.rs @@ -1,4 +1,5 @@ -//! `ElectronicCodeBook` trait conformance, via the shared test framework. +//! `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 @@ -13,15 +14,15 @@ //! 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::{TDES, TDES2Key}; +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::(); + TestFrameworkElectronicCodeBook::new().test::(); } #[test] fn tdes2key_conforms_to_electronic_code_book() { - TestFrameworkElectronicCodeBook::new().test::(); + TestFrameworkElectronicCodeBook::new().test::(); } diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index 59d1b20bf..6f608661a 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -1,4 +1,5 @@ -//! Key-bundle handling: what `TDES::new` accepts and rejects, and why. +//! 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 @@ -9,12 +10,12 @@ //! * The `KeyMaterial` checks: type, length and security strength. use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; -use bouncycastle_core::hazmat::ElectronicCodeBook; 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::{TDES, TDES2Key}; +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)". @@ -67,13 +68,13 @@ fn bundle(k: [u64; 3]) -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() } -fn is_weak_key_error(result: Result) -> bool { +fn is_weak_key_error(result: Result) -> bool { matches!(result, Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::WeakKey(_)))) } #[test] fn a_good_bundle_is_accepted() { - assert!(TDES::new(&bundle(GOOD)).is_ok()); + assert!(TDESInternal::new(&bundle(GOOD)).is_ok()); } #[test] @@ -86,14 +87,14 @@ fn every_listed_weak_key_is_rejected_in_every_position() { let mut k = GOOD; k[position] = weak; assert!( - is_weak_key_error(TDES::new(&bundle(k))), + 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(TDES::new(&bundle(k))), + is_weak_key_error(TDESInternal::new(&bundle(k))), "weak key {weak:016x} with even parity" ); } @@ -150,7 +151,10 @@ fn a_key_one_bit_away_from_weak_is_accepted() { let nearly = weak ^ (1u64 << 62); // key bit 2, not a parity bit let mut k = GOOD; k[1] = nearly; - assert!(TDES::new(&bundle(k)).is_ok(), "{nearly:016x} should not be treated as weak"); + assert!( + TDESInternal::new(&bundle(k)).is_ok(), + "{nearly:016x} should not be treated as weak" + ); } } @@ -158,21 +162,21 @@ fn a_key_one_bit_away_from_weak_is_accepted() { 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(TDES::new(&bundle([a, a, c]))), "Key1 = Key2"); - assert!(is_weak_key_error(TDES::new(&bundle([a, b, b]))), "Key2 = Key3"); - assert!(is_weak_key_error(TDES::new(&bundle([a, b, a]))), "Key1 = Key3 (two-key TDEA)"); - assert!(is_weak_key_error(TDES::new(&bundle([a, a, a]))), "three identical keys"); + 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(TDES::new(&bundle([a, a ^ !PARITY_MASK, c]))), + 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 = TDES::new(&bundle(GOOD)).unwrap(); - let flipped = TDES::new(&bundle([ + 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, @@ -192,7 +196,7 @@ fn parity_bits_do_not_change_the_cipher() { #[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 = TDES::new(&bundle(GOOD)).unwrap(); + let good = TDESInternal::new(&bundle(GOOD)).unwrap(); let block = [0x5Au8; BLOCK_LEN]; let mut reference = block; good.encrypt_block(&mut reference); @@ -204,7 +208,7 @@ fn a_non_parity_bit_does_change_the_cipher() { let mut k = GOOD; k[position] ^= 1u64 << bit; let mut out = block; - TDES::new(&bundle(k)).unwrap().encrypt_block(&mut out); + TDESInternal::new(&bundle(k)).unwrap().encrypt_block(&mut out); assert_ne!(out, reference, "key {} bit {bit} had no effect", position + 1); } } @@ -220,7 +224,7 @@ fn the_key_material_checks() { // Wrong type. let mac_key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::MACKey).unwrap(); assert!(matches!( - TDES::new(&mac_key), + TDESInternal::new(&mac_key), Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) )); @@ -228,28 +232,28 @@ fn the_key_material_checks() { let mut key = bundle(GOOD); do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::None)).unwrap(); assert!(matches!( - TDES::new(&key), + TDESInternal::new(&key), Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) )); do_hazardous_operations(&mut key, |k| k.set_security_strength(SecurityStrength::_112bit)) .unwrap(); - assert!(TDES::new(&key).is_ok()); + 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!( - TDES::new(&zero), + TDESInternal::new(&zero), Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) )); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); } #[test] fn debug_output_names_the_algorithm_only() { - let tdes = TDES::new(&bundle(GOOD)).unwrap(); + let tdes = TDESInternal::new(&bundle(GOOD)).unwrap(); assert_eq!(format!("{tdes:?}"), "TDES"); } @@ -263,20 +267,20 @@ fn bundle2(k: [u64; 2]) -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() } -fn is_weak_key_error2(result: Result) -> bool { +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 (TDES rejects + // 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 = TDES2Key::new(&bundle2([a, b])).unwrap(); - let three = TDES::new(&bundle([a, b, c])).unwrap(); + 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); @@ -291,16 +295,16 @@ fn two_key_bundle_is_the_three_key_bundle_with_key3_equal_to_key1() { #[test] fn two_key_repeated_or_weak_components_are_rejected() { let [a, b, _] = GOOD; - assert!(is_weak_key_error2(TDES2Key::new(&bundle2([a, a]))), "Key1 = Key2 is single DES"); + assert!(is_weak_key_error2(TDES2Internal::new(&bundle2([a, a]))), "Key1 = Key2 is single DES"); assert!( - is_weak_key_error2(TDES2Key::new(&bundle2([a, a ^ !PARITY_MASK]))), + 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(TDES2Key::new(&bundle2([weak, b]))), "{weak:016x} as Key1"); - assert!(is_weak_key_error2(TDES2Key::new(&bundle2([a, weak]))), "{weak:016x} as Key2"); + 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!(TDES2Key::new(&bundle2([a, b])).is_ok()); + assert!(TDES2Internal::new(&bundle2([a, b])).is_ok()); } #[test] @@ -311,7 +315,7 @@ fn two_key_material_checks_and_metadata() { bytes[8..].copy_from_slice(&b.to_be_bytes()); let mac_key = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::MACKey).unwrap(); assert!(matches!( - TDES2Key::new(&mac_key), + TDES2Internal::new(&mac_key), Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) )); @@ -319,10 +323,10 @@ fn two_key_material_checks_and_metadata() { // 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!(TDES2Key::new(&key).is_ok()); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); - assert_eq!(::ALG_NAME, "TDES-2KEY"); - assert!(!>::ENCRYPTION_APPROVED); - assert!(>::ENCRYPTION_APPROVED); - assert_eq!(format!("{:?}", TDES2Key::new(&bundle2([a, b])).unwrap()), "TDES-2KEY"); + assert!(TDES2Internal::new(&key).is_ok()); + assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!(::ALG_NAME, "TDES-2KEY"); + assert!(!>::ENCRYPTION_APPROVED); + assert!(>::ENCRYPTION_APPROVED); + 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..a673941e1 --- /dev/null +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -0,0 +1,168 @@ +//! 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, CTR, ModeNames}; +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +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, +}; +use bouncycastle_core::traits::{BlockCipherDecryptor, StreamCipherDecryptor}; +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::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"); +} 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/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index 8dd9f0c1e..c7171d16b 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -23,7 +23,7 @@ //! //! # What to expect //! -//! Like AES, TDES has no interesting stack profile: peak usage is a small constant plus the key +//! Like AES, TDESInternal has no interesting stack profile: peak usage is a small constant plus the key //! schedule. The numbers worth recording in the crate docs are the persistent size of the engine //! (three packed DEA schedules, 384 bytes) and the confirmation that per-block work is a fixed, //! small amount of stack -- the six input planes and the 128-byte working array of the S-box @@ -35,9 +35,9 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::hazmat::{BlockPermutation, ElectronicCodeBook}; use bouncycastle::core::key_material::{KeyMaterial, KeyType}; -use bouncycastle::tdes::hazmat::{TDES, TDES2Key}; +use bouncycastle::tdes::hazmat::{TDES2Internal, TDESInternal}; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN}; /// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. @@ -52,9 +52,9 @@ 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::()); + println!("size_of: {}", size_of::()); // Two DEA keys: 2 * 16 * 2 * 4 = 256. - println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); } fn key() -> KeyMaterial { @@ -70,14 +70,14 @@ fn key() -> KeyMaterial { fn bench_tdes_key_expansion() { eprintln!("TDES::new (key expansion)"); - let tdes = TDES::new(&key()).unwrap(); + let tdes = TDESInternal::new(&key()).unwrap(); print!("{tdes:?}"); } fn bench_tdes_encrypt_block() { eprintln!("TDES::encrypt_block"); - let tdes = TDES::new(&key()).unwrap(); + let tdes = TDESInternal::new(&key()).unwrap(); let mut block = [0x11u8; BLOCK_LEN]; tdes.encrypt_block(&mut block); print!("{block:x?}"); @@ -86,7 +86,7 @@ fn bench_tdes_encrypt_block() { fn bench_tdes_decrypt_block() { eprintln!("TDES::decrypt_block"); - let tdes = TDES::new(&key()).unwrap(); + let tdes = TDESInternal::new(&key()).unwrap(); let mut block = [0x11u8; BLOCK_LEN]; tdes.decrypt_block(&mut block); print!("{block:x?}"); @@ -95,7 +95,7 @@ fn bench_tdes_decrypt_block() { fn bench_tdes_encrypt_4blocks() { eprintln!("TDES::encrypt_4blocks"); - let tdes = TDES::new(&key()).unwrap(); + let tdes = TDESInternal::new(&key()).unwrap(); let mut blocks = [[0x11u8; BLOCK_LEN]; 4]; tdes.encrypt_4blocks(&mut blocks); print!("{blocks:x?}"); From 7d1e7ccb0b94c58d9f56e0f9ae1cdb6e389d8282 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 25 Oct 2026 18:33:57 +1100 Subject: [PATCH 15/32] tdes: the two engines and the keys compare in constant time Lifted onto this branch from feature/key-rework-memory (5607ba98), where AESKey and AESInternal gained the same: the two engines compares the schedule through Secret's byte-wise equality, which inspects every byte so the time taken does not say where two differ, and a key is equal when its mode state is, bounded on the state rather than on Mode::KeyState. The key objects never hand the material back, so this is the only way two keys can be compared. mode_key_tests: equality follows the material for a clone, the same bytes and a single flipped bit, and a flipped parity bit, which PC-1 drops, leaves a bundle equal. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/src/hazmat/tdes.rs | 11 ++++++++++ crypto/tdes/src/hazmat/tdes2.rs | 10 ++++++++++ crypto/tdes/src/key.rs | 13 ++++++++++++ crypto/tdes/tests/mode_key_tests.rs | 31 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs index d57ad71d4..57a3e590b 100644 --- a/crypto/tdes/src/hazmat/tdes.rs +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -32,6 +32,17 @@ 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 diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index d907875ad..358a380cc 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -79,6 +79,16 @@ 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 diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index 24a1b04f0..9aaf8e795 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -69,6 +69,19 @@ impl> Clone for TDESKey { } } +// 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 {} + /// Every TDEA mode is a cipher mode, so the material must be a `SymmetricCipherKey`; the engine's /// constructor checks that too, but this is the error the role contract names. fn check_key_type( diff --git a/crypto/tdes/tests/mode_key_tests.rs b/crypto/tdes/tests/mode_key_tests.rs index a673941e1..66cec1e2c 100644 --- a/crypto/tdes/tests/mode_key_tests.rs +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -166,3 +166,34 @@ fn two_key_keys_decrypt() { 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() + ); +} From 3604a59d46ac7be7fa83429995a2a8b9e0775300 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 19:58:28 +1100 Subject: [PATCH 16/32] tdes: review fixes -- the keys' redundant key-type check goes, the stack table is measured, and the doc nits The same round as PR #140's review, applied here. TDESKey's two constructors ran check_key_type ahead of the engine's own validation, which is the same check for every mode that has a role; it goes (a scoped `cargo mutants` on key.rs is 0 missed; the crate lists 500 mutants). The constructors' docs say the checks are the engine's. The Security Considerations heading takes the style guide's form and the anchor follows it. The Memory Usage section tabulates the measured stack per entry point and engine: bench_tdes_mem_usage takes the AES bench's measure-closure shape for new and the block entry points of both engines, the two-key engine's decrypting ones only, under the TECBMMT3 COUNT = 0 bundle, and the figures are the closure frames from the release build's frame-layout remarks plus the return address. The unit tests in hazmat/tdes.rs and hazmat/tdes2.rs that tested public behaviour only (the engine sizes, the algorithm names and strengths, the ENCRYPTION_APPROVED gate) move to alg_name_tests; the split/join, parity and schedule pins stay. Nits: the manifest comment, the reference-style link in key.rs that could not fit, and every doc or comment line past 100 columns. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/Cargo.toml | 5 +- crypto/tdes/src/cbc.rs | 12 +- crypto/tdes/src/cfb.rs | 6 +- crypto/tdes/src/cfb8.rs | 6 +- crypto/tdes/src/ctr.rs | 9 +- crypto/tdes/src/des.rs | 20 +- crypto/tdes/src/hazmat/ecb.rs | 9 +- crypto/tdes/src/hazmat/tdes.rs | 15 -- crypto/tdes/src/hazmat/tdes2.rs | 35 +-- crypto/tdes/src/key.rs | 37 ++-- crypto/tdes/src/lib.rs | 38 ++-- crypto/tdes/src/sbox.rs | 4 +- crypto/tdes/tests/alg_name_tests.rs | 22 +- crypto/tdes/tests/key_tests.rs | 14 +- mem_usage_benches/src/bench_tdes_mem_usage.rs | 201 ++++++++++++++---- 15 files changed, 279 insertions(+), 154 deletions(-) diff --git a/crypto/tdes/Cargo.toml b/crypto/tdes/Cargo.toml index 94d0e4a0a..888f667d6 100644 --- a/crypto/tdes/Cargo.toml +++ b/crypto/tdes/Cargo.toml @@ -6,10 +6,9 @@ edition.workspace = true [dependencies] bouncycastle-core.workspace = true bouncycastle-utils.workspace = true -# Only for the TDES mode type aliases in `cbc.rs`, `cfb.rs`, `cfb8.rs`, `ctr.rs` and `ecb.rs`; the -# engine itself does not use it. +# 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 -# Only for the padded TDES-CBC and TDES-ECB aliases; the engine itself does not use it. [dev-dependencies] bouncycastle-core-test-framework.workspace = true diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index 2a4a28036..e5df7d39a 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -49,7 +49,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// use bouncycastle_cipher::modes::CBC; /// use bouncycastle_cipher::padding::PKCS7; /// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +/// 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(); @@ -80,7 +81,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// /// 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 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. @@ -106,7 +108,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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 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. @@ -150,7 +153,8 @@ pub type TDES_CBC<'k, Dir, Pad> = ::Select< /// 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 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]; /// diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index 2656d9a4f..0cb21749d 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -23,13 +23,15 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti // 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. +/// [`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, SymmetricEncryptionKey}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; /// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; /// use bouncycastle_rng::HashDRBG_SHA512; /// diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index 18218a8c4..c4f0343d5 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -21,13 +21,15 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti // 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. +/// [`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, SymmetricEncryptionKey}; +/// 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(); diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index ad3f8b03c..7493d641f 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -46,7 +46,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti 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. +/// [`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. @@ -55,7 +56,8 @@ pub const CTR_NONCE_LEN: usize = 6; /// 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, SymmetricEncryptionKey}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey}; +/// use bouncycastle_core::symmetric_key::SymmetricEncryptionKey; /// use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; /// use bouncycastle_rng::HashDRBG_SHA512; /// @@ -108,7 +110,8 @@ pub type TDES_CTR<'k, Dir> = Ctr<'k, TDESInternal, Dir, BLOCK_LEN, CTR_NONCE_LEN /// 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 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. diff --git a/crypto/tdes/src/des.rs b/crypto/tdes/src/des.rs index 2aa8153f6..ea81e7099 100644 --- a/crypto/tdes/src/des.rs +++ b/crypto/tdes/src/des.rs @@ -6,19 +6,21 @@ //! 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. +//! `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 +//! `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. +//! `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`]. @@ -310,8 +312,8 @@ mod tests { } } - /// `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`. + /// `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() { diff --git a/crypto/tdes/src/hazmat/ecb.rs b/crypto/tdes/src/hazmat/ecb.rs index 4fe3b660a..ec89ee567 100644 --- a/crypto/tdes/src/hazmat/ecb.rs +++ b/crypto/tdes/src/hazmat/ecb.rs @@ -51,7 +51,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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, SymmetricEncryptionKey}; +/// 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(); @@ -82,7 +83,8 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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 material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); /// let key = TDES3Key::>::from_key_material(&material).unwrap(); /// /// // Two identical blocks in... @@ -117,7 +119,8 @@ pub type TDES_ECB<'k, Dir, Pad> = ::Select< /// 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 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"); diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs index 57a3e590b..a455a271a 100644 --- a/crypto/tdes/src/hazmat/tdes.rs +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -252,21 +252,6 @@ impl core::fmt::Debug for TDESInternal { mod tests { use super::*; - #[test] - fn test_engine_size_is_exactly_the_three_schedules() { - // The "Memory Usage" table in the crate docs quotes this. No direction flag, no counters: - // three DEA keys of sixteen two-word round keys. - assert_eq!(size_of::(), 3 * 16 * 2 * 4); - assert_eq!(size_of::(), 384); - } - - #[test] - fn test_alg_name_and_strength() { - assert_eq!(::ALG_NAME, "TDES"); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); - assert!(>::ENCRYPTION_APPROVED); - } - #[test] fn test_split_and_join_round_trip_big_endian() { let block: Block = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]; diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index 358a380cc..10fbf7980 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -34,7 +34,8 @@ use bouncycastle_utils::secret::Secret; /// 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 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. @@ -52,7 +53,8 @@ use bouncycastle_utils::secret::Secret; /// 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 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. @@ -60,8 +62,9 @@ use bouncycastle_utils::secret::Secret; /// ``` /// /// `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 +/// [`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). @@ -105,8 +108,8 @@ impl ElectronicCodeBook for TDES2Internal { /// * [`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. + /// 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 { @@ -189,23 +192,3 @@ impl core::fmt::Debug for TDES2Internal { f.write_str(Self::ALG_NAME) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_engine_size_is_exactly_the_two_schedules() { - assert_eq!(size_of::(), 2 * 16 * 2 * 4); - assert_eq!(size_of::(), 256); - } - - #[test] - fn test_alg_name_strength_and_gate() { - assert_eq!(::ALG_NAME, "TDES-2KEY"); - assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); - assert!( - !>::ENCRYPTION_APPROVED - ); - } -} diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index 9aaf8e795..d6553fe3b 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -13,23 +13,26 @@ //! 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`]. -//! -//! [`ElectronicCodeBook::ENCRYPTION_APPROVED`]: bouncycastle_core::hazmat::ElectronicCodeBook::ENCRYPTION_APPROVED use crate::hazmat::{TDES2Internal, TDESInternal}; -use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::errors::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::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::errors::KeyMaterialError; +// end of imports needed for docs + mod sealed { pub trait Sealed {} } @@ -82,38 +85,23 @@ where impl> Eq for TDESKey where M::KeyState: Eq {} -/// Every TDEA mode is a cipher mode, so the material must be a `SymmetricCipherKey`; the engine's -/// constructor checks that too, but this is the error the role contract names. -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. + /// Checks and expands a 24-byte `Key1 || Key2 || Key3` bundle. The checks are the engine's: + /// every TDEA mode is a cipher mode and asks for the `SymmetricCipherKey` it requires. /// /// # Errors - /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`]. + /// * [`KeyMaterialError::InvalidKeyType`] if the key is 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)?) }) } } @@ -121,17 +109,16 @@ impl> FromKeyMaterial<{ crate::KEY_LEN }> for TDES3Key impl> FromKeyMaterial<{ crate::KEY_LEN_2KEY }> for TDES2Key { const KEY_TYPE: KeyType = M::KEY_TYPE; - /// Checks and expands a 16-byte `Key1 || Key2` bundle. + /// Checks and expands a 16-byte `Key1 || Key2` bundle; the checks are the engine's. /// /// # Errors - /// * [`KeyMaterialError::InvalidKeyType`] if the key is not of [`Self::KEY_TYPE`]. + /// * [`KeyMaterialError::InvalidKeyType`] if the key is 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)?) }) } } diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index c8ff28118..c40e8786e 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -7,7 +7,7 @@ //! [`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) +//! and TDEA is a **legacy algorithm**: see [Security Considerations](#-security-considerations-) //! before using it for anything new. //! //! # Usage Examples @@ -38,7 +38,8 @@ //! use bouncycastle_cipher::modes::CBC; //! use bouncycastle_cipher::padding::PKCS7; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey}; +//! 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 @@ -120,7 +121,8 @@ //! 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 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]; //! @@ -183,13 +185,21 @@ //! 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. +//! reference to the key. Measured as the frame of each entry point's `measure` closure in +//! `mem_usage_benches/src/bench_tdes_mem_usage.rs`, from the compiler's frame-layout remarks in +//! the release build (x86-64, rustc 1.97 nightly, return address included); the two-key engine +//! is decryption-only, so only its decrypting entry points are measured: +//! +//! | Entry point | [`TDESInternal`] | [`TDES2Internal`] | +//! |---|---|---| +//! | `new` (key expansion) | 800 B | 544 B | +//! | `encrypt_block` / `decrypt_block` | 48 B | 48 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. //! -//! Measure with `cargo run --release -p mem_usage_benches --bin bench_tdes_mem_usage`. -//! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! ## TDEA is a legacy algorithm //! @@ -218,15 +228,15 @@ //! ## 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 +//! 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`]; +//! 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 diff --git a/crypto/tdes/src/sbox.rs b/crypto/tdes/src/sbox.rs index 4be7ca9cb..18334db9f 100644 --- a/crypto/tdes/src/sbox.rs +++ b/crypto/tdes/src/sbox.rs @@ -3,8 +3,8 @@ //! //! # 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 +//! [`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. diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs index 169643edd..03a22941d 100644 --- a/crypto/tdes/tests/alg_name_tests.rs +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -12,7 +12,7 @@ use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ @@ -22,8 +22,8 @@ use bouncycastle_core::traits::Algorithm; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_tdes::hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; use bouncycastle_tdes::{ - BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, TDES2_CBC, - TDES2_CFB, TDES2_CFB8, TDES2_CTR, TDES3Key, + 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; @@ -82,6 +82,22 @@ fn the_engines_keep_their_own_names() { 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. +#[test] +fn only_the_three_key_engine_is_approved_for_encryption() { + assert!(>::ENCRYPTION_APPROVED); + 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) diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index 6f608661a..c605fe4a8 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -203,7 +203,8 @@ fn a_non_parity_bit_does_change_the_cipher() { for position in 0..3 { for bit in 0..64u32 { if bit % 8 == 0 { - continue; // a parity bit: bits 8, 16, ..., 64 in spec numbering are word bits 56, 48, .., 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; @@ -228,7 +229,8 @@ fn the_key_material_checks() { 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. + // 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!( @@ -274,10 +276,10 @@ fn is_weak_key_error2(result: Result) -> bo #[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. + // 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(); diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index c7171d16b..c969e7912 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -21,16 +21,27 @@ //! 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 +//! # What to expect, and why massif cannot see it //! -//! Like AES, TDESInternal has no interesting stack profile: peak usage is a small constant plus the key -//! schedule. The numbers worth recording in the crate docs are the persistent size of the engine -//! (three packed DEA schedules, 384 bytes) and the confirmation that per-block work is a fixed, -//! small amount of stack -- the six input planes and the 128-byte working array of the S-box -//! layer's ANF evaluation. +//! 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 point of comparison is that a table-driven Triple DES carries 2 KiB of combined S/P tables -//! on top of its schedules; this implementation carries none. +//! 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"` and read the frame of each +//! `measure` closure, which is the operation's frame with nothing else in it. 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 closure frames are 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 crate docs tabulate them with the 8-byte return address added. #![allow(dead_code)] #![allow(unused_imports)] @@ -38,7 +49,7 @@ 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}; +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() { @@ -47,7 +58,7 @@ fn bench_do_nothing() { print!("{}", 1 + 1); } -/// Prints the in-memory size of the engine, i.e. the persistent cost of holding a key schedule. +/// 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; @@ -57,55 +68,171 @@ fn print_struct_sizes() { println!("size_of: {}", size_of::()); } -fn key() -> KeyMaterial { - // A fixed pattern whose three 8-byte components are distinct and not weak; an all-zero buffer - // would be tagged KeyType::Zeroized and rejected. - let mut bytes = [0u8; KEY_LEN]; - for (i, b) in bytes.iter_mut().enumerate() { - *b = (i as u8).wrapping_mul(7).wrapping_add(1); - } - KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +/// 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!("TDES::new (key expansion)"); + eprintln!("TDESInternal::new (key expansion)"); + let key = key(&KEY_3); + measure(|| { + let tdes = TDESInternal::new(&key).unwrap(); + print!("{tdes:?}"); + }); +} - 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:?}"); + }); } -fn bench_tdes_encrypt_block() { - eprintln!("TDES::encrypt_block"); +// ---- 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. - let tdes = TDESInternal::new(&key()).unwrap(); - let mut block = [0x11u8; BLOCK_LEN]; - tdes.encrypt_block(&mut block); - print!("{block:x?}"); +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!("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?}"); + }); +} - let tdes = TDESInternal::new(&key()).unwrap(); - let mut block = [0x11u8; BLOCK_LEN]; - tdes.decrypt_block(&mut block); - print!("{block: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!("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?}"); + }); +} - let tdes = TDESInternal::new(&key()).unwrap(); - let mut blocks = [[0x11u8; BLOCK_LEN]; 4]; - tdes.encrypt_4blocks(&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() } From a9fb3dcf735832fd2e82a6b9a2868526b30fb1dc Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 19:58:28 +1100 Subject: [PATCH 17/32] cli: the tdes commands take the aes contract -- PKCS7, --iv/--iv-file and -d, with a test suite As for sm4 and camellia: tdes-cbc was CBC with no --iv while aes*-cbc on this base is PKCS7 at any length with --iv/--iv-file, and the two-key commands took their action positionally. tdes-cbc and tdes2-cbc are now CBC over the same block-mode helpers; the stream commands, three- and two-key, take --iv/--iv-file through load_iv (8 bytes, or the 6-byte CTR nonce); every command takes the direction as -d/--direction, the two-key ones with `decrypt` as the one value the parser accepts; tdes-ecb and tdes2-ecb keep NoPadding and no IV, as aes*-ecb does. The tdes commands had no CLI tests. tdes_cli_tests.rs pins COUNT = 0 of the CAVP MMT files the crate's suite uses, read from bc-test-data at test time and skipped with a warning without it: tdes-cbc, -cfb, -cfb8 and -ecb encrypt under --iv reproduce TCBCMMT3 (plus the PKCS7 block, computed from the engine), TCFB64MMT3, TCFB8MMT3 and TECBMMT3, and decrypt takes them back; the tdes2-* commands decrypt the two-key [DECRYPT] vectors. The contract tests cover the round trips of every mode, CBC's padding against ECB's alignment rule, a supplied CTR nonce held to the library, -x, the parser's refusal of `tdes2-* -d encrypt`, and the bundle-length, repeated-component and weak-key rejections. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/tdes2_cbc_cmd.rs | 22 ++- cli/src/tdes2_cfb8_cmd.rs | 11 +- cli/src/tdes2_cfb_cmd.rs | 9 +- cli/src/tdes2_ctr_cmd.rs | 11 +- cli/src/tdes2_ecb_cmd.rs | 3 +- cli/src/tdes_cbc_cmd.rs | 42 ++-- cli/src/tdes_cfb8_cmd.rs | 9 +- cli/src/tdes_cfb_cmd.rs | 9 +- cli/src/tdes_ctr_cmd.rs | 17 +- cli/src/tdes_ecb_cmd.rs | 5 +- cli/tests/tdes_cli_tests.rs | 378 ++++++++++++++++++++++++++++++++++++ 11 files changed, 457 insertions(+), 59 deletions(-) create mode 100644 cli/tests/tdes_cli_tests.rs diff --git a/cli/src/tdes2_cbc_cmd.rs b/cli/src/tdes2_cbc_cmd.rs index e70dbab60..6f9b03292 100644 --- a/cli/src/tdes2_cbc_cmd.rs +++ b/cli/src/tdes2_cbc_cmd.rs @@ -2,13 +2,16 @@ //! //! 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, the rest whole 8-byte blocks -- see [`crate::tdes_cbc_cmd`] and -//! [`crate::helpers::block_mode_helpers`]. The key is the 16-byte `KEY1 || KEY2`. +//! [`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_key_object}; +use crate::helpers::block_mode_helpers::{ + DecryptOnlyAction, decrypt_stream, load_iv, load_key_object, +}; use bouncycastle::cipher::modes::CBC; -use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::cipher::padding::PKCS7; use bouncycastle::core::symmetric_key::SymmetricDecryptionKey; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN_2KEY, TDES2Key}; @@ -16,15 +19,16 @@ 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"); + let key = load_key_object::>, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( - None, + load_iv::(iv, iv_file), |iv| key.create_decryptor(iv), output_hex, - Some("CBC"), + None, ) } diff --git a/cli/src/tdes2_cfb8_cmd.rs b/cli/src/tdes2_cfb8_cmd.rs index bdcad835b..ba3828a9e 100644 --- a/cli/src/tdes2_cfb8_cmd.rs +++ b/cli/src/tdes2_cfb8_cmd.rs @@ -3,10 +3,11 @@ //! 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, 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`. +//! 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_key_object}; +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}; @@ -15,9 +16,11 @@ 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, None, output_hex) + 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 index 9467a47af..677db994a 100644 --- a/cli/src/tdes2_cfb_cmd.rs +++ b/cli/src/tdes2_cfb_cmd.rs @@ -3,10 +3,11 @@ //! 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, the rest any length -- see [`crate::tdes_cfb_cmd`] and +//! 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_key_object}; +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}; @@ -15,9 +16,11 @@ 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, None, output_hex) + 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 index da1cefdb5..0240790ef 100644 --- a/cli/src/tdes2_ctr_cmd.rs +++ b/cli/src/tdes2_ctr_cmd.rs @@ -3,10 +3,11 @@ //! 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, 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`. +//! 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_key_object}; +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}; @@ -15,11 +16,13 @@ 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, None, output_hex) + 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 index 815cde98d..e722fd803 100644 --- a/cli/src/tdes2_ecb_cmd.rs +++ b/cli/src/tdes2_ecb_cmd.rs @@ -4,7 +4,8 @@ //! `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`. +//! 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; diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs index 66596940a..e79453290 100644 --- a/cli/src/tdes_cbc_cmd.rs +++ b/cli/src/tdes_cbc_cmd.rs @@ -1,53 +1,47 @@ //! TDES-CBC encryption and decryption, streaming stdin to stdout. //! -//! Only the mode wiring lives here: the IV convention, key loading, stdin framing and -//! block-alignment enforcement are all in [`crate::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. +//! 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. +//! 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. +//! 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_key_object, start_encryptor, + CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, start_encryptor, }; use bouncycastle::cipher::modes::CBC; -use bouncycastle::cipher::padding::NoPadding; +use bouncycastle::cipher::padding::PKCS7; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; -/// Names the mode in error messages. -const MODE: &str = "CBC"; - pub(crate) fn tdes_cbc_cmd( action: &CipherDirection, key: &Option, key_file: &Option, + iv: &Option, + iv_file: &Option, output_hex: bool, ) { - // `NoPadding`: whole blocks only, as the command's contract says. - let key = load_key_object::>, KEY_LEN>(key, key_file, "TDES"); + 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(None, |rng| key.create_encryptor(rng)); - encrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( - enc, - generated_iv.as_ref(), - output_hex, - Some(MODE), - ) + 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>( - None, + iv, |iv| key.create_decryptor(iv), output_hex, - Some(MODE), + None, ), } } diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs index 600d75a53..3582c4f95 100644 --- a/cli/src/tdes_cfb8_cmd.rs +++ b/cli/src/tdes_cfb8_cmd.rs @@ -1,7 +1,8 @@ //! 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 loader), shared with the +//! [`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. //! @@ -18,7 +19,7 @@ //! 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_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::cipher::modes::CFB8; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; @@ -27,8 +28,10 @@ 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, None, output_hex) + 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 index a699b3a27..63012921d 100644 --- a/cli/src/tdes_cfb_cmd.rs +++ b/cli/src/tdes_cfb_cmd.rs @@ -1,7 +1,8 @@ //! 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 loader), shared with the +//! [`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`. @@ -19,7 +20,7 @@ //! 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_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::cipher::modes::CFB; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; @@ -28,8 +29,10 @@ 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, None, output_hex) + 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 index 81dbd11c8..ecd19310f 100644 --- a/cli/src/tdes_ctr_cmd.rs +++ b/cli/src/tdes_ctr_cmd.rs @@ -1,7 +1,8 @@ //! 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 loader), shared with the +//! [`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. //! @@ -29,14 +30,15 @@ //! 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 and there is no way to -//! supply one, but with only 48 bits of nonce the number of messages under one key bundle must -//! stay small -- which SP 800-67 Rev 2 Sec 3.4's limit of 2^20 blocks per bundle already demands. +//! 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_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::cipher::modes::CTR; use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES3Key}; @@ -45,9 +47,12 @@ 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"); - run_stream_mode::<_, KEY_LEN, CTR_NONCE_LEN>(action, &key, None, output_hex) + 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 index f96fb45dc..af55e9bd3 100644 --- a/cli/src/tdes_ecb_cmd.rs +++ b/cli/src/tdes_ecb_cmd.rs @@ -1,8 +1,9 @@ //! 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 +//! 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. //! 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}"); + } +} From 0b195aeeed2271350c3450d31a3d097d93decbab Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 01:34:57 +1100 Subject: [PATCH 18/32] tdes: the stack table sums the frame chain, as the AES table does The same undercount Roy found on PR #140's SM4 table: the figures were each entry point's `measure` closure frame alone. The disassembly of the remark build 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, so those rows stand. Summing the chains with return addresses, on rustc 1.97 nightly x86-64: new 1304 B for the three-key engine and 920 B for the two-key one, the one-block calls 104 B. The bench's method note says to sum the chain and lists each frame, and its `[ENCRYPT]` is no longer read as a rustdoc link. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/src/lib.rs | 15 +++++++----- mem_usage_benches/src/bench_tdes_mem_usage.rs | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index c40e8786e..507ae06d6 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -185,15 +185,18 @@ //! 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 frame of each entry point's `measure` closure in -//! `mem_usage_benches/src/bench_tdes_mem_usage.rs`, from the compiler's frame-layout remarks in -//! the release build (x86-64, rustc 1.97 nightly, return address included); the two-key engine -//! is decryption-only, so only its decrypting entry points are measured: +//! 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) | 800 B | 544 B | -//! | `encrypt_block` / `decrypt_block` | 48 B | 48 B (decrypt) | +//! | `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) | //! diff --git a/mem_usage_benches/src/bench_tdes_mem_usage.rs b/mem_usage_benches/src/bench_tdes_mem_usage.rs index c969e7912..2bcc6e404 100644 --- a/mem_usage_benches/src/bench_tdes_mem_usage.rs +++ b/mem_usage_benches/src/bench_tdes_mem_usage.rs @@ -31,17 +31,23 @@ //! //! 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"` and read the frame of each -//! `measure` closure, which is the operation's frame with nothing else in it. 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. +//! `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 closure frames are 792 B for `TDESInternal::new` and +//! 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 crate docs tabulate them with the 8-byte return address added. +//! 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)] @@ -75,7 +81,7 @@ fn measure(f: impl FnOnce()) { f() } -/// The NIST CAVP TECBMMT3.rsp [ENCRYPT] COUNT = 0 bundle, so the engine under measurement is a +/// 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, From 620b86e85f10be5d51c4b04febc7ae8794116e9c Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 01:34:57 +1100 Subject: [PATCH 19/32] tdes: a key checks the key type its mode asks for before the engine does As for SM4 on PR #140: 5f367891 dropped the check against Mode::KEY_TYPE, leaving a key under a MAC mode declaring KEY_TYPE = MACKey while its constructor took cipher material and refused MAC material. The check is back on both bundle lengths, as AESKey has it; mode_key_tests pins the constant, both materials and generate for the three-key and two-key forms. Scoped mutants on key.rs: 27 tested, 5 caught, 22 unviable, none missed. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/src/key.rs | 41 ++++++++++++++++++++--------- crypto/tdes/tests/mode_key_tests.rs | 30 ++++++++++++++++++++- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index d6553fe3b..3ef5f00fa 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -15,24 +15,19 @@ //! is disallowed for encryption. See [`TDES2Internal`]. use crate::hazmat::{TDES2Internal, TDESInternal}; -use bouncycastle_core::errors::SymmetricCipherError; +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, KeyType}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; -// Imports needed for docs -#[allow(unused_imports)] -use bouncycastle_core::errors::KeyMaterialError; -// end of imports needed for docs - mod sealed { pub trait Sealed {} } @@ -85,23 +80,42 @@ where 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 checks are the engine's: - /// every TDEA mode is a cipher mode and asks for the `SymmetricCipherKey` it requires. + /// 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 a `SymmetricCipherKey`. + /// * [`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)?) }) } } @@ -109,16 +123,19 @@ impl> FromKeyMaterial<{ crate::KEY_LEN }> for TDES3Key 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 engine's. + /// 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 a `SymmetricCipherKey`. + /// * [`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)?) }) } } diff --git a/crypto/tdes/tests/mode_key_tests.rs b/crypto/tdes/tests/mode_key_tests.rs index 66cec1e2c..47e5b862d 100644 --- a/crypto/tdes/tests/mode_key_tests.rs +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -3,8 +3,9 @@ //! a two-key key can do, decrypt. use bouncycastle_cipher::modes::hazmat::ecb::ECB; -use bouncycastle_cipher::modes::{Blocks, CBC, CFB, CFB8, CTR, ModeNames}; +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; @@ -197,3 +198,30 @@ fn keys_compare_equal_exactly_when_their_material_does_to_the_engine() { .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())); +} From ec4eb5c8a4eefc9fea9676be02991a04b54dae71 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 03:30:42 +1100 Subject: [PATCH 20/32] tdes: the clippy nits from the camellia round The two that carry over from Roy's E on PR #141, the rest of B, C and E having no counterpart here: the key-schedule reference helper in the unit tests iterates the shift table instead of indexing it, and the two runtime asserts on ENCRYPTION_APPROVED, which clippy flags as constant and which key_tests and alg_name_tests both carried, are one compile-time pair in alg_name_tests. Clippy on the crate is clean; cargo test -p bouncycastle-tdes and the workspace all-targets build pass. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/src/schedule.rs | 6 +++--- crypto/tdes/tests/alg_name_tests.rs | 9 ++++----- crypto/tdes/tests/key_tests.rs | 2 -- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/crypto/tdes/src/schedule.rs b/crypto/tdes/src/schedule.rs index e3743eb01..9fc748485 100644 --- a/crypto/tdes/src/schedule.rs +++ b/crypto/tdes/src/schedule.rs @@ -170,9 +170,9 @@ mod tests { /// `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 i in 0..n { - c = rotl28(c, SHIFTS[i]); - d = rotl28(d, SHIFTS[i]); + 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; diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs index 03a22941d..3b152b304 100644 --- a/crypto/tdes/tests/alg_name_tests.rs +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -82,13 +82,12 @@ fn the_engines_keep_their_own_names() { 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. -#[test] -fn only_the_three_key_engine_is_approved_for_encryption() { +// 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. diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index c605fe4a8..c2f8b13b6 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -328,7 +328,5 @@ fn two_key_material_checks_and_metadata() { assert!(TDES2Internal::new(&key).is_ok()); assert_eq!(::MAX_SECURITY_STRENGTH, SecurityStrength::None); assert_eq!(::ALG_NAME, "TDES-2KEY"); - assert!(!>::ENCRYPTION_APPROVED); - assert!(>::ENCRYPTION_APPROVED); assert_eq!(format!("{:?}", TDES2Internal::new(&bundle2([a, b])).unwrap()), "TDES-2KEY"); } From 3b920bb261af2f26d54c4ac5206fe637099bc770 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 13:09:56 +1100 Subject: [PATCH 21/32] tdes: the PR #142 findings that apply here -- the mode and key docs sit on the items, and the CTR limit is pinned through a key Two of the aria review's findings hold for this crate too. The cbc, cfb, cfb8, ctr and key modules are private, so the docs each carried never rendered; they now sit on `TDES_CBC`, `TDES_CFB`, `TDES_CFB8`, `TDES_CTR` and `TDESKey`, the one path each has, and the module headers say so. And no test reached `Ctr`'s counter limit through a TDES key: mode_key_tests now does, under `CTR<7, 8>`, where the 256th block is the last and the byte after it is refused. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/src/cbc.rs | 46 ++++++++++---------- crypto/tdes/src/cfb.rs | 27 ++++++------ crypto/tdes/src/cfb8.rs | 23 +++++----- crypto/tdes/src/ctr.rs | 67 +++++++++++++++-------------- crypto/tdes/src/key.rs | 29 +++++++------ crypto/tdes/tests/mode_key_tests.rs | 29 +++++++++++-- 6 files changed, 124 insertions(+), 97 deletions(-) diff --git a/crypto/tdes/src/cbc.rs b/crypto/tdes/src/cbc.rs index e5df7d39a..72d872385 100644 --- a/crypto/tdes/src/cbc.rs +++ b/crypto/tdes/src/cbc.rs @@ -1,27 +1,5 @@ //! Type aliases for TDES in CBC mode (NIST SP 800-38A Sec 6.2; "TCBC" in SP 800-38A Appendix E), -//! with padding. -//! -//! 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. +//! with padding. The docs are on [`TDES_CBC`], which the crate root re-exports. use crate::BLOCK_LEN; use crate::hazmat::{TDES2Internal, TDESInternal}; @@ -116,6 +94,28 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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< diff --git a/crypto/tdes/src/cfb.rs b/crypto/tdes/src/cfb.rs index 0cb21749d..8eed3cd87 100644 --- a/crypto/tdes/src/cfb.rs +++ b/crypto/tdes/src/cfb.rs @@ -1,16 +1,5 @@ -//! Type aliases for TDES in CFB mode (NIST SP 800-38A Sec 6.3), full-block segment. -//! -//! 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. +//! 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}; @@ -62,6 +51,18 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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>; diff --git a/crypto/tdes/src/cfb8.rs b/crypto/tdes/src/cfb8.rs index c4f0343d5..06a897ded 100644 --- a/crypto/tdes/src/cfb8.rs +++ b/crypto/tdes/src/cfb8.rs @@ -1,14 +1,5 @@ -//! Type aliases for TDES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). -//! -//! 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. +//! 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}; @@ -55,6 +46,16 @@ use bouncycastle_core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncrypti /// 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>; diff --git a/crypto/tdes/src/ctr.rs b/crypto/tdes/src/ctr.rs index 7493d641f..cd5e4c85e 100644 --- a/crypto/tdes/src/ctr.rs +++ b/crypto/tdes/src/ctr.rs @@ -1,36 +1,5 @@ -//! Type aliases for TDES in CTR mode (NIST SP 800-38A Sec 6.5). -//! -//! 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. +//! 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}; @@ -90,6 +59,38 @@ pub const CTR_NONCE_LEN: usize = 6; /// 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>; diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index 3ef5f00fa..9b0d3b8bf 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -1,18 +1,6 @@ //! The TDEA key objects: a key bundle checked and expanded once, under a mode, handing out that -//! mode's workers through the role traits. -//! -//! [`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`]. +//! 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}; @@ -47,6 +35,19 @@ impl TDESEngine for TDES2Internal {} /// 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, } diff --git a/crypto/tdes/tests/mode_key_tests.rs b/crypto/tdes/tests/mode_key_tests.rs index 47e5b862d..847a2a380 100644 --- a/crypto/tdes/tests/mode_key_tests.rs +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -10,15 +10,17 @@ use bouncycastle_core::hazmat::{BlockPermutation, EcbDecryptionKey, ElectronicCo use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ - BlockDecryptionKey, FromKeyMaterial, SymmetricDecryptionKey, + BlockDecryptionKey, FromKeyMaterial, SymmetricDecryptionKey, SymmetricEncryptionKey, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; -use bouncycastle_core::traits::{BlockCipherDecryptor, StreamCipherDecryptor}; 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::HashDRBG_SHA512; +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}; @@ -225,3 +227,24 @@ fn a_mac_mode_key_refuses_every_material_consistently() { 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" + ); +} From a83f737df530a28eda99e05a8679ffb0c9f0590c Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 13:34:12 +1100 Subject: [PATCH 22/32] tdes: the merge-time adaptations to feature/key-rework-memory, carried over the rebase onto its release-based history This branch took feature/key-rework-memory by merge, and the merge commits carried the adaptations to what arrived. The rebase onto the branch's new history (key-rework-memory rebased onto release/0.1.3alpha, which carries feature/key-rework as one squash commit) dropped those merges; this commit restores the affected files to the branch's pre-rebase tip, so the tree is identical to it. Assisted-by: Claude:claude-mythos-5-1 --- crypto/tdes/tests/cavp_mmt_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs index 504173f4c..8f2daf643 100644 --- a/crypto/tdes/tests/cavp_mmt_tests.rs +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -43,8 +43,8 @@ use bouncycastle_core::symmetric_key::{ use bouncycastle_core::traits::{ BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; -use bouncycastle_core_test_framework::FixedSeedRNG; 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; From f3f1515a4d8ce9c51c7d49673bd9c24e5dbcaa12 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 5 Nov 2026 08:05:33 +1100 Subject: [PATCH 23/32] core, cipher, tdes: the decryption-only gate is BlockPermutation::DECRYPT_ONLY, checked wherever an encrypting worker is made -- ElectronicCodeBook::ENCRYPTION_APPROVED covered a key's create_encryptor alone, and a two-key decryptor's suspended state resumed in the encrypting direction, or its keystream wrapped as an encryptor, compiled and encrypted; the flag moves onto the permutation as library policy (two-key TDEA only; three-key encryption stays for the OpenSSL-provider interop, said so in the crate's Security Considerations), Direction gains IS_ENCRYPTING, every from_suspended in cbc, cfb, cfb8, ecb and the stream cipher asserts the pair in a const, StreamCipher::from_keystream is crate-private and asserts it through a sealed KeyStreamPolicy on the two keystream types, and a compile_fail doctest pins the resume route; the two-key docs say that CFB and CTR decryption are the forward cipher, and the NIST withdrawal notice is quoted verbatim and cited (review of 2026-11-04, items 1, 5, 9, 10) Assisted-by: Claude:claude-fable-5-1 --- crypto/cipher/src/lib.rs | 9 +++- crypto/cipher/src/modes/cbc.rs | 21 ++++++--- crypto/cipher/src/modes/ccm.rs | 24 +++++++++- crypto/cipher/src/modes/cfb.rs | 17 +++++-- crypto/cipher/src/modes/cfb8.rs | 17 +++++-- crypto/cipher/src/modes/ctr.rs | 4 +- crypto/cipher/src/modes/gcm.rs | 2 + .../cipher/src/modes/hazmat/ctr_key_stream.rs | 17 ++++++- crypto/cipher/src/modes/hazmat/ecb.rs | 21 ++++++--- crypto/cipher/src/stream.rs | 41 +++++++++++++--- .../core/src/hazmat/electronic_code_book.rs | 31 +++++++----- crypto/tdes/src/hazmat/tdes2.rs | 47 +++++++++++++++---- crypto/tdes/src/key.rs | 8 ++-- crypto/tdes/src/lib.rs | 23 +++++++-- crypto/tdes/tests/alg_name_tests.rs | 12 ++--- 15 files changed, 224 insertions(+), 70 deletions(-) diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs index ec8ea7c13..445bb96b2 100644 --- a/crypto/cipher/src/lib.rs +++ b/crypto/cipher/src/lib.rs @@ -83,12 +83,13 @@ pub struct Encrypting; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Decrypting; -mod sealed { +pub(crate) mod sealed { /// Private supertrait of [`Direction`](super::Direction): only this module can name it, so /// only the two markers below can implement `Direction`. pub trait Sealed {} impl Sealed for super::Encrypting {} impl Sealed for super::Decrypting {} + // The keystream types implement it beside their definitions, for `KeyStreamPolicy`. } /// Selects a type by direction: `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. @@ -124,14 +125,20 @@ pub trait Direction: sealed::Sealed { /// mode keeps for its phase: `0x10` encrypting, `0x20` decrypting. A state resumed under the /// other marker is refused, so an encryptor's state never continues as a decryptor. const STATE_BITS: u8; + + /// `true` for [`Encrypting`]: the direction a permutation that decrypts only + /// (`BlockPermutation::DECRYPT_ONLY`) refuses, at compile time, however the worker is made. + const IS_ENCRYPTING: bool; } impl Direction for Encrypting { type Select = Enc; const STATE_BITS: u8 = 0x10; + const IS_ENCRYPTING: bool = true; } impl Direction for Decrypting { type Select = Dec; const STATE_BITS: u8 = 0x20; + const IS_ENCRYPTING: bool = false; } diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs index 296a3db2f..5c9a07c37 100644 --- a/crypto/cipher/src/modes/cbc.rs +++ b/crypto/cipher/src/modes/cbc.rs @@ -109,7 +109,7 @@ use crate::modes::iv::random_iv; use crate::modes::{Blocks, ModeNames}; use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ BlockDecryptMode, BlockEncryptMode, BlockPermutation, ElectronicCodeBook, Mode, @@ -160,8 +160,8 @@ where ) -> 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; let iv = random_iv::(rng)?; @@ -204,8 +204,8 @@ where ) -> 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; let iv = random_iv::(rng)?; @@ -431,6 +431,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for Cbc<'k, P, Dir, BLOCK_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { type Key = P; @@ -440,8 +441,16 @@ where /// The state carries no direction: one suspended by the other direction's worker is accepted /// and resumes over the same chaining value, which gives wrong output rather than an error. - /// Only GCM, whose state means different things per direction, refuses the other one. + /// Only GCM, whose state means different things per direction, refuses the other one. A + /// permutation that decrypts only cannot be resumed in the encrypting direction at all: that + /// is a compile error, as making the encryptor any other way is. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { + const { + assert!( + !(Dir::IS_ENCRYPTING && P::DECRYPT_ONLY), + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it, resumed or otherwise" + ) + }; resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 3f900bc38..12e871f88 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -106,7 +106,7 @@ use crate::modes::ModeNames; use crate::modes::ctr::apply_counter_blocks; use crate::modes::iv::random_iv; -use crate::stream::StreamCipher; +use crate::stream::{KeyStreamPolicy, StreamCipher}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, @@ -126,7 +126,7 @@ use bouncycastle_utils::suspendable_state::{ }; use core::marker::PhantomData; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; /// CCM (SP 800-38C) over any [`ElectronicCodeBook`] with a 128-bit block, for Sec 3's "packet /// environment, i.e., when all of the data is available in storage before CCM is applied": the @@ -179,6 +179,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The spec's `q`: the octet length of the payload-length field `Q`. A.1 requires `n + q = 15`. const Q_LEN: usize = CcmKeyStream::::Q_LEN; @@ -641,6 +642,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: for Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The permutation's name for this mode, [`ModeNames::CCM_ALG_NAME`]. const ALG_NAME: &'static str = P::CCM_ALG_NAME; @@ -744,6 +746,21 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> crate::sealed::Sealed + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> +where + P: BlockPermutation + ModeNames, +{ +} + +impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> KeyStreamPolicy + for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> +where + P: BlockPermutation + ModeNames, +{ + const DECRYPT_ONLY: bool = P::DECRYPT_ONLY; +} + impl<'k, P, const BLOCK_LEN: usize, const NONCE_LEN: usize> KeyStream for CcmKeyStream<'k, P, BLOCK_LEN, NONCE_LEN> where @@ -815,6 +832,7 @@ impl< > CcmAdapter<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { /// The compile-time check for the trait adapters, run from both [`CcmPacketEncryptor`]'s and /// [`CcmPacketDecryptor`]'s constructors and from their one-shots, which never build an adapter, so @@ -1478,6 +1496,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: SuspendableComponent<'k> for Ccm<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { const STATE_LEN: usize = Self::CTR_STATE_LEN + BLOCK_LEN + 8 + 8 + 8; type Key = P; @@ -1947,6 +1966,7 @@ impl< for CcmAdapter<'k, P, Dir, BLOCK_LEN, NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { const STATE_LEN: usize = as SuspendableComponent<'k>>::STATE_LEN diff --git a/crypto/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs index e63714a1e..677119a4f 100644 --- a/crypto/cipher/src/modes/cfb.rs +++ b/crypto/cipher/src/modes/cfb.rs @@ -145,7 +145,7 @@ use crate::modes::ModeNames; use crate::modes::iv::random_iv; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, @@ -198,8 +198,8 @@ where ) -> 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; // `I1 = IV`. @@ -609,6 +609,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for Cfb<'k, P, Dir, BLOCK_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { type Key = P; @@ -618,8 +619,16 @@ where /// The state carries no direction: one suspended by the other direction's worker is accepted /// and resumes over the same chaining value, which gives wrong output rather than an error. - /// Only GCM, whose state means different things per direction, refuses the other one. + /// Only GCM, whose state means different things per direction, refuses the other one. A + /// permutation that decrypts only cannot be resumed in the encrypting direction at all: that + /// is a compile error, as making the encryptor any other way is. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { + const { + assert!( + !(Dir::IS_ENCRYPTING && P::DECRYPT_ONLY), + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it, resumed or otherwise" + ) + }; resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs index 24a9d2649..5a3168700 100644 --- a/crypto/cipher/src/modes/cfb8.rs +++ b/crypto/cipher/src/modes/cfb8.rs @@ -54,7 +54,7 @@ use crate::modes::ModeNames; use crate::modes::iv::random_iv; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, @@ -105,8 +105,8 @@ where ) -> 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; // `I1 = IV`. @@ -398,6 +398,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for Cfb8<'k, P, Dir, BLOCK_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { type Key = P; @@ -407,8 +408,16 @@ where /// The state carries no direction: one suspended by the other direction's worker is accepted /// and resumes over the same chaining value, which gives wrong output rather than an error. - /// Only GCM, whose state means different things per direction, refuses the other one. + /// Only GCM, whose state means different things per direction, refuses the other one. A + /// permutation that decrypts only cannot be resumed in the encrypting direction at all: that + /// is a compile error, as making the encryptor any other way is. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { + const { + assert!( + !(Dir::IS_ENCRYPTING && P::DECRYPT_ONLY), + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it, resumed or otherwise" + ) + }; resume_component(&state, key) } } diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs index f0a98bce8..0e39bc16b 100644 --- a/crypto/cipher/src/modes/ctr.rs +++ b/crypto/cipher/src/modes/ctr.rs @@ -173,8 +173,8 @@ where ) -> 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; let nonce = random_iv::(rng)?; diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 7daf12888..57425e1bf 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -378,6 +378,7 @@ where impl<'k, P, Dir, const TAG_LEN: usize> Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { /// The `N` of this type's [`SuspendableKeyed<'k, N>`] impl: the version header, the CTR state, /// the GHASH state, the two byte counts, the phase, the held-back tail and its length. See @@ -531,6 +532,7 @@ where impl<'k, P, Dir, const TAG_LEN: usize> Algorithm for Gcm<'k, P, Dir, TAG_LEN> where P: BlockPermutation<16> + ModeNames, + Dir: Direction, { const ALG_NAME: &'static str = P::GCM_ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index 6b000c088..43e4505e5 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -13,7 +13,7 @@ use bouncycastle_utils::suspendable_state::{Cursor, CursorMut, SuspendableCompon #[allow(unused_imports)] use crate::modes::Ctr; #[allow(unused_imports)] -use crate::stream::StreamCipher; +use crate::stream::{KeyStreamPolicy, StreamCipher}; // end of imports needed for docs /// The CTR keystream `Oj = CIPH_K(Tj)` (SP 800-38A Sec 6.5) over any [`BlockPermutation`], with @@ -145,6 +145,21 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> crate::sealed::Sealed + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> +where + P: BlockPermutation + ModeNames, +{ +} + +impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> KeyStreamPolicy + for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> +where + P: BlockPermutation + ModeNames, +{ + const DECRYPT_ONLY: bool = P::DECRYPT_ONLY; +} + impl<'k, P, const BLOCK_LEN: usize, const INIT_DATA_LEN: usize> KeyStream for CtrKeyStream<'k, P, BLOCK_LEN, INIT_DATA_LEN> where diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs index 2efe068e7..6f091a148 100644 --- a/crypto/cipher/src/modes/hazmat/ecb.rs +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -83,7 +83,7 @@ use crate::modes::{Blocks, ModeNames}; use crate::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ BlockPermutation, EcbDecryptMode, EcbEncryptMode, ElectronicCodeBook, Mode, @@ -131,8 +131,8 @@ where 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)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; Ecb::from_perm(state) @@ -173,8 +173,8 @@ where ) -> Result<(Self::Encryptor<'k>, [u8; 0]), SymmetricCipherError> { const { assert!( - P::ENCRYPTION_APPROVED, - "this permutation is approved for decryption only (ElectronicCodeBook::ENCRYPTION_APPROVED is false)" + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it" ) }; Ok((PaddedBlockCipherEncryptor::from_inner(Ecb::from_perm(state)), [])) @@ -330,6 +330,7 @@ impl<'k, P, Dir, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for Ecb<'k, P, Dir, BLOCK_LEN> where P: BlockPermutation + ModeNames, + Dir: Direction, { type Key = P; @@ -339,8 +340,16 @@ where /// The state carries no direction: one suspended by the other direction's worker is accepted /// and resumes over the same chaining value, which gives wrong output rather than an error. - /// Only GCM, whose state means different things per direction, refuses the other one. + /// Only GCM, whose state means different things per direction, refuses the other one. A + /// permutation that decrypts only cannot be resumed in the encrypting direction at all: that + /// is a compile error, as making the encryptor any other way is. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { + const { + assert!( + !(Dir::IS_ENCRYPTING && P::DECRYPT_ONLY), + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting worker may be made over it, resumed or otherwise" + ) + }; resume_component(&state, key) } } diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index 4447ea086..6adb698a1 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -18,7 +18,7 @@ //! docs](crate#suspending-and-resuming-execution) for an example. //! -use crate::{Decrypting, Encrypting}; +use crate::{Decrypting, Direction, Encrypting, sealed}; use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; use bouncycastle_core::hazmat::KeyStream; use bouncycastle_core::security_strength::SecurityStrength; @@ -67,6 +67,16 @@ where _marker: PhantomData, } +/// Whether a keystream's permutation decrypts only (`BlockPermutation::DECRYPT_ONLY`), so that +/// [`StreamCipher`] can refuse, at compile time, to be made in the encrypting direction over it, +/// resumed or otherwise. `KeyStream` itself is `dyn`-compatible and so carries no constant; this +/// sealed trait is implemented for the crate's keystream types alone, which is where every +/// `StreamCipher` comes from. +pub trait KeyStreamPolicy: sealed::Sealed { + /// The permutation's `DECRYPT_ONLY`. + const DECRYPT_ONLY: bool; +} + impl StreamCipher where @@ -75,11 +85,21 @@ where /// Wraps a keystream that has already been constructed and positioned, such as one that /// starts part-way into its counter space (GCM's GCTR starts at `inc32(J0)`). /// - /// # 🚨 Security Considerations 🚨 - /// This bypasses init-data generation: the keystream's nonce is whatever it was constructed - /// with, and the caller is responsible for it never repeating under the key. A key's - /// `create_encryptor` is the safe path. - pub fn from_keystream(keystream: KS) -> Self { + /// Crate-private: it bypasses init-data generation (the keystream's nonce is whatever it was + /// constructed with, and a repeat under the key is the two-time pad), and a decryptor's + /// keystream wrapped in the encrypting direction would be an encryptor over a permutation + /// that decrypts only. A key's `create_encryptor` is the public path. + pub(crate) fn from_keystream(keystream: KS) -> Self + where + KS: KeyStreamPolicy, + Dir: Direction, + { + const { + assert!( + !(Dir::IS_ENCRYPTING && KS::DECRYPT_ONLY), + "this keystream's permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting stream cipher may be made over it" + ) + }; Self::check_shape(); Self { keystream, pending: Secret::new(), used: BLOCK_LEN, _marker: PhantomData } } @@ -342,7 +362,8 @@ where impl<'k, KS, Dir, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, const N: usize> SuspendableKeyed<'k, N> for StreamCipher where - KS: KeyStream + SuspendableComponent<'k>, + KS: KeyStream + SuspendableComponent<'k> + KeyStreamPolicy, + Dir: Direction, { type Key = KS::Key; @@ -354,6 +375,12 @@ where /// and resumes over the same chaining value, which gives wrong output rather than an error. /// Only GCM, whose state means different things per direction, refuses the other one. fn from_suspended(state: [u8; N], key: &'k Self::Key) -> Result { + const { + assert!( + !(Dir::IS_ENCRYPTING && KS::DECRYPT_ONLY), + "this keystream's permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no encrypting stream cipher may be made over it, resumed or otherwise" + ) + }; resume_component(&state, key) } } diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs index d80d8bbd1..ceb2815d9 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -35,6 +35,24 @@ pub trait BlockPermutation: Algorithm + Sized { /// The inverse cipher function, in place. fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); + /// Whether this library allows the permutation to decrypt only: no encrypting worker may be + /// made over it, by a key's `create_encryptor`, by resuming a suspended state in the + /// encrypting direction, or otherwise. A mode checks this in an inline `const` wherever it + /// makes an encrypting worker, so the refusal is a compile error (reported after + /// monomorphization, so by `cargo build` or `cargo test` rather than `cargo check`). + /// + /// This records the library's policy, not a standard's status. It is `true` for two-key TDEA + /// alone, which NIST SP 800-131A Rev 2 Table 1 lists as "Disallowed" for encryption and + /// "Legacy use" for decryption. Three-key TDEA encryption, which the same table disallows + /// after 2023, keeps `false` because the OpenSSL provider this library serves still exposes + /// it; the tdes crate's Security Considerations say so. + /// + /// The block methods themselves are not gated: a decrypting CFB or CTR needs the forward + /// cipher function, and a raw permutation is not something to encrypt data with in any case + /// (see the trait docs). Those decryptors are therefore an encryption capability in a + /// determined caller's hands, which the tdes crate's docs spell out. + const DECRYPT_ONLY: bool = false; + /// The forward cipher function on two *independent* blocks, in place. /// /// Required, with no default, so that every implementor decides for itself how to run a pair. @@ -88,17 +106,4 @@ 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/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index 10fbf7980..c5c9813f7 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -21,9 +21,11 @@ use bouncycastle_utils::secret::Secret; /// **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: +/// [`BlockPermutation::DECRYPT_ONLY`], and every mode in `bouncycastle_cipher::modes` refuses to +/// make an encrypting worker over it **at compile time**, however the worker would be made: a +/// two-key key's `create_encryptor` does not compile, and neither does resuming a suspended state +/// in the encrypting direction. (The error is reported after monomorphization, by `cargo build` +/// or `cargo test`, with its span in the mode's code; `cargo check` does not reach it.) /// /// ```compile_fail /// use bouncycastle_tdes::TDES2Key; @@ -42,8 +44,8 @@ use bouncycastle_utils::secret::Secret; /// 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: +/// The stream modes check the same constant through their keystream, so the refusal holds for +/// CTR, CFB and CFB8 too: /// /// ```compile_fail /// use bouncycastle_tdes::{BLOCK_LEN, CTR_NONCE_LEN, TDES2Key}; @@ -61,13 +63,40 @@ use bouncycastle_utils::secret::Secret; /// let _ = key.create_encryptor(&mut HashDRBG_SHA512::new_from_os()); /// ``` /// +/// Nor can a decryptor's suspended state be resumed as an encryptor: +/// +/// ```compile_fail +/// use bouncycastle_cipher::Encrypting; +/// use bouncycastle_cipher::modes::{Blocks, CBC, Cbc}; +/// use bouncycastle_core::hazmat::ModeKey; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{BlockDecryptionKey, FromKeyMaterial}; +/// use bouncycastle_core::traits::SuspendableKeyed; +/// use bouncycastle_tdes::hazmat::TDES2Internal; +/// use bouncycastle_tdes::{BLOCK_LEN, TDES2Key}; +/// +/// 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(); +/// const N: usize = Cbc::<'static, TDES2Internal, Encrypting, BLOCK_LEN>::SUSPENDED_STATE_LEN; +/// let state: [u8; N] = key.create_decryptor(&[0u8; BLOCK_LEN]).suspend(); +/// +/// // The decryptor's state resumed in the encrypting direction: this does not compile. +/// let _ = Cbc::<'_, TDES2Internal, Encrypting, BLOCK_LEN>::from_suspended(state, key.state()); +/// ``` +/// /// `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). +/// anyway (see the crate docs). For the same reason the gate stops accidental encryption through +/// the encrypting APIs, not deliberate use of the forward cipher: CTR decryption is CTR +/// encryption, and a CFB decryptor under a chosen IV gives `CIPH(IV)` for a zero block, so a +/// caller who wants two-key TDEA encryption can assemble it from those decryptors. A deployment +/// that must rule it out should expose only the CBC and ECB decryptors. /// /// # Security strength /// @@ -130,12 +159,12 @@ impl ElectronicCodeBook for TDES2Internal { } 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 { + /// Two-key TDEA decrypts only (SP 800-131A Rev 2 Table 1); see the type docs. + const DECRYPT_ONLY: bool = true; + /// The forward cipher operation with `Key3 = Key1`: `O = F_Key1(I_Key2(F_Key1(d)))`. /// /// Present because CFB and CTR decryption need it, and because the trait requires it; the diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index 9b0d3b8bf..bd0a6a5ef 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -45,9 +45,11 @@ impl TDESEngine for TDES2Internal {} /// 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`]. +/// the same roles, but every encrypting worker over it is a compile error: the modes check +/// [`BlockPermutation::DECRYPT_ONLY`](bouncycastle_core::hazmat::BlockPermutation::DECRYPT_ONLY) +/// wherever they make one, a key's `create_encryptor` and a +/// resume in the encrypting direction alike, and two-key TDEA is disallowed for encryption. See +/// [`TDES2Internal`]. pub struct TDESKey> { state: M::KeyState, } diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index 507ae06d6..0d4477b81 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -103,8 +103,14 @@ //! //! [`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 +//! Its engine sets `BlockPermutation::DECRYPT_ONLY`, so no encrypting worker can be made over +//! it: the key's `create_encryptor` refuses to compile under every mode, and so does resuming a +//! suspended state in the encrypting direction, while its decryptors work. That gate +//! stops accidental encryption through the encrypting APIs, not deliberate use of the forward +//! cipher function: CTR decryption *is* CTR encryption, and a CFB decryptor under a chosen IV +//! maps a zero block to `CIPH(IV)`, so two-key encryption can be assembled from those decryptors +//! with the safe API alone (SP 800-38A defines the modes that way). A deployment that must rule +//! two-key encryption out altogether should expose only the CBC and ECB decryptors. The aliases //! [`TDES2_CBC`], [`TDES2_CFB`], [`TDES2_CFB8`], [`TDES2_CTR`] and [`TDES2_ECB`] are those //! decryptors, with the IV or nonce supplied by the caller as it was received: //! @@ -206,9 +212,11 @@ //! //! ## 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." +//! NIST withdrew SP 800-67 Rev 2 on 1 January 2024. Its announcement +//! (): "The scheduled +//! withdrawal of SP 800-67 Rev. 2 will signify that TDEA is no longer an approved block cipher. +//! TDEA will continue to be allowed for the decryption, key unwrapping, and verification of MACs +//! of already-protected data". //! SP 800-131A Rev 2, Table 1, gives the transition: three-key TDEA encryption "Deprecated through //! 2023, Disallowed after 2023", three-key decryption "Legacy use", two-key encryption //! "Disallowed". SP 800-57 Part 1 Rev 5, Table 2, rates 3TDEA at 112 bits of security, which is @@ -217,6 +225,11 @@ //! This crate exists so that data and protocols that still depend on TDEA can be handled. Do not //! choose it for a new design; use AES from `bouncycastle-aes`. //! +//! `BlockPermutation::DECRYPT_ONLY` records this library's policy, not NIST's status: it is set +//! for two-key TDEA alone. Three-key TDEA encryption, which SP 800-131A Rev 2 also disallows +//! after 2023, stays available here because the OpenSSL provider this library serves exposes it; +//! a deployment that must not encrypt under TDEA should not use [`TDES3Key`]'s encrypting roles. +//! //! ## The 64-bit block, and the data limit //! //! SP 800-67r2 Sec 3.4: "One key bundle shall not be used to apply cryptographic protection (e.g., diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs index 3b152b304..c0da2f735 100644 --- a/crypto/tdes/tests/alg_name_tests.rs +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -12,7 +12,7 @@ use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, }; use bouncycastle_cipher::{Decrypting, Encrypting}; -use bouncycastle_core::hazmat::{ElectronicCodeBook, Mode}; +use bouncycastle_core::hazmat::{BlockPermutation, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{ @@ -22,8 +22,8 @@ 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, + BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, TDES2_CBC, + TDES2_CFB, TDES2_CFB8, TDES2_CTR, TDES3Key, }; use core::mem::size_of; @@ -84,10 +84,8 @@ fn the_engines_keep_their_own_names() { // 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); +const _: () = assert!(!>::DECRYPT_ONLY); +const _: () = assert!(>::DECRYPT_ONLY); /// The engines are exactly their packed schedules, the sizes the crate docs quote: sixteen /// two-word round keys per DEA key, three or two keys, and nothing else. From 657f758f03e11d055989b5fdcc1d0d8a7e07cd00 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 5 Nov 2026 08:05:33 +1100 Subject: [PATCH 24/32] tdes: spread goes through ct::value_barrier off x86-64 and aarch64, since LLVM folded its shifts back into a muls on Cortex-M0, and the assembly harness compiles the round function from des.rs and sbox.rs by path with a plain-shift spread as the control (no multiply on thumbv6m or riscv32i); is_weak refuses the spec's 64 keys and no others -- the odd-weight period-4 nibbles gave it 256 classes -- with the 256 rotation-invariant (C0, D0) pairs enumerated against it, 1F0101010E010101 pinned as accepted, and random equal-halves keys accepted, which kills the OR-to-XOR mutant the PR description had called equivalent; the CAVP suite reads through the framework's bc_test_data loader, which panics on a missing file rather than passing vacuously; the stale des.rs link names forward and inverse (review of 2026-11-04, items 2, 3, 4, 7, 11) Assisted-by: Claude:claude-fable-5-1 --- crypto/tdes/src/des.rs | 19 ++++++- crypto/tdes/src/schedule.rs | 26 ++++++--- crypto/tdes/tests/cavp_mmt_tests.rs | 42 ++++---------- crypto/tdes/tests/key_tests.rs | 79 ++++++++++++++++++++++++--- dev_scripts/aes_ct_harness/src/lib.rs | 31 +++++++++-- dev_scripts/check_aes_ct_asm.sh | 22 +++++--- 6 files changed, 157 insertions(+), 62 deletions(-) diff --git a/crypto/tdes/src/des.rs b/crypto/tdes/src/des.rs index ea81e7099..4f8589132 100644 --- a/crypto/tdes/src/des.rs +++ b/crypto/tdes/src/des.rs @@ -33,6 +33,7 @@ //! to the spec's tables by the unit-vector tests below. use crate::sbox::sbox_layer; +use bouncycastle_utils::ct; /// One expanded DEA key: sixteen round keys `K1..K16`, two words each -- `[lo, hi]` for round `n` /// at `[2n, 2n + 1]`. See [`crate::schedule`] for the packing. @@ -41,12 +42,23 @@ pub(crate) type Subkeys = [u32; 32]; /// Every nibble's least significant bit. const NIBBLE_LSB: u32 = 0x1111_1111; +/// Targets whose integer multiply is fixed-latency, so [`spread`] may be left for LLVM to fold +/// into one. A `const` rather than a `cfg` on the body, so both arms compile on every target. +const MULTIPLY_IS_FIXED_LATENCY: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); + /// Spreads the least significant bit of each nibble to all four bits of that nibble. /// -/// The input must have no other bits set. Written as shifts rather than a multiply by 15 so that -/// nothing depends on the multiplier's timing. +/// The input must have no other bits set. Written as shifts rather than a multiply by 15, but +/// LLVM sees that the four shifted copies OR disjoint bits and folds them back into `v * 15` +/// wherever it may: two `lea`s on x86-64, where that is harmless, and a `muls` on Cortex-M0, +/// whose timing depends on the operand. So off the two fixed-latency targets `v` first goes +/// through [`ct::value_barrier`], which hides its known-zero bits, and the ORs stay ORs; this is +/// the same guard as `bouncycastle-aes`'s round-key widening, and +/// `dev_scripts/check_aes_ct_asm.sh` greps the round function's assembly for it on Cortex-M0 +/// and RISC-V 32I. #[inline(always)] fn spread(v: u32) -> u32 { + let v = if MULTIPLY_IS_FIXED_LATENCY { v } else { ct::value_barrier(v) }; v | (v << 1) | (v << 2) | (v << 3) } @@ -175,7 +187,8 @@ fn f(r: u32, lo: u32, hi: u32) -> u32 { /// /// `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 +/// [`crate::hazmat::tdes::forward`] (and `inverse`) applies each permutation once around all +/// forty-eight rounds. Every /// intermediate value is identical to the spec's; only two redundant permutations are skipped. #[inline(always)] pub(crate) fn rounds(l: u32, r: u32, sk: &Subkeys, inverse: bool) -> (u32, u32) { diff --git a/crypto/tdes/src/schedule.rs b/crypto/tdes/src/schedule.rs index 9fc748485..7a661be59 100644 --- a/crypto/tdes/src/schedule.rs +++ b/crypto/tdes/src/schedule.rs @@ -131,13 +131,18 @@ pub(crate) fn expand(key: u64, out: &mut 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. +/// table: after PC-1, `C0` and `D0` are each a 28-bit pattern of period 4 whose repeated nibble +/// has even weight (all zeros, all ones, `0101...`, `1010...`, `0011...`, `0110...`, `1100...` +/// or `1001...`). The schedule only ever rotates `C` and `D`, and a period-4 pattern has at most +/// four distinct rotations, hence at most four distinct subkeys -- one for the weak keys (period +/// 1), two for the semi-weak (period 2), four for the possibly weak. Eight patterns for `C0` +/// times eight for `D0` is exactly 64 keys, and PC-1 is a bijection on the 56 non-parity bits. +/// +/// The other eight period-4 nibbles, the odd-weight ones (`0001`, `0010`, ... `1110`), also give +/// only four distinct subkeys, but the spec's lists do not include them, and this predicate +/// follows the lists: the parity term below keeps those 192 further key classes accepted, as +/// BC Java's `DESParameters` and the spec's own count do. The tests enumerate all 256 +/// rotation-invariant `(C0, D0)` pairs against this predicate, and the spec's three lists. /// /// Parity bits are not consulted (PC-1 does not select them), which is also how the spec's lists /// are meant: "the weak keys listed above and the semi-weak keys and the possibly weak keys listed @@ -146,8 +151,11 @@ pub(crate) fn expand(key: u64, out: &mut Subkeys) { /// 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()) + // Bit 0 of `x ^ (x >> 1) ^ (x >> 2) ^ (x >> 3)` is the parity of the low nibble, which for a + // period-4 pattern is the parity of every nibble. + let odd = |x: u32| (x ^ (x >> 1) ^ (x >> 2) ^ (x >> 3)) & 1; + let period4_even = (rotl28(c, 4) ^ c) | (rotl28(d, 4) ^ d) | odd(c) | odd(d); + ct_eq_zero_bytes(&period4_even.to_be_bytes()) } #[cfg(test)] diff --git a/crypto/tdes/tests/cavp_mmt_tests.rs b/crypto/tdes/tests/cavp_mmt_tests.rs index 8f2daf643..ecd65540a 100644 --- a/crypto/tdes/tests/cavp_mmt_tests.rs +++ b/crypto/tdes/tests/cavp_mmt_tests.rs @@ -2,11 +2,11 @@ //! `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. +//! The files are read through the test framework's `bc_test_data` loader: `bc-test-data` cloned +//! beside this repository, with the response files from NIST's `tdesmmt.zip` (CAVS 18.0) under +//! `crypto/aes_tdes_vectors/TDES/`. If the repository is absent the tests print a warning and +//! pass, as the AES ACVP and ML-KEM / ML-DSA suites do; if it is present and a file is missing, +//! the loader panics rather than letting that file's test pass vacuously. //! //! Every case is checked in **both** directions regardless of the section it came from: //! encrypting `PLAINTEXT` must give `CIPHERTEXT` and decrypting `CIPHERTEXT` must give @@ -43,33 +43,14 @@ use bouncycastle_core::symmetric_key::{ use bouncycastle_core::traits::{ BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, }; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; use bouncycastle_hex as hex; use bouncycastle_rng::FixedSeedRNG; use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; use bouncycastle_tdes::{BLOCK_LEN, KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; -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 -} + +/// Where the response files sit under `bc-test-data`. +const CAVP_DIR: &str = "crypto/aes_tdes_vectors/TDES"; /// One MMT case. `iv` is empty for ECB. #[derive(Default, Debug, Clone)] @@ -127,15 +108,14 @@ fn parse_rsp(text: &str) -> Vec { /// 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 text = bc_test_data(CAVP_DIR, file)?; let cases = parse_rsp(&text); let encrypt = cases.iter().filter(|c| c.section == "ENCRYPT").count(); let decrypt = cases.iter().filter(|c| c.section == "DECRYPT").count(); assert_eq!(encrypt + decrypt, cases.len(), "{file}: unexpected section"); assert_eq!(decrypt, 10, "{file}: ten decrypt cases"); assert_eq!(encrypt, if file.ends_with("MMT2.rsp") { 0 } else { 10 }, "{file}: encrypt cases"); - println!("{file}: {} cases found at {}", cases.len(), dir.display()); + println!("{file}: {} cases found under bc-test-data/{CAVP_DIR}", cases.len()); Some(cases) } diff --git a/crypto/tdes/tests/key_tests.rs b/crypto/tdes/tests/key_tests.rs index c2f8b13b6..2374258c4 100644 --- a/crypto/tdes/tests/key_tests.rs +++ b/crypto/tdes/tests/key_tests.rs @@ -72,6 +72,77 @@ fn is_weak_key_error(result: Result) -> bool matches!(result, Err(SymmetricCipherError::KeyMaterialError(KeyMaterialError::WeakKey(_)))) } +/// The DEA key (odd parity bits cleared) whose PC-1 output is `(c, d)`: inverse PC-1, which is a +/// bijection on the 56 non-parity bits. +fn key_from_cd(c: u32, d: u32) -> u64 { + let cd = ((c as u64) << 28) | d as u64; + let mut key = 0u64; + for (j, &dst) in PC1.iter().enumerate() { + let bit = (cd >> (55 - j)) & 1; + key |= bit << (64 - dst as u32); + } + key & PARITY_MASK +} + +/// The engine's verdict on `key` in the first component position of an otherwise good bundle. +fn refused_as_weak(key: u64) -> bool { + is_weak_key_error(TDESInternal::new(&bundle([key, GOOD[1], GOOD[2]]))) +} + +/// The 28-bit pattern that repeats the nibble `n` seven times. +fn repeated(n: u32) -> u32 { + (0..7).fold(0, |acc, _| (acc << 4) | n) +} + +/// Every key that is invariant under the schedule's rotations -- `C0` and `D0` each one of the +/// 16 period-4 patterns, 256 pairs -- gets the documented verdict: refused exactly when both +/// nibbles have even weight, which is the spec's 64 keys, and accepted for the other 192 (such +/// as `1F0101010E010101`, `C0 = 0`, `D0 = 0x1111111`), which Sec 3.3.2 does not list though they +/// too give four distinct subkeys. This asks the predicate about every rotation-invariant key, +/// not only the listed ones. +#[test] +fn every_rotation_invariant_key_gets_the_documented_verdict() { + let mut refused = 0; + for cn in 0..16u32 { + for dn in 0..16u32 { + let key = key_from_cd(repeated(cn), repeated(dn)); + let listed = cn.count_ones().is_multiple_of(2) && dn.count_ones().is_multiple_of(2); + assert_eq!(refused_as_weak(key), listed, "C0 nibble {cn:04b}, D0 nibble {dn:04b}"); + refused += usize::from(listed); + } + } + assert_eq!(refused, 64); + assert!(!refused_as_weak(0x1F0101010E010101), "C0 = 0, D0 = 0x1111111 is not in Sec 3.3.2"); + assert!(refused_as_weak(0x1F1F1F1F0E0E0E0E), "the weak key with C0 = 0, D0 = 0xFFFFFFF is"); +} + +/// Keys whose `C0` and `D0` are equal but have no period are accepted: the predicate ORs the two +/// halves' period tests, and an XOR in its place would call every such key weak (2^28 classes). +#[test] +fn equal_halves_without_a_period_are_accepted() { + let mut x: u32 = 0x9E37_79B9; + let mut accepted = 0; + for _ in 0..2000 { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let half = x & 0x0FFF_FFFF; + if half.rotate_left(4) & 0x0FFF_FFFF == half + || (half << 4 | half >> 24) & 0x0FFF_FFFF == half + { + continue; + } + let key = key_from_cd(half, half); + if [GOOD[1], GOOD[2]].contains(&key) { + continue; + } + assert!(!refused_as_weak(key), "C0 = D0 = {half:07x} has no period 4"); + accepted += 1; + } + assert!(accepted > 1900, "{accepted} keys checked"); + assert!(!refused_as_weak(0x10BA7C00FE543800), "C0 = D0 = 0x1234567"); +} + #[test] fn a_good_bundle_is_accepted() { assert!(TDESInternal::new(&bundle(GOOD)).is_ok()); @@ -115,13 +186,7 @@ fn the_three_lists_are_exactly_the_period_four_keys() { 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); + structural.insert(key_from_cd(c, d)); } } assert_eq!(structural.len(), 64); diff --git a/dev_scripts/aes_ct_harness/src/lib.rs b/dev_scripts/aes_ct_harness/src/lib.rs index 8f1670d47..6d76c1873 100644 --- a/dev_scripts/aes_ct_harness/src/lib.rs +++ b/dev_scripts/aes_ct_harness/src/lib.rs @@ -1,14 +1,22 @@ //! The two bodies of `PlaneWord::splat` from `crypto/aes/src/bitslice.rs`, with the barrier the //! crate applies off x86-64 and aarch64 and without it, each out of line so that it lands in the -//! assembly as one symbol. `../check_aes_ct_asm.sh` builds this for the Cortex-M0 and RISC-V 32I -//! targets and checks that the barriered bodies hold no multiply, call or branch, and that the -//! plain ones do show the multiply LLVM folds the shifts and ORs into, which is what the check -//! exists to catch. Keep the bodies the same as the crate's. +//! assembly as one symbol; and the TDES round function, whose `spread` carries the same guard, +//! compiled from the crate's own `des.rs` and `sbox.rs` (included by path, unmodified), with a +//! plain-shift `spread` beside it as the control. `../check_aes_ct_asm.sh` builds this for the +//! Cortex-M0 and RISC-V 32I targets and checks that the barriered bodies hold no multiply, call +//! or branch, and that the plain ones do show the multiply LLVM folds the shifts and ORs into, +//! which is what the check exists to catch. Keep the `splat` bodies the same as the crate's. #![no_std] #![forbid(unsafe_code)] +#![allow(dead_code)] use bouncycastle_utils::ct; +#[path = "../../../crypto/tdes/src/des.rs"] +mod des; +#[path = "../../../crypto/tdes/src/sbox.rs"] +mod sbox; + /// `u32::splat` as the crate compiles it off the fixed-latency targets. #[inline(never)] pub fn splat32_barrier(pattern: u16) -> u32 { @@ -36,3 +44,18 @@ pub fn splat64_plain(pattern: u16) -> u64 { let x = u64::from(pattern); x | (x << 16) | (x << 32) | (x << 48) } + +/// The TDES round function as the crate compiles it: sixteen DEA rounds over a schedule, with +/// `spread` going through the barrier off the fixed-latency targets. +#[inline(never)] +pub fn tdes_rounds(l: u32, r: u32, sk: &des::Subkeys) -> (u32, u32) { + des::rounds(l, r, sk, false) +} + +/// The crate's `spread` without the barrier: the positive control, which LLVM folds into a +/// multiply by 15 on Cortex-M0. +#[inline(never)] +pub fn spread_plain(v: u32) -> u32 { + let v = v & 0x1111_1111; + v | (v << 1) | (v << 2) | (v << 3) +} diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh index 4804d6c86..c110ca23d 100755 --- a/dev_scripts/check_aes_ct_asm.sh +++ b/dev_scripts/check_aes_ct_asm.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) -# compiles to no multiply, call or branch on the targets whose multiply is not fixed-latency. +# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) and +# the TDES round function (`spread` in crypto/tdes/src/des.rs) compile to no multiply, call or +# branch on the targets whose multiply is not fixed-latency. # Builds dev_scripts/aes_ct_harness with the nightly toolchain and `-Zbuild-std=core` (the # targets have no shipped `core`), emits the assembly and greps the four `splat*` bodies: the # barriered pair must be clean, and the plain pair must show the multiply the barrier prevents, @@ -48,18 +49,23 @@ for t in $TARGETS; do exit 2 fi - for f in splat32_barrier splat64_barrier; do - hits=$(body "$asm" "$f" | grep -cE "$bad" || true) + # splat is straight-line code, so any call or branch in it is a fault; the TDES round function + # has a loop counter and a buffer clear, which are not secret-dependent, so for it only a + # multiply counts. + for spec in splat32_barrier:all splat64_barrier:all tdes_rounds:mul; do + f=${spec%%:*}; kind=${spec##*:} + if [ "$kind" = all ]; then pat=$bad; what="multiply, call or branch"; else pat=$mul; what="multiply"; fi + hits=$(body "$asm" "$f" | grep -cE "$pat" || true) n=$(body "$asm" "$f" | wc -l) if [ "$hits" -ne 0 ]; then - echo "FAIL $t $f: $hits multiply/call/branch line(s) in $n instructions" - body "$asm" "$f" | grep -E "$bad" + echo "FAIL $t $f: $hits $what line(s) in $n instructions" + body "$asm" "$f" | grep -E "$pat" status=1 else - echo "ok $t $f: no multiply, call or branch in $n instructions" + echo "ok $t $f: no $what in $n instructions" fi done - control=$(for f in splat32_plain splat64_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) + control=$(for f in splat32_plain splat64_plain spread_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) if [ "$control" -eq 0 ]; then echo "FAIL $t: the unbarriered bodies show no multiply, so the grep proves nothing" status=1 From fd55fc5447e345c88a17a815bb2c843b2d5fec27 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 5 Nov 2026 08:05:33 +1100 Subject: [PATCH 25/32] cli: the tdes-* encrypt commands refuse input past SP 800-67 Rev 2 Sec 3.4's 2^20 blocks in one invocation, through an InputLimit on the block- and stream-mode encrypt helpers (one byte less for the padded CBC), with cli/tests/test_tdes.sh driving every mode past and up to the bound; the help says what is enforced, the three-key help points at tdes2-*, the two-key CBC help says CFB and CTR decryption are the forward cipher, and the CAVP file probe reports a missing file instead of skipping (review of 2026-11-04, items 6, 7, 8, 9) Assisted-by: Claude:claude-fable-5-1 --- cli/src/helpers/block_mode_helpers.rs | 47 ++++++++++++++++++++++++ cli/src/helpers/stream_mode_helpers.rs | 33 +++++++++++++++-- cli/src/main.rs | 11 ++++-- cli/src/tdes_cbc_cmd.rs | 14 +++++-- cli/src/tdes_cfb8_cmd.rs | 11 +++++- cli/src/tdes_cfb_cmd.rs | 11 +++++- cli/src/tdes_ctr_cmd.rs | 11 +++++- cli/src/tdes_ecb_cmd.rs | 11 +++++- cli/tests/tdes_cli_tests.rs | 28 +++++++------- cli/tests/test_tdes.sh | 51 ++++++++++++++++++++++++++ 10 files changed, 198 insertions(+), 30 deletions(-) create mode 100755 cli/tests/test_tdes.sh diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index 2ea2756e5..eb3dc95cb 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -283,10 +283,49 @@ pub(crate) fn start_encryptor( /// 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( + enc: E, + generated_iv: Option<&[u8; INIT_DATA_LEN]>, + output_hex: bool, + aligned_mode: Option<&str>, +) where + E: SymmetricCipherEncryptor, +{ + encrypt_stream_bounded::( + enc, generated_iv, output_hex, aligned_mode, None, + ) +} + +/// A bound on how much one invocation may encrypt under its key, for an algorithm whose +/// standard limits the data a key may protect: the command refuses input past `max_bytes` with +/// `message` on stderr and an error exit, before any byte past the bound is processed. +pub(crate) struct InputLimit { + pub(crate) max_bytes: u64, + pub(crate) message: &'static str, +} + +/// SP 800-67 Rev 2 Sec 3.4: "One key bundle shall not be used to apply cryptographic protection +/// (e.g., encrypt) more than 2^20 64-bit data blocks", 8 MiB. The command is the one place that +/// sees a bundle's whole use within an invocation, so it holds the input to that; it cannot count +/// across invocations. +pub(crate) const TDEA_LIMIT: InputLimit = InputLimit { + max_bytes: 1 << 23, + message: "TDEA: one key bundle may protect at most 2^20 blocks (8 MiB; SP 800-67 Rev 2 Sec \ + 3.4), and the input exceeds that", +}; + +/// The same bound for a padded mode, whose padding adds up to one block: one byte less, so the +/// padded message still fits. +pub(crate) const TDEA_LIMIT_PADDED: InputLimit = + InputLimit { max_bytes: (1 << 23) - 1, message: TDEA_LIMIT.message }; + +/// [`encrypt_stream`] under an [`InputLimit`]: the running input total is checked against the +/// bound before each chunk is encrypted, so nothing past it is ever written. +pub(crate) fn encrypt_stream_bounded( mut enc: E, generated_iv: Option<&[u8; INIT_DATA_LEN]>, output_hex: bool, aligned_mode: Option<&str>, + limit: Option<&InputLimit>, ) where E: SymmetricCipherEncryptor, { @@ -298,7 +337,15 @@ pub(crate) fn encrypt_stream limit.max_bytes { + eprintln!("Error: {}", limit.message); + exit(-1); + } + } // Cannot fail: `out` has room for every whole block of `data` plus the fewer than // `BLOCK_LEN` bytes held over from the last call, and none of these modes has a per-IV // data limit. diff --git a/cli/src/helpers/stream_mode_helpers.rs b/cli/src/helpers/stream_mode_helpers.rs index d26a272a7..7b32a3f44 100644 --- a/cli/src/helpers/stream_mode_helpers.rs +++ b/cli/src/helpers/stream_mode_helpers.rs @@ -28,7 +28,7 @@ //! stdin is read as binary so the commands compose in a pipeline. `-x` renders the *output* as hex. //! For hex input, pipe through `hex-decode` first. -use crate::helpers::block_mode_helpers::{CHUNK_LEN, CipherDirection, start_encryptor}; +use crate::helpers::block_mode_helpers::{CHUNK_LEN, CipherDirection, InputLimit, start_encryptor}; use crate::helpers::{flush_stdout, write_bytes_or_hex, write_stdout}; use bouncycastle::core::symmetric_key::{SymmetricDecryptionKey, SymmetricEncryptionKey}; use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; @@ -37,13 +37,16 @@ use std::io::Read; use std::process::exit; /// Encrypts stdin to stdout under the key's stream mode. With no `iv`, the IV is generated and -/// written first; with one, it is used and the output is ciphertext only. +/// written first; with one, it is used and the output is ciphertext only. Under an +/// [`InputLimit`], the running input total is checked against the bound before each chunk is +/// encrypted, so nothing past it is ever written. /// /// `INIT_DATA_LEN` is the mode's: one block for CFB and CFB8, 12 bytes for CTR. pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( key: &'k K, iv: Option<[u8; INIT_DATA_LEN]>, output_hex: bool, + limit: Option<&InputLimit>, ) where K: SymmetricEncryptionKey, K::Encryptor<'k>: StreamCipherEncryptor, @@ -57,7 +60,15 @@ pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: u } // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + let mut seen: u64 = 0; stream(|data| { + seen += data.len() as u64; + if let Some(limit) = limit { + if seen > limit.max_bytes { + eprintln!("Error: {}", limit.message); + exit(-1); + } + } // Cannot fail: neither CFB nor CFB8 has a per-IV data limit. // CFB and CFB8 cannot fail here; CTR can, once its counter is exhausted, which is a real // limit a long enough stream reaches rather than a bug. @@ -151,10 +162,26 @@ pub(crate) fn run_stream_mode<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: + SymmetricDecryptionKey, K::Encryptor<'k>: StreamCipherEncryptor, K::Decryptor<'k>: StreamCipherDecryptor, +{ + run_stream_mode_bounded::(action, key, iv, output_hex, None) +} + +/// [`run_stream_mode`] with an [`InputLimit`] on the encrypting direction. +pub(crate) fn run_stream_mode_bounded<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: usize>( + action: &CipherDirection, + key: &'k K, + iv: Option<[u8; INIT_DATA_LEN]>, + output_hex: bool, + limit: Option<&InputLimit>, +) where + K: SymmetricEncryptionKey + + SymmetricDecryptionKey, + K::Encryptor<'k>: StreamCipherEncryptor, + K::Decryptor<'k>: StreamCipherDecryptor, { match action { CipherDirection::Encrypt => { - encrypt_stream::(key, iv, output_hex) + encrypt_stream::(key, iv, output_hex, limit) } CipherDirection::Decrypt => { decrypt_stream::(key, iv, output_hex) diff --git a/cli/src/main.rs b/cli/src/main.rs index e4b5ffb0a..11ab08375 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2384,11 +2384,13 @@ enum Subcommands { /// 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. + /// is a weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). Two-key TDEA decrypts through the + /// `tdes2-*` commands. /// /// WARNING: CBC provides confidentiality only. It does not detect tampering, and neither the /// ciphertext nor the IV is authenticated. Do not decrypt data you have not authenticated - /// separately. One key bundle must not encrypt more than 2^20 blocks (8 MiB) in total. + /// separately. One key bundle must not encrypt more than 2^20 blocks (8 MiB) in total: + /// `encrypt` refuses longer input, and counting across invocations is the caller's. /// /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. @@ -2586,7 +2588,10 @@ enum Subcommands { /// 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. + /// decryption for legacy use, so `encrypt` does not exist for this command. That is a guard + /// against accidental encryption: `tdes2-cfb` and `tdes2-ctr` decryption are the forward + /// cipher, which a determined caller can turn into encryption, so a deployment that must rule + /// that out should expose only `tdes2-cbc` and `tdes2-ecb`. /// /// The first 8 bytes of input are the IV, as the `tdes-cbc decrypt` convention, unless `--iv` /// or `--iv-file` supplies it; the rest is PKCS7-padded ciphertext, as `tdes-cbc` writes it. diff --git a/cli/src/tdes_cbc_cmd.rs b/cli/src/tdes_cbc_cmd.rs index e79453290..1ce4d6f10 100644 --- a/cli/src/tdes_cbc_cmd.rs +++ b/cli/src/tdes_cbc_cmd.rs @@ -12,10 +12,12 @@ //! //! 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. +//! (8 MiB) in total (SP 800-67 Rev 2 Sec 3.4): `encrypt` refuses input past that in one +//! invocation, and nothing is counted across invocations, which is the caller's to track. use crate::helpers::block_mode_helpers::{ - CipherDirection, decrypt_stream, encrypt_stream, load_iv, load_key_object, start_encryptor, + CipherDirection, TDEA_LIMIT_PADDED, decrypt_stream, encrypt_stream_bounded, load_iv, + load_key_object, start_encryptor, }; use bouncycastle::cipher::modes::CBC; use bouncycastle::cipher::padding::PKCS7; @@ -35,7 +37,13 @@ pub(crate) fn tdes_cbc_cmd( 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) + encrypt_stream_bounded::<_, BLOCK_LEN, BLOCK_LEN>( + enc, + generated_iv.as_ref(), + output_hex, + None, + Some(&TDEA_LIMIT_PADDED), + ) } CipherDirection::Decrypt => decrypt_stream::<_, BLOCK_LEN, BLOCK_LEN>( iv, diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs index 3582c4f95..f853dcc6f 100644 --- a/cli/src/tdes_cfb8_cmd.rs +++ b/cli/src/tdes_cfb8_cmd.rs @@ -19,8 +19,9 @@ //! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see //! the `bouncycastle-tdes` crate docs. +use crate::helpers::block_mode_helpers::TDEA_LIMIT; use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; -use crate::helpers::stream_mode_helpers::run_stream_mode; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; use bouncycastle::cipher::modes::CFB8; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; @@ -33,5 +34,11 @@ pub(crate) fn tdes_cfb8_cmd( 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) + run_stream_mode_bounded::<_, KEY_LEN, BLOCK_LEN>( + action, + &key, + load_iv(iv, iv_file), + output_hex, + Some(&TDEA_LIMIT), + ) } diff --git a/cli/src/tdes_cfb_cmd.rs b/cli/src/tdes_cfb_cmd.rs index 63012921d..1f3edfe46 100644 --- a/cli/src/tdes_cfb_cmd.rs +++ b/cli/src/tdes_cfb_cmd.rs @@ -20,8 +20,9 @@ //! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see //! the `bouncycastle-tdes` crate docs. +use crate::helpers::block_mode_helpers::TDEA_LIMIT; use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; -use crate::helpers::stream_mode_helpers::run_stream_mode; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; use bouncycastle::cipher::modes::CFB; use bouncycastle::tdes::{BLOCK_LEN, KEY_LEN, TDES3Key}; @@ -34,5 +35,11 @@ pub(crate) fn tdes_cfb_cmd( 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) + run_stream_mode_bounded::<_, KEY_LEN, BLOCK_LEN>( + action, + &key, + load_iv(iv, iv_file), + output_hex, + Some(&TDEA_LIMIT), + ) } diff --git a/cli/src/tdes_ctr_cmd.rs b/cli/src/tdes_ctr_cmd.rs index ecd19310f..2802f903c 100644 --- a/cli/src/tdes_ctr_cmd.rs +++ b/cli/src/tdes_ctr_cmd.rs @@ -38,8 +38,9 @@ //! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see //! the `bouncycastle-tdes` crate docs. +use crate::helpers::block_mode_helpers::TDEA_LIMIT; use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; -use crate::helpers::stream_mode_helpers::run_stream_mode; +use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; use bouncycastle::cipher::modes::CTR; use bouncycastle::tdes::{BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES3Key}; @@ -54,5 +55,11 @@ pub(crate) fn tdes_ctr_cmd( 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) + run_stream_mode_bounded::<_, KEY_LEN, CTR_NONCE_LEN>( + action, + &key, + nonce, + output_hex, + Some(&TDEA_LIMIT), + ) } diff --git a/cli/src/tdes_ecb_cmd.rs b/cli/src/tdes_ecb_cmd.rs index af55e9bd3..b9e66557a 100644 --- a/cli/src/tdes_ecb_cmd.rs +++ b/cli/src/tdes_ecb_cmd.rs @@ -20,7 +20,8 @@ //! the `bouncycastle-tdes` crate docs. use crate::helpers::block_mode_helpers::{ - CipherDirection, decrypt_stream, encrypt_stream, load_key_object, start_encryptor, + CipherDirection, TDEA_LIMIT, decrypt_stream, encrypt_stream_bounded, load_key_object, + start_encryptor, }; use bouncycastle::cipher::modes::hazmat::ecb::ECB; use bouncycastle::cipher::padding::NoPadding; @@ -41,7 +42,13 @@ pub(crate) fn tdes_ecb_cmd( 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)) + encrypt_stream_bounded::<_, 0, BLOCK_LEN>( + enc, + None, + output_hex, + Some(MODE), + Some(&TDEA_LIMIT), + ) } CipherDirection::Decrypt => decrypt_stream::<_, 0, BLOCK_LEN>( None, diff --git a/cli/tests/tdes_cli_tests.rs b/cli/tests/tdes_cli_tests.rs index 6571bb69e..76605bad4 100644 --- a/cli/tests/tdes_cli_tests.rs +++ b/cli/tests/tdes_cli_tests.rs @@ -13,7 +13,7 @@ //! the engine. use std::io::{ErrorKind, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::{Command, Output, Stdio}; /// The path to the binary under test, resolved by cargo. @@ -104,24 +104,26 @@ struct Vector { 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); +/// The text of a TDES CAVP response file, or `None` (with a warning) when `bc-test-data` is not +/// cloned beside this repository. When it is and the file is missing, this panics rather than +/// letting the test pass vacuously, as the framework's `bc_test_data` loader does. +fn cavp_file(file: &str) -> Option { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bc-test-data"); + if !root.is_dir() { + println!( + "WARNING: bc-test-data not found at {}; the CAVP vector tests are skipped", + root.display() + ); + return None; } - println!( - "WARNING: bc-test-data not found at {}; the CAVP vector tests are skipped", - dir.display() - ); - None + let path = root.join("crypto/aes_tdes_vectors/TDES").join(file); + Some(std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()))) } /// `COUNT = 0` of `[section]` in `file`, or `None` without the data. The key is `KEY1 || KEY2` /// for a two-key file (whose `KEY3 = KEY1`) and `KEY1 || KEY2 || KEY3` otherwise. fn vector(file: &str, section: &str) -> Option { - let text = std::fs::read_to_string(cavp_dir()?.join(file)).expect("readable response file"); + let text = cavp_file(file)?; let body = text.split(&format!("[{section}]")).nth(1).expect("the section"); let case = body.split("COUNT = 0").nth(1).expect("COUNT = 0"); let case = case.split("COUNT = 1").next().expect("one case"); diff --git a/cli/tests/test_tdes.sh b/cli/tests/test_tdes.sh new file mode 100755 index 000000000..f718b7ee7 --- /dev/null +++ b/cli/tests/test_tdes.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# The tdes-* subcommands' data limit, end to end through the binary. SP 800-67 Rev 2 Sec 3.4 +# allows one key bundle 2^20 64-bit blocks (8 MiB) of protection; `encrypt` refuses input past +# that with the usual `Error:` line, before any byte past the bound is processed. CBC's PKCS7 +# padding adds up to a block, so its bound is one byte lower; the stream modes take exactly +# 8 MiB. CTR never reaches it: its 6-byte nonce leaves a 16-bit counter, so the library's own +# keystream limit (2^16 blocks, 512 KiB) binds first, as `DataLimitExceeded`. Decryption is not +# limited. This lives here rather than in `tdes_cli_tests.rs` because 8 MiB through a debug build +# of the bit-sliced engine takes a minute or two per run, and this file makes five. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +KEY=0123456789abcdef23456789abcdef01456789abcdef0123 +LIMIT=$((1 << 23)) + +# `zeros N` writes N zero bytes. +zeros() { head -c "$1" /dev/zero; } + +test_cbc_refuses_a_padded_message_past_the_limit() { + expect_fail "8 MiB pads to 2^20 + 1 blocks" \ + "$BC_RUST" tdes-cbc -d encrypt --key "$KEY" < <(zeros "$LIMIT") + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cbc_takes_a_message_that_pads_to_the_limit() { + expect_ok "8 MiB less one byte pads to exactly 2^20 blocks" \ + "$BC_RUST" tdes-cbc -d encrypt --key "$KEY" < <(zeros $((LIMIT - 1))) >/dev/null +} + +test_cfb_modes_refuse_one_byte_past_the_limit() { + local cmd + for cmd in tdes-cfb tdes-cfb8; do + expect_fail "$cmd: 2^20 blocks plus a byte" \ + "$BC_RUST" "$cmd" -d encrypt --key "$KEY" < <(zeros $((LIMIT + 1))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" + done +} + +test_ctr_runs_out_of_counter_first() { + expect_fail "tdes-ctr: 2^16 blocks plus a byte exhausts the 16-bit counter" \ + "$BC_RUST" tdes-ctr -d encrypt --key "$KEY" < <(zeros $(((1 << 19) + 1))) + assert_stderr_has "DataLimitExceeded" +} + +test_decrypt_is_not_limited() { + # 2^20 + 1 blocks of ciphertext decrypt (ECB, no padding), since the limit is on protecting. + expect_ok "tdes-ecb decrypt past the limit" \ + "$BC_RUST" tdes-ecb -d decrypt --key "$KEY" < <(zeros $((LIMIT + 8))) >/dev/null +} + +run_all From 9a00e1a201faa8ed756ca2fc680b70c6da98bbb8 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 09:33:12 +1100 Subject: [PATCH 26/32] cipher: TKW, the TDEA key wrap of SP 800-38F Sec 7, as a mode over any 64-bit block permutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapping function W and its inverse become generic over the block and semiblock widths, with the step counter XORed in as a big-endian semiblock, so TW and TW⁻¹ (Algorithms 7 and 8) are the same loop at 8 and 4 bytes; KW and KWP call them at 16 and 8 as before. The TKW mode type and the Tkw worker follow Kw: ICV3, Table 1's 2 to 2^28 - 1 semiblock limits, every check the same DecryptionFailed. A permutation that decrypts only (BlockPermutation::DECRYPT_ONLY) refuses to make a wrapper at compile time, in wrap_init, as the encrypting modes refuse their encryptors; its unwrapper works. Tkw reports the permutation's KW_ALG_NAME, since a permutation has one block length and so one key-wrap algorithm, rather than adding a name to ModeNames that every permutation would have to supply. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/kw.rs | 62 ++++--- crypto/cipher/src/modes/kwp.rs | 4 +- crypto/cipher/src/modes/mod.rs | 12 +- crypto/cipher/src/modes/tkw.rs | 308 +++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 29 deletions(-) create mode 100644 crypto/cipher/src/modes/tkw.rs diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index e7f8ad1c6..62ead5306 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -169,71 +169,87 @@ pub(crate) const fn kw_wrapped_len(len: usize) -> usize { len.saturating_add(SEMIBLOCK_LEN) } -/// The wrapping function W (SP 800-38F Algorithm 1), in place over `S = a || r`. +/// The wrapping function W (SP 800-38F Algorithm 1), in place over `S = a || r`, with +/// `SEMI`-byte semiblocks over a `BLOCK`-byte permutation: 8 and 16 for KW and KWP, 4 and 8 for +/// TKW, whose TW (Algorithm 7) is this same loop with `[t]32` in place of `[t]64`. /// /// `a` holds `S1` and `r` holds `S2 .. Sn`, so `r.len() == n - 1`, which must be at least 2 /// (Algorithm 1 is defined for `n >= 3`). On return `a` holds `C1` and `r` holds `C2 .. Cn`. /// /// Runs in place rather than through the shift register of the spec's step 2; see the module /// docs for why the two are the same computation. -pub(crate) fn w

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

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

Mode

for TKW +where + P: BlockPermutation + ModeNames + Clone, +{ + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl KeyWrapMode for TKW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Wrapper<'k> + = Tkw<'k, P> + where + P: 'k; + + fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; + Tkw::from_perm(state) + } +} + +impl KeyUnwrapMode for TKW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Unwrapper<'k> + = Tkw<'k, P> + where + P: 'k; + + fn unwrap_init<'k>(state: &'k Self::KeyState) -> Self::Unwrapper<'k> { + Tkw::from_perm(state) + } +} + +/// TKW over any 64-bit permutation that impls [`BlockPermutation`]: the one-shot worker a +/// [`TKW`]-bound key hands out, borrowing the keyed permutation for `'k`, as +/// [`Kw`](super::kw::Kw) does. It reports the permutation's [`ModeNames::KW_ALG_NAME`], since a +/// permutation has one block length and so one key-wrap algorithm. See the module docs. +pub struct Tkw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + perm: &'k P, +} + +impl<'k, P> Tkw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// Over an already-keyed permutation, which the worker borrows: what [`TKW`]'s mode impls + /// build for a key's roles. + pub(crate) fn from_perm(perm: &'k P) -> Self { + Self { perm } + } +} + +impl<'k, P> Algorithm for Tkw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// The permutation's name for its key wrap, [`ModeNames::KW_ALG_NAME`]. + const ALG_NAME: &'static str = P::KW_ALG_NAME; + /// TKW does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl<'k, P> KeyWrapper for Tkw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// TKW-AE with the lengths fixed at compile time. `KEY_LEN` must be a valid TKW plaintext + /// length and `CT_LEN` one semiblock more; anything else fails to compile. + fn wrap_key_out( + &self, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + const { + assert!( + tkw_plaintext_len_is_valid(KEY_LEN), + "TKW wraps 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == tkw_wrapped_len(KEY_LEN), + "a TKW ciphertext is exactly one 4-byte semiblock longer than its plaintext" + ); + } + self.wrap_out(key, ciphertext) + } + + /// One semiblock more than the plaintext. + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + tkw_wrapped_len(plaintext_len) + } + + /// TKW-AE (SP 800-38F Algorithm 9). + fn wrap_out( + &self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + // Nothing stale may survive a failure, so the whole buffer starts from zero. + ciphertext.fill(0); + let ct_len = tkw_wrapped_len(plaintext.len()); + if ciphertext.len() < ct_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); + } + if !tkw_plaintext_len_is_valid(plaintext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "TKW wraps 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)", + )); + } + + // Steps 1-2: S = ICV3 || P, assembled in the caller's buffer so that the plaintext is + // copied exactly once and TW can run on it in place. + let (a, r) = ciphertext[..ct_len].split_at_mut(TKW_SEMIBLOCK_LEN); + let (a, _) = a.as_chunks_mut::(); + let a = &mut a[0]; + *a = ICV3; + r.copy_from_slice(plaintext); + let (r, _) = r.as_chunks_mut::(); + // Step 3: C = TW(S). + w::(self.perm, a, r); + Ok(ct_len) + } +} + +impl<'k, P> KeyUnwrapper for Tkw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// TKW-AD with the lengths fixed at compile time; the same pairs as + /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. + fn unwrap_key_out( + &self, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + const { + assert!( + tkw_plaintext_len_is_valid(KEY_LEN), + "TKW unwraps to 2 to 2^28 - 1 whole 4-byte semiblocks (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == tkw_wrapped_len(KEY_LEN), + "a TKW ciphertext is exactly one 4-byte semiblock longer than its plaintext" + ); + } + // `key` is exactly the n-1 semiblocks the run-time method writes into, and that method + // scrubs the buffer on every failure it can reach from here. + self.unwrap_out(ciphertext, &mut key[..]) + } + + /// One semiblock less than the ciphertext: exact for TKW. + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TKW_SEMIBLOCK_LEN) + } + + /// TKW-AD (SP 800-38F Algorithm 10). + fn unwrap_out( + &self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + // Nothing this function writes may survive a failure, so start from zero and only ever + // return `Ok` with the buffer fully written. + plaintext.fill(0); + let pt_len = self.unwrap_out_max_len(ciphertext.len()); + if plaintext.len() < pt_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); + } + if !tkw_ciphertext_len_is_valid(ciphertext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "a TKW ciphertext is 3 to 2^28 whole 4-byte semiblocks (SP 800-38F Table 1)", + )); + } + + // Step 3: S = TW⁻¹(C). C1 goes to a scratch semiblock that is scrubbed on return; C2..Cn + // go straight into the caller's buffer, which is exactly n-1 semiblocks, so TW⁻¹ runs + // in place and the recovered plaintext is never copied. + let mut a: Secret<[u8; TKW_SEMIBLOCK_LEN]> = Secret::new(); + a.copy_from_slice(&ciphertext[..TKW_SEMIBLOCK_LEN]); + let r = &mut plaintext[..pt_len]; + r.copy_from_slice(&ciphertext[TKW_SEMIBLOCK_LEN..]); + let (r, _) = r.as_chunks_mut::(); + w_inv::(self.perm, &mut a, r); + + // Step 4: if MSB32(S) != ICV3, FAIL. Compared in constant time, and the buffer is + // scrubbed so that a caller who ignores the `Result` is not left holding the + // unauthenticated bytes. + if !ct_eq_bytes(&*a, &ICV3) { + plaintext.fill(0); + return Err(SymmetricCipherError::DecryptionFailed); + } + // Step 5: P = LSB32(n-1)(S), which is already in place. + Ok(pt_len) + } +} + +#[cfg(test)] +mod tests { + //! The 2^28 upper bounds of SP 800-38F Table 1, which no buffer can reach: the last valid + //! plaintext is 2^28 - 1 semiblocks, the last valid ciphertext 2^28. The lower bounds and + //! the alignment rule are pinned through the key in `bouncycastle-tdes`. + + use super::*; + + #[test] + fn the_table_1_upper_bounds() { + const MAX_PT: usize = ((1usize << 28) - 1) * TKW_SEMIBLOCK_LEN; + assert!(tkw_plaintext_len_is_valid(MAX_PT)); + assert!(!tkw_plaintext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN / 2)); + assert!(!tkw_plaintext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN)); + assert!(tkw_ciphertext_len_is_valid(MAX_PT + TKW_SEMIBLOCK_LEN)); + assert!(!tkw_ciphertext_len_is_valid(MAX_PT + 2 * TKW_SEMIBLOCK_LEN)); + } +} From e3622a3e542dec57855474e4c1e16723554df73a Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 09:33:12 +1100 Subject: [PATCH 27/32] tdes, cli: TDES3Key wraps keys and TDES2Key unwraps them; the TDES_TKW / TDES2_TKW aliases name the workers The key gets the two key-wrap roles, forwarded to the mode as the others are; the engines name the mode TDES_TKW and TDES2_TKW, and the two-key alias has only the unwrapping half, with a compile_fail doctest on its wrapper as the cipher modes have on their encryptors. Tests: the NIST CAVP TKW_AE and TKW_AD sample files (500 trials each, 100 forgeries) from bc-test-data, where they are new; Algorithm 9 transcribed with its shift register over the raw engine, for both bundles, which is the only known answer for two-key unwrapping; the shared key-wrap suite, the construction suite and the alias names. CLI: tdes-tkw (-d encrypt|decrypt) and the unwrap-only tdes2-tkw over the aes*-kw runner, which splits into run and run_with so that the unwrap-only command never instantiates a wrapper for its key, with a subprocess test on the CAVP answers. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_kw_cmd.rs | 30 +++-- cli/src/main.rs | 62 +++++++++ cli/src/tdes_tkw_cmd.rs | 40 ++++++ cli/tests/tdes_tkw_cli_tests.rs | 163 +++++++++++++++++++++++ crypto/tdes/src/hazmat/tdes.rs | 3 +- crypto/tdes/src/hazmat/tdes2.rs | 3 +- crypto/tdes/src/key.rs | 37 ++++- crypto/tdes/src/lib.rs | 11 +- crypto/tdes/src/tkw.rs | 91 +++++++++++++ crypto/tdes/tests/alg_name_tests.rs | 13 +- crypto/tdes/tests/mode_key_tests.rs | 15 ++- crypto/tdes/tests/tkw_cavp_tests.rs | 118 ++++++++++++++++ crypto/tdes/tests/tkw_reference_tests.rs | 68 ++++++++++ 13 files changed, 633 insertions(+), 21 deletions(-) create mode 100644 cli/src/tdes_tkw_cmd.rs create mode 100644 cli/tests/tdes_tkw_cli_tests.rs create mode 100644 crypto/tdes/src/tkw.rs create mode 100644 crypto/tdes/tests/tkw_cavp_tests.rs create mode 100644 crypto/tdes/tests/tkw_reference_tests.rs diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs index 56d5ec7b1..b1e8cf15f 100644 --- a/cli/src/aes_kw_cmd.rs +++ b/cli/src/aes_kw_cmd.rs @@ -138,23 +138,35 @@ pub(crate) fn aes256_kwp_cmd( } /// Reads all of stdin, wraps or unwraps it under `kek`, and writes the result. `alg` names the -/// algorithm in error messages. -fn run(direction: &CipherDirection, kek: &K, output_hex: bool, alg: &str) -where +/// algorithm in error messages. Shared with the other block ciphers' key-wrap commands. +pub(crate) fn run( + direction: &CipherDirection, + kek: &K, + output_hex: bool, + alg: &str, +) where K: KeyWrappingKey + KeyUnwrappingKey, { + match direction { + CipherDirection::Encrypt => run_with(|input| kek.wrap(input), output_hex, alg), + CipherDirection::Decrypt => run_with(|input| kek.unwrap(input), output_hex, alg), + } +} + +/// Reads all of stdin, applies `op` to it and writes the result; the unwrap-only commands pass +/// their key's `unwrap` alone, so that no wrapper is ever instantiated for their key type. +pub(crate) fn run_with( + op: impl FnOnce(&[u8]) -> Result, SymmetricCipherError>, + output_hex: bool, + alg: &str, +) { let mut input = Vec::new(); if let Err(e) = std::io::stdin().read_to_end(&mut input) { eprintln!("Error: couldn't read stdin: {e}"); exit(-1); } - let result = match direction { - CipherDirection::Encrypt => kek.wrap(&input), - CipherDirection::Decrypt => kek.unwrap(&input), - }; - - match result { + match op(&input) { Ok(output) => write_bytes_or_hex(&output, output_hex), Err(SymmetricCipherError::DecryptionFailed) => { eprintln!( diff --git a/cli/src/main.rs b/cli/src/main.rs index abc3081df..1e65bca70 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -31,6 +31,7 @@ mod tdes_cfb8_cmd; mod tdes_cfb_cmd; mod tdes_ctr_cmd; mod tdes_ecb_cmd; +mod tdes_tkw_cmd; use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; @@ -2353,6 +2354,61 @@ enum Subcommands { x: bool, }, + /// Triple DES Key Wrap, TKW (NIST SP 800-38F Sec 7): `-d encrypt` wraps stdin to stdout, + /// `-d decrypt` unwraps it. LEGACY ONLY: SP 800-38F approves TKW to support legacy systems + /// and says it should not be used for new applications. + /// + /// The same command as `aes128-kw` over TDEA at half the width: input to `encrypt` is a whole + /// number of 4-byte blocks, at least 8 bytes, and the output is 4 bytes longer; input to + /// `decrypt` is a whole number of 4-byte blocks, at least 12 bytes. There is NO IV, and the + /// whole input is read before anything is written. See `aes128-kw` for the rest. + /// + /// The key is 24 bytes, KEY1 || KEY2 || KEY3, with three distinct component keys none of which + /// is a weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). Two-key TDEA unwraps through + /// `tdes2-tkw`. + /// + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + TDES_TKW { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte TDES key-encryption key bundle in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte TDES key-encryption key bundle (raw bytes; hex with + /// --hex-input). If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Two-key Triple DES Key Wrap (2TDEA TKW), UNWRAPPING ONLY. See `tdes2-cbc` for why there is + /// no `encrypt`, and `tdes-tkw` for the input rules. + TDES2_TKW { + #[arg(short = 'd', long = "direction")] + action: DecryptOnlyAction, + + /// The 16-byte two-key TDES bundle in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte two-key TDES bundle (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-KEM-512 key encapsulation algorithm. MLKEM512 { action: mlkem_cmd::MLKEMAction, @@ -3575,6 +3631,12 @@ fn run() { Some(Subcommands::AES256_KWP { direction, key, key_file, x }) => { aes_kw_cmd::aes256_kwp_cmd(direction, key, key_file, *x); } + Some(Subcommands::TDES_TKW { direction, key, key_file, x }) => { + tdes_tkw_cmd::tdes_tkw_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES2_TKW { action, key, key_file, x }) => { + tdes_tkw_cmd::tdes2_tkw_cmd(action, key, key_file, *x); + } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); } diff --git a/cli/src/tdes_tkw_cmd.rs b/cli/src/tdes_tkw_cmd.rs new file mode 100644 index 000000000..5fd3af47d --- /dev/null +++ b/cli/src/tdes_tkw_cmd.rs @@ -0,0 +1,40 @@ +//! TDEA key wrap: `tdes-tkw -d encrypt|decrypt` (three-key, both directions) and +//! `tdes2-tkw -d decrypt` (two-key, unwrapping only), stdin to stdout. +//! +//! They are the `aes*-kw` command over TDEA (NIST SP 800-38F Sec 7, TKW), so everything in +//! [`crate::aes_kw_cmd`] -- the whole-input reading, the absence of an IV, the fail-closed unwrap +//! -- applies unchanged, at half the width: the input is a whole number of 4-byte semiblocks, at +//! least 8 bytes, and the output is 4 bytes longer. TKW is approved for legacy systems only +//! (SP 800-38F Sec 8), and two-key TDEA may not apply protection at all, so `tdes2-tkw` has no +//! `encrypt`, as the other `tdes2-*` commands have none. + +use crate::aes_kw_cmd::{run, run_with}; +use crate::helpers::block_mode_helpers::{CipherDirection, DecryptOnlyAction, load_key_object}; +use bouncycastle::cipher::modes::TKW; +use bouncycastle::core::symmetric_key::KeyUnwrappingKey; +use bouncycastle::tdes::{KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +pub(crate) fn tdes_tkw_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, KEY_LEN>( + direction, + &load_key_object::, KEY_LEN>(key, key_file, "TDES"), + output_hex, + "TDES-TKW", + ); +} + +pub(crate) fn tdes2_tkw_cmd( + action: &DecryptOnlyAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let DecryptOnlyAction::Decrypt = action; + let kek = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); + run_with(|input| kek.unwrap(input), output_hex, "two-key TDES-TKW"); +} diff --git a/cli/tests/tdes_tkw_cli_tests.rs b/cli/tests/tdes_tkw_cli_tests.rs new file mode 100644 index 000000000..7a3944c0b --- /dev/null +++ b/cli/tests/tdes_tkw_cli_tests.rs @@ -0,0 +1,163 @@ +//! Tests for the `tdes-tkw` and `tdes2-tkw` subcommands. +//! +//! They share everything but the key type and the semiblock width with the `aes*-kw` commands, +//! whose `test_aes_kw.sh` covers the command-line contract exhaustively. This file pins what is +//! specific to TKW: the CAVP known answers in both directions, a CAVP forgery, the 4-byte +//! alignment rule, and that the two-key command unwraps only. +//! +//! The known answers are `COUNT = 0` of the 64- and 128-bit sections of the NIST CAVP `TKW_AE.txt` +//! sample file, and the first `FAIL` of `TKW_AD.txt`, read from `../bc-test-data` at test time; +//! without it those tests print a warning and pass, as the crate's suite does. + +use std::io::{ErrorKind, Write}; +use std::path::Path; +use std::process::{Command, Output, Stdio}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// A three-key bundle of distinct, non-weak components, for the tests that need no vector. +const KEY_3: &str = "0123456789abcdef23456789abcdef01456789abcdef0123"; +/// A two-key bundle of distinct, non-weak components. +const KEY_2: &str = "0123456789abcdef23456789abcdef01"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + // A command that exits before reading stdin makes this write race its exit; that is the + // expected outcome for the error-path tests, which assert on the status and stderr only. + match child.stdin.as_mut().expect("stdin piped").write_all(stdin_bytes) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + child.wait_with_output().expect("failed to wait for bc-rust") +} + +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + assert!(out.stdout.is_empty(), "a rejected command must write nothing: {args:?}"); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// The text of a CAVP sample file, or `None` (with a warning) when `bc-test-data` is not cloned +/// beside this repository. +fn cavp_file(file: &str) -> Option { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../bc-test-data"); + let path = root.join("crypto/aes_tdes_vectors/TDES").join(file); + if !root.exists() { + println!( + "WARNING: bc-test-data not found at {}; the CAVP tests are skipped", + root.display() + ); + return None; + } + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + // The CAVS files have CRLF line ends. + Some(text.replace('\r', "")) +} + +/// `(K, P, C)` of `COUNT = 0` in the `[PLAINTEXT LENGTH = bits]` section of `file`; `P` is empty +/// for a `FAIL` case. +fn case(file: &str, bits: u32, count: u32) -> Option<(String, String, String)> { + let text = cavp_file(file)?; + let section = text.split(&format!("[PLAINTEXT LENGTH = {bits}]")).nth(1).expect("the section"); + let body = section.split(&format!("COUNT = {count}\n")).nth(1).expect("the case"); + let body = body.split("COUNT = ").next().unwrap(); + let field = |name: &str| -> String { + body.lines() + .map(str::trim) + .find_map(|l| l.strip_prefix(&format!("{name} = "))) + .unwrap_or_default() + .to_lowercase() + }; + Some((field("K"), field("P"), field("C"))) +} + +#[test] +fn tdes_tkw_matches_the_cavp_vectors_in_both_directions() { + for bits in [64, 128] { + let Some((k, p, c)) = case("TKW_AE.txt", bits, 0) else { return }; + let wrapped = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", &k], &unhex(&p)); + assert_eq!(tohex(&wrapped), c, "{bits}-bit wrap"); + let hex = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", &k, "-x"], &unhex(&p)); + assert_eq!(String::from_utf8_lossy(&hex).trim(), c, "{bits}-bit wrap -x"); + let unwrapped = run_ok(&["tdes-tkw", "-d", "decrypt", "--key", &k], &unhex(&c)); + assert_eq!(tohex(&unwrapped), p, "{bits}-bit unwrap"); + } +} + +#[test] +fn tdes_tkw_rejects_a_cavp_forgery() { + let Some(text) = cavp_file("TKW_AD.txt") else { return }; + // The first FAIL: its K and C are the two `NAME = value` lines just above it. + let before = text.split("\nFAIL").next().expect("a FAIL line"); + let mut lines = before.lines().rev().map(str::trim); + let c = lines.next().unwrap().strip_prefix("C = ").expect("C before FAIL").to_lowercase(); + let k = lines.next().unwrap().strip_prefix("K = ").expect("K before C").to_lowercase(); + let err = run_err(&["tdes-tkw", "-d", "decrypt", "--key", &k], &unhex(&c)); + assert!(err.contains("not an authentic"), "{err}"); +} + +#[test] +fn tkw_takes_whole_4_byte_semiblocks_and_at_least_two() { + let err = run_err(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 4]); + assert!(err.contains("4 bytes rejected"), "{err}"); + let err = run_err(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 10]); + assert!(err.contains("10 bytes rejected"), "{err}"); + let wrapped = run_ok(&["tdes-tkw", "-d", "encrypt", "--key", KEY_3], &[0x5Au8; 12]); + assert_eq!(wrapped.len(), 16, "12 bytes wrap to 16"); + let back = run_ok(&["tdes-tkw", "-d", "decrypt", "--key", KEY_3], &wrapped); + assert_eq!(back, [0x5Au8; 12]); +} + +#[test] +fn tdes2_tkw_unwraps_only() { + // `encrypt` is not a value of the two-key direction at all. + let err = run_err(&["tdes2-tkw", "-d", "encrypt", "--key", KEY_2], &[0x5Au8; 8]); + assert!(err.contains("encrypt"), "{err}"); + // A ciphertext this key never made is rejected, not unwrapped to garbage. + let err = run_err(&["tdes2-tkw", "-d", "decrypt", "--key", KEY_2], &[0x5Au8; 12]); + assert!(err.contains("not an authentic"), "{err}"); + // A three-key bundle is not a two-key one. + let err = run_err(&["tdes2-tkw", "-d", "decrypt", "--key", KEY_3], &[0x5Au8; 12]); + assert!(err.contains("16-byte key"), "{err}"); +} + +#[test] +fn the_subcommands_are_listed_in_help() { + let help = String::from_utf8(run_ok(&["--help"], b"")).unwrap(); + for cmd in ["tdes-tkw", "tdes2-tkw"] { + assert!(help.contains(cmd), "--help should list {cmd}"); + } +} diff --git a/crypto/tdes/src/hazmat/tdes.rs b/crypto/tdes/src/hazmat/tdes.rs index f69d30ed3..69a562ca7 100644 --- a/crypto/tdes/src/hazmat/tdes.rs +++ b/crypto/tdes/src/hazmat/tdes.rs @@ -239,7 +239,8 @@ impl ModeNames for TDESInternal { const ECB_ALG_NAME: &'static str = "TDES_ECB"; const GCM_ALG_NAME: &'static str = ""; const GMAC_ALG_NAME: &'static str = ""; - const KW_ALG_NAME: &'static str = ""; + /// TKW, the TDEA key wrap of SP 800-38F Sec 7, is the key wrap of a 64-bit block. + const KW_ALG_NAME: &'static str = "TDES_TKW"; const KWP_ALG_NAME: &'static str = ""; } diff --git a/crypto/tdes/src/hazmat/tdes2.rs b/crypto/tdes/src/hazmat/tdes2.rs index 0ceaf1d11..dd26becdd 100644 --- a/crypto/tdes/src/hazmat/tdes2.rs +++ b/crypto/tdes/src/hazmat/tdes2.rs @@ -189,7 +189,8 @@ impl ModeNames for TDES2Internal { const ECB_ALG_NAME: &'static str = "TDES2_ECB"; const GCM_ALG_NAME: &'static str = ""; const GMAC_ALG_NAME: &'static str = ""; - const KW_ALG_NAME: &'static str = ""; + /// TKW-AD only: the engine decrypts only, so no wrapper is made over it. + const KW_ALG_NAME: &'static str = "TDES2_TKW"; const KWP_ALG_NAME: &'static str = ""; } diff --git a/crypto/tdes/src/key.rs b/crypto/tdes/src/key.rs index 235ce3c13..591af9be4 100644 --- a/crypto/tdes/src/key.rs +++ b/crypto/tdes/src/key.rs @@ -6,12 +6,13 @@ use crate::hazmat::{TDES2Internal, TDESInternal}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, - EcbEncryptionKey, ElectronicCodeBook, Mode, SymmetricDecryptMode, SymmetricEncryptMode, + EcbEncryptionKey, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ - BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, SymmetricDecryptionKey, - SymmetricEncryptionKey, + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey, + SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; @@ -244,3 +245,33 @@ where M::decrypt_init(&self.state) } } + +impl KeyWrappingKey for TDESKey +where + P: TDESEngine, + M: KeyWrapMode, +{ + type Wrapper<'k> + = M::Wrapper<'k> + where + Self: 'k; + + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k> { + M::wrap_init(&self.state) + } +} + +impl KeyUnwrappingKey for TDESKey +where + P: TDESEngine, + M: KeyUnwrapMode, +{ + type Unwrapper<'k> + = M::Unwrapper<'k> + where + Self: 'k; + + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k> { + M::unwrap_init(&self.state) + } +} diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index 4b86a5bf4..0bc96dc82 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -20,9 +20,9 @@ //! 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. +//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher); and `TKW` for +//! TDEA Key Wrap (SP 800-38F Sec 7), legacy only. The aliases [`TDES_CBC`], [`TDES_CFB`], +//! [`TDES_CFB8`], [`TDES_CTR`], [`TDES_ECB`] and [`TDES_TKW`] name the workers the key hands out. //! //! SP 800-38A Appendix E is what makes these the standard TDEA modes: TECB, TCBC and TCFB of ANSI //! X9.52 "are equivalent to" ECB, CBC and CFB "with the TDEA as the underlying block cipher", and @@ -112,7 +112,8 @@ //! with the safe API alone (SP 800-38A defines the modes that way). A deployment that must rule //! two-key encryption out altogether should expose only the CBC and ECB decryptors. The aliases //! [`TDES2_CBC`], [`TDES2_CFB`], [`TDES2_CFB8`], [`TDES2_CTR`] and [`TDES2_ECB`] are those -//! decryptors, with the IV or nonce supplied by the caller as it was received: +//! decryptors, with the IV or nonce supplied by the caller as it was received, and [`TDES2_TKW`] +//! is the key unwrapper, with no wrapper beside it: //! //! ``` //! use bouncycastle_tdes::TDES2Key; @@ -307,6 +308,7 @@ pub mod hazmat; mod key; mod sbox; mod schedule; +mod tkw; /// The DEA block length in bytes: 64 bits (SP 800-67r2 Sec 2). pub const BLOCK_LEN: usize = 8; @@ -324,6 +326,7 @@ pub use cfb::{TDES_CFB, TDES2_CFB}; pub use cfb8::{TDES_CFB8, TDES2_CFB8}; pub use ctr::{CTR_NONCE_LEN, TDES_CTR, TDES2_CTR}; pub use key::{TDES2Key, TDES3Key, TDESEngine, TDESKey}; +pub use tkw::{TDES_TKW, TDES2_TKW}; // Imports needed for docs #[allow(unused_imports)] diff --git a/crypto/tdes/src/tkw.rs b/crypto/tdes/src/tkw.rs new file mode 100644 index 000000000..c9fea207b --- /dev/null +++ b/crypto/tdes/src/tkw.rs @@ -0,0 +1,91 @@ +//! Type aliases for TDEA Key Wrap, TKW (NIST SP 800-38F Sec 7). The docs are on [`TDES_TKW`], +//! which the crate root re-exports. + +use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::modes::Tkw; + +/// TDEA Key Wrap: the `Tkw` worker of `bouncycastle-cipher` over the three-key TDEA +/// permutation, which implements `KeyWrapper` and `KeyUnwrapper`, from a `TDES3Key` of +/// `KeyType::SymmetricCipherKey` material through `KeyWrappingKey` and `KeyUnwrappingKey`. It +/// wraps data that is a whole number of 4-byte semiblocks, at least 8 bytes, to one semiblock +/// more; the construction, the one-shots and the security notes are those of +/// [`bouncycastle_cipher::modes::tkw`], and the API is that of `AES_KW_128` in `bouncycastle-aes` +/// at half the width. +/// +/// SP 800-38F Sec 8: TKW is "approved to support legacy systems but should not be used for new +/// applications". A forgery passes its check with probability 2^-32 per attempt (Appendix A.3), +/// against 2^-64 for KW, and TDEA is itself a legacy algorithm; see the crate's security +/// considerations. SP 800-38F publishes no vectors for it; the NIST CAVP `TKW_AE` and `TKW_AD` +/// sample files are run from `bc-test-data`. +/// +/// # Usage Examples +/// +/// ``` +/// use bouncycastle_tdes::TDES3Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +/// +/// // Three distinct component keys; see `TDESInternal` for what a key bundle must satisfy. +/// let bytes: [u8; 24] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<24>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let kek = TDES3Key::::from_key_material(&material).expect("a valid key bundle"); +/// +/// // A 16-byte key wraps to 20 bytes; any other pair of lengths is a compile error. +/// let key_to_wrap = [0x5Au8; 16]; +/// let wrapped: [u8; 20] = kek.wrap_key(&key_to_wrap).expect("wrapping"); +/// assert_eq!(*kek.unwrap_key::<16, 20>(&wrapped).expect("unwrapping"), key_to_wrap); +/// +/// // Lengths known only at run time, through the allocating one-shots. +/// let wrapped = kek.wrap(&[0x5Au8; 24]).expect("wrapping"); +/// assert_eq!(kek.unwrap(&wrapped).expect("unwrapping"), [0x5Au8; 24]); +/// ``` +/// +/// # Memory Usage +/// +/// A key is the engine's expanded schedule, as for the other modes (see the crate's table). A +/// worker is the borrow of it, one pointer, and the wrapping function's scratch is one 8-byte +/// block on the stack; the data is wrapped and unwrapped in the caller's buffers. +#[allow(non_camel_case_types)] +pub type TDES_TKW<'k> = Tkw<'k, TDESInternal>; + +/// Two-key TDEA Key Wrap, unwrapping only: what a [`TDES2Key`](crate::TDES2Key) hands out +/// through `KeyUnwrappingKey`. Two-key TDEA may not apply protection (SP 800-131A Rev 2 Table 1), +/// and its engine decrypts only, so the key's `create_wrapper` and every wrapping one-shot refuse +/// to compile, exactly as its `create_encryptor` does under the cipher modes: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyWrappingKey}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // Two-key TDEA may not wrap: this does not compile. +/// let _ = kek.wrap(&[0u8; 16]); +/// ``` +/// +/// Unwrapping works, with the ciphertext as it was received: +/// +/// ``` +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::errors::SymmetricCipherError; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // Not a ciphertext this key made, so it is rejected rather than unwrapped to garbage. +/// assert!(matches!(kek.unwrap(&[0u8; 20]), Err(SymmetricCipherError::DecryptionFailed))); +/// ``` +#[allow(non_camel_case_types)] +pub type TDES2_TKW<'k> = Tkw<'k, TDES2Internal>; diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs index c0da2f735..753dd3ccf 100644 --- a/crypto/tdes/tests/alg_name_tests.rs +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -22,8 +22,8 @@ use bouncycastle_core::traits::Algorithm; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_tdes::hazmat::{TDES_ECB, TDES2_ECB, TDES2Internal, TDESInternal}; use bouncycastle_tdes::{ - BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, TDES2_CBC, - TDES2_CFB, TDES2_CFB8, TDES2_CTR, TDES3Key, + BLOCK_LEN, CTR_NONCE_LEN, KEY_LEN, TDES_CBC, TDES_CFB, TDES_CFB8, TDES_CTR, TDES_TKW, + TDES2_CBC, TDES2_CFB, TDES2_CFB8, TDES2_CTR, TDES2_TKW, TDES3Key, }; use core::mem::size_of; @@ -52,6 +52,15 @@ fn cbc() { check_decrypting::>("TDES2_CBC"); } +/// One worker serves both halves of a key wrap, so the alias is checked against itself; the +/// two-key alias has only the unwrapping half. +#[test] +fn tkw() { + use SecurityStrength::*; + check::, TDES_TKW<'_>>("TDES_TKW", _112bit); + check_decrypting::>("TDES2_TKW"); +} + #[test] fn ecb() { use SecurityStrength::*; diff --git a/crypto/tdes/tests/mode_key_tests.rs b/crypto/tdes/tests/mode_key_tests.rs index 847a2a380..5dd5cd08c 100644 --- a/crypto/tdes/tests/mode_key_tests.rs +++ b/crypto/tdes/tests/mode_key_tests.rs @@ -3,7 +3,7 @@ //! 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::modes::{Blocks, CBC, CFB, CFB8, CMAC, CTR, ModeNames, TKW}; use bouncycastle_cipher::padding::{NoPadding, PKCS7}; use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{BlockPermutation, EcbDecryptionKey, ElectronicCodeBook, Mode}; @@ -17,6 +17,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; use bouncycastle_core_test_framework::symmetric_ciphers::{ TestFrameworkStreamCipher, TestFrameworkSymmetricCipher, }; @@ -64,6 +65,18 @@ fn every_mode_key_enforces_the_material_policy() { construction_suite::(TDESInternal::CFB_ALG_NAME, TDES2Internal::CFB_ALG_NAME); construction_suite::(TDESInternal::CFB8_ALG_NAME, TDES2Internal::CFB8_ALG_NAME); construction_suite::(TDESInternal::CTR_ALG_NAME, TDES2Internal::CTR_ALG_NAME); + construction_suite::(TDESInternal::KW_ALG_NAME, TDES2Internal::KW_ALG_NAME); +} + +/// The key-wrap suite over the three-key bundle: round trips, the length helpers, the error +/// cases and tamper rejection, at the shortest plaintext and two longer ones. The answers are +/// CAVP's, in `tkw_cavp_tests.rs`; the two-key bundle unwraps only, in `tkw_reference_tests.rs`. +#[test] +fn tkw_keys_pass_the_key_wrap_framework() { + let tf = TestFrameworkKeyWrap::new(); + tf.test::>(); + tf.test::>(); + tf.test::>(); } #[test] diff --git a/crypto/tdes/tests/tkw_cavp_tests.rs b/crypto/tdes/tests/tkw_cavp_tests.rs new file mode 100644 index 000000000..2e9e862f2 --- /dev/null +++ b/crypto/tdes/tests/tkw_cavp_tests.rs @@ -0,0 +1,118 @@ +//! Known-answer tests against the NIST CAVP sample files for TKW, `TKW_AE.txt` and `TKW_AD.txt` +//! from `kwtestvectors.zip` ("Key Wrap Validation System" sample vectors, CAVS 17.4), read from +//! `bc-test-data/crypto/aes_tdes_vectors/TDES` through the framework's `bc_test_data` loader; +//! without `bc-test-data` beside this repository the tests print a warning and pass. +//! +//! Each file carries five `[PLAINTEXT LENGTH = n]` sections of 100 trials, for 64, 96, 128, 160 +//! and 2048 bits. `TKW_AE` gives `K`, `P` and `C`; `TKW_AD` gives `K`, `C` and either `P` or +//! `FAIL`, 20 forgeries per section. The forward cipher function is the designated one (the +//! `_inv` files of the zip are not used). `K` is `KEY1 || KEY2 || KEY3`, 24 bytes. + +use bouncycastle_cipher::modes::TKW; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_tdes::{KEY_LEN, TDES3Key}; + +const CAVP_DIR: &str = "crypto/aes_tdes_vectors/TDES"; + +/// One trial: `plaintext` is `None` for a `FAIL` line. +#[derive(Default)] +struct Case { + section: String, + count: u32, + key: Vec, + plaintext: Option>, + ciphertext: Vec, +} + +/// Parses a CAVS sample file: `[PLAINTEXT LENGTH = n]` headers, `COUNT = n` starting a case, +/// `K`, `P` and `C` hex fields, and a bare `FAIL` line. +fn parse(text: &str) -> Vec { + let mut cases: Vec = Vec::new(); + let mut section = String::new(); + for line in text.lines().map(str::trim) { + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { + section = name.to_string(); + continue; + } + if line == "FAIL" { + let case = cases.last_mut().expect("a FAIL after a COUNT"); + assert!(case.plaintext.is_none(), "FAIL on a case that has a plaintext"); + continue; + } + let (name, value) = line.split_once(" = ").expect("`NAME = value` line"); + if name == "COUNT" { + cases.push(Case { + section: section.clone(), + count: value.parse().expect("a decimal COUNT"), + ..Default::default() + }); + continue; + } + let case = cases.last_mut().expect("a field before any COUNT"); + let bytes = hex::decode(value).expect("hex field"); + match name { + "K" => case.key = bytes, + "P" => case.plaintext = Some(bytes), + "C" => case.ciphertext = bytes, + other => panic!("unexpected field {other}"), + } + } + cases +} + +fn load(file: &str) -> Option> { + let cases = parse(&bc_test_data(CAVP_DIR, file)?); + assert_eq!(cases.len(), 500, "{file}: five sections of 100 trials"); + println!("{file}: {} cases found under bc-test-data/{CAVP_DIR}", cases.len()); + Some(cases) +} + +fn kek(case: &Case) -> TDES3Key { + let key: [u8; KEY_LEN] = case.key.as_slice().try_into().expect("a 24-byte K"); + let material = KeyMaterial::::from_bytes_as_type(&key, KeyType::SymmetricCipherKey) + .expect("a symmetric cipher key"); + TDES3Key::::from_key_material(&material).unwrap_or_else(|e| { + panic!("[{}] COUNT = {}: the CAVP bundle was refused: {e:?}", case.section, case.count) + }) +} + +#[test] +fn tkw_ae() { + let Some(cases) = load("TKW_AE.txt") else { return }; + for case in &cases { + let where_ = format!("[{}] COUNT = {}", case.section, case.count); + let plaintext = case.plaintext.as_ref().expect("an AE case has a plaintext"); + let kek = kek(case); + assert_eq!(kek.wrap(plaintext).unwrap(), case.ciphertext, "{where_}: wrap"); + assert_eq!(kek.unwrap(&case.ciphertext).unwrap(), *plaintext, "{where_}: unwrap"); + } +} + +#[test] +fn tkw_ad() { + let Some(cases) = load("TKW_AD.txt") else { return }; + let mut forgeries = 0; + for case in &cases { + let where_ = format!("[{}] COUNT = {}", case.section, case.count); + let kek = kek(case); + let result = kek.unwrap(&case.ciphertext); + match &case.plaintext { + Some(plaintext) => assert_eq!(result.unwrap(), *plaintext, "{where_}: unwrap"), + None => { + assert!( + matches!(result, Err(SymmetricCipherError::DecryptionFailed)), + "{where_}: a forgery must fail with DecryptionFailed, got {result:?}" + ); + forgeries += 1; + } + } + } + assert_eq!(forgeries, 100, "20 forgeries in each of the five sections"); +} diff --git a/crypto/tdes/tests/tkw_reference_tests.rs b/crypto/tdes/tests/tkw_reference_tests.rs new file mode 100644 index 000000000..4d9cd1602 --- /dev/null +++ b/crypto/tdes/tests/tkw_reference_tests.rs @@ -0,0 +1,68 @@ +//! TKW against SP 800-38F Algorithm 7 written out over the raw TDEA permutation, for both key +//! bundles. The CAVP sample files pin the three-key answers in `tkw_cavp_tests.rs`; this file +//! pins the loop itself, with the 32-bit step counter and ICV3, against a direct transcription, +//! and is the only known answer for two-key unwrapping, since no wrapper exists for a two-key +//! bundle and CAVP tests TKW with three keys only. + +use bouncycastle_cipher::modes::TKW; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_tdes::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_tdes::{KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; + +fn material() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// TKW-AE (Algorithm 9) as written: `S = ICV3 || P`, then TW (Algorithm 7) with its shift +/// register, `A^t = MSB32(CIPH_K(A^{t-1} || R2^{t-1})) xor [t]32`, `R_i^t = R_{i+1}^{t-1}`, +/// `R_n^t = LSB32(CIPH_K(A^{t-1} || R2^{t-1}))`, for `t = 1 .. 6(n-1)`. +fn tkw_ae_as_written>(perm: &P, plaintext: &[u8]) -> Vec { + let mut a = [0xA6u8; 4]; + let mut r: Vec<[u8; 4]> = plaintext.chunks(4).map(|c| c.try_into().unwrap()).collect(); + let n = r.len() + 1; + for t in 1..=(6 * (n - 1)) as u32 { + let mut block = [0u8; 8]; + block[..4].copy_from_slice(&a); + block[4..].copy_from_slice(&r[0]); + perm.encrypt_block(&mut block); + a.copy_from_slice(&block[..4]); + for (x, y) in a.iter_mut().zip(t.to_be_bytes()) { + *x ^= y; + } + r.rotate_left(1); + r[n - 2].copy_from_slice(&block[4..]); + } + let mut out = a.to_vec(); + out.extend(r.iter().flatten()); + out +} + +#[test] +fn three_key_tkw_matches_algorithm_9_as_written() { + let perm = TDESInternal::new(&material::()).unwrap(); + let kek = TDES3Key::::from_key_material(&material::()).unwrap(); + for len in [8usize, 12, 16, 20, 24, 256] { + let plaintext: Vec = (0..len).map(|i| (i as u8).wrapping_mul(0x9D)).collect(); + let expected = tkw_ae_as_written(&perm, &plaintext); + assert_eq!(kek.wrap(&plaintext).unwrap(), expected, "{len} bytes"); + assert_eq!(kek.unwrap(&expected).unwrap(), plaintext, "{len} bytes"); + } +} + +/// The two-key engine's forward function is not gated (only the key's wrapper is), so a +/// ciphertext can be made from it here and the key's unwrapper must recover the plaintext. +#[test] +fn two_key_tkw_unwraps_what_algorithm_9_wrapped() { + let perm = TDES2Internal::new(&material::()).unwrap(); + let kek = TDES2Key::::from_key_material(&material::()).unwrap(); + for len in [8usize, 16, 20, 256] { + let plaintext: Vec = (0..len).map(|i| (i as u8).wrapping_mul(0x9D)).collect(); + let mut ciphertext = tkw_ae_as_written(&perm, &plaintext); + assert_eq!(kek.unwrap(&ciphertext).unwrap(), plaintext, "{len} bytes"); + ciphertext[len / 2] ^= 0x01; + assert!(kek.unwrap(&ciphertext).is_err(), "{len} bytes, modified"); + } +} From cf6e5a4a02553344b22cc22b6f6da4a418ce44e4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 7 Nov 2026 03:53:25 +1100 Subject: [PATCH 28/32] cipher, aes: Kw and Kwp carry their direction in the type `Kw<'k, P>` and `Kwp<'k, P>` were one type for both halves of the algorithm, implementing `KeyWrapper` and `KeyUnwrapper` together, so a key's unwrapper could wrap. Nothing in the AES key wraps can be made decrypt-only, but the shape is the one the TKW review found a hole in, and it is not the shape the cipher modes use. Both workers now take `Dir`, `Kw<'k, P, Dir>`: `KeyWrapper` is implemented for the `Encrypting` worker alone and `KeyUnwrapper` for the `Decrypting` one, the mode impls name each, and `wrap_init` refuses a permutation that decrypts only in an inline `const`, as the encrypting cipher modes do. The marker is zero-sized, so a worker is still one pointer. The `AES_KW_*` and `AES_KWP_*` aliases gain the `Dir` parameter, and the alias and size tests name a direction. cargo test for cipher, aes, tdes and cli green; cargo doc warnings unchanged. The refactor the TKW fix needs, ahead of it. Assisted-by: Claude:claude-mythos-5-1 --- alpha_0.1.4_release_notes.md | 4 ++ crypto/aes/src/kw.rs | 12 +++--- crypto/aes/tests/alg_name_tests.rs | 27 ++++++------- crypto/aes/tests/kw_tests.rs | 19 +++++----- crypto/cipher/src/modes/kw.rs | 42 +++++++++++++-------- crypto/cipher/src/modes/kwp.rs | 29 +++++++++----- crypto/cipher/tests/modes/alg_name_tests.rs | 6 ++- crypto/cipher/tests/modes/kw_tests.rs | 5 ++- 8 files changed, 83 insertions(+), 61 deletions(-) diff --git a/alpha_0.1.4_release_notes.md b/alpha_0.1.4_release_notes.md index 97ad4dee3..0030c06b3 100644 --- a/alpha_0.1.4_release_notes.md +++ b/alpha_0.1.4_release_notes.md @@ -15,6 +15,10 @@ ## Minor features / bug fixes +* The key-wrap workers `Kw`, `Kwp` and `Tkw` carry their direction in the type, as the cipher modes do: + `KeyWrapper` is implemented for the encrypting worker only, so an unwrapper has no wrapping method and a + permutation that decrypts only is refused a wrapper at compile time by every route. The `AES_KW_*`, `AES_KWP_*` + and `TDES_TKW` aliases gain the `Dir` parameter; `TDES2_TKW` is the decrypting one. * Cipher and MAC workers no longer implement `SuspendableKeyed` (#173): a resumed worker is a second copy of a nonce, counter or keystream position. The AES modes, CMAC and Ascon-AEAD128 lose their suspended-state constants and `hazmat::ModeKey` goes with them; HMAC and HKDF still suspend, and hashes, XOFs and KMAC keep diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs index d5036e85c..2e9c8257b 100644 --- a/crypto/aes/src/kw.rs +++ b/crypto/aes/src/kw.rs @@ -102,27 +102,27 @@ use bouncycastle_cipher::modes::{Kw, Kwp}; /// [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey /// [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey #[allow(non_camel_case_types)] -pub type AES_KW_128<'k> = Kw<'k, AES128Internal>; +pub type AES_KW_128<'k, Dir> = Kw<'k, AES128Internal, Dir>; /// AES-192 Key Wrap (SP 800-38F KW; RFC 3394 with a 192-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_192<'k> = Kw<'k, AES192Internal>; +pub type AES_KW_192<'k, Dir> = Kw<'k, AES192Internal, Dir>; /// AES-256 Key Wrap (SP 800-38F KW; RFC 3394 with a 256-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_256<'k> = Kw<'k, AES256Internal>; +pub type AES_KW_256<'k, Dir> = Kw<'k, AES256Internal, Dir>; /// AES-128 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 128-bit KEK): the worker an /// `AES128Key` hands out. #[allow(non_camel_case_types)] -pub type AES_KWP_128<'k> = Kwp<'k, AES128Internal>; +pub type AES_KWP_128<'k, Dir> = Kwp<'k, AES128Internal, Dir>; /// AES-192 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 192-bit KEK). See /// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_192<'k> = Kwp<'k, AES192Internal>; +pub type AES_KWP_192<'k, Dir> = Kwp<'k, AES192Internal, Dir>; /// AES-256 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 256-bit KEK). See /// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_256<'k> = Kwp<'k, AES256Internal>; +pub type AES_KWP_256<'k, Dir> = Kwp<'k, AES256Internal, Dir>; diff --git a/crypto/aes/tests/alg_name_tests.rs b/crypto/aes/tests/alg_name_tests.rs index f9676a50e..d6d0e5a42 100644 --- a/crypto/aes/tests/alg_name_tests.rs +++ b/crypto/aes/tests/alg_name_tests.rs @@ -158,25 +158,20 @@ fn cmac() { ); } -/// One worker serves both halves of a key wrap, so there is one name per alias to check. +/// A key wrap's two directions report the one name and strength. #[test] fn kw_kwp() { use SecurityStrength::*; - assert_eq!((AES_KW_128::ALG_NAME, AES_KW_128::MAX_SECURITY_STRENGTH), ("AES_KW_128", _128bit)); - assert_eq!((AES_KW_192::ALG_NAME, AES_KW_192::MAX_SECURITY_STRENGTH), ("AES_KW_192", _192bit)); - assert_eq!((AES_KW_256::ALG_NAME, AES_KW_256::MAX_SECURITY_STRENGTH), ("AES_KW_256", _256bit)); - assert_eq!( - (AES_KWP_128::ALG_NAME, AES_KWP_128::MAX_SECURITY_STRENGTH), - ("AES_KWP_128", _128bit) - ); - assert_eq!( - (AES_KWP_192::ALG_NAME, AES_KWP_192::MAX_SECURITY_STRENGTH), - ("AES_KWP_192", _192bit) - ); - assert_eq!( - (AES_KWP_256::ALG_NAME, AES_KWP_256::MAX_SECURITY_STRENGTH), - ("AES_KWP_256", _256bit) - ); + fn check(name: &str, strength: SecurityStrength) { + assert_eq!((Enc::ALG_NAME, Enc::MAX_SECURITY_STRENGTH), (name, strength)); + assert_eq!((Dec::ALG_NAME, Dec::MAX_SECURITY_STRENGTH), (name, strength)); + } + check::, AES_KW_128<'_, Decrypting>>("AES_KW_128", _128bit); + check::, AES_KW_192<'_, Decrypting>>("AES_KW_192", _192bit); + check::, AES_KW_256<'_, Decrypting>>("AES_KW_256", _256bit); + check::, AES_KWP_128<'_, Decrypting>>("AES_KWP_128", _128bit); + check::, AES_KWP_192<'_, Decrypting>>("AES_KWP_192", _192bit); + check::, AES_KWP_256<'_, Decrypting>>("AES_KWP_256", _256bit); } fn key_for>() -> AES128Key { diff --git a/crypto/aes/tests/kw_tests.rs b/crypto/aes/tests/kw_tests.rs index 032c64602..6ba63e106 100644 --- a/crypto/aes/tests/kw_tests.rs +++ b/crypto/aes/tests/kw_tests.rs @@ -15,6 +15,7 @@ use bouncycastle_aes::{ AES192Key, AES256Key, }; use bouncycastle_cipher::modes::{KW, KWP, ModeNames}; +use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::Mode; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; @@ -210,18 +211,18 @@ fn the_three_key_lengths_are_not_interchangeable() { #[test] fn the_keys_hand_out_the_aliased_workers() { let kek = key128::(&RFC3394_KEK[..32]); - let _: AES_KW_128<'_> = kek.create_wrapper(); - let _: AES_KW_128<'_> = kek.create_unwrapper(); + let _: AES_KW_128<'_, Encrypting> = kek.create_wrapper(); + let _: AES_KW_128<'_, Decrypting> = kek.create_unwrapper(); let kek = key192::(&RFC3394_KEK[..48]); - let _: AES_KW_192<'_> = kek.create_wrapper(); + let _: AES_KW_192<'_, Encrypting> = kek.create_wrapper(); let kek = key256::(RFC3394_KEK); - let _: AES_KW_256<'_> = kek.create_unwrapper(); + let _: AES_KW_256<'_, Decrypting> = kek.create_unwrapper(); let kek = key128::(&RFC3394_KEK[..32]); - let _: AES_KWP_128<'_> = kek.create_wrapper(); + let _: AES_KWP_128<'_, Encrypting> = kek.create_wrapper(); let kek = key192::(&RFC3394_KEK[..48]); - let _: AES_KWP_192<'_> = kek.create_unwrapper(); + let _: AES_KWP_192<'_, Decrypting> = kek.create_unwrapper(); let kek = key256::(RFC3394_KEK); - let _: AES_KWP_256<'_> = kek.create_wrapper(); + let _: AES_KWP_256<'_, Encrypting> = kek.create_wrapper(); } /// The sizes the docs quote: the key is the schedule, the worker is the borrow. @@ -233,8 +234,8 @@ fn sizes() { [size_of::(), size_of::(), size_of::()] ); assert_eq!(size_of::>(), size_of::>()); - assert_eq!(size_of::(), size_of::<&AES128Internal>()); - assert_eq!(size_of::(), size_of::<&AES256Internal>()); + assert_eq!(size_of::>(), size_of::<&AES128Internal>()); + assert_eq!(size_of::>(), size_of::<&AES256Internal>()); } /// Equality follows the material: the same bytes compare equal, as does a clone, and different diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index 62ead5306..8a6c1bcfa 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -75,12 +75,13 @@ //! )); //! ``` //! -//! # No direction marker +//! # The direction is in the type //! -//! Unlike [`Ecb`](crate::modes::hazmat::ecb::Ecb) and the other modes, [`Kw`] has no `Dir` -//! parameter. The wrapping and unwrapping halves are already separate traits ([`KeyWrapper`] / -//! [`KeyUnwrapper`]), so a policy can import one trait and not the other, and there is no -//! per-direction state for a marker type to select. +//! [`Kw<'k, P, Dir>`](Kw) carries [`Encrypting`] or [`Decrypting`] as the cipher modes do: +//! [`KeyWrapper`] is implemented for the encrypting worker alone and [`KeyUnwrapper`] for the +//! decrypting one, so an unwrapper has no wrapping method to call, and a permutation that +//! decrypts only ([`BlockPermutation::DECRYPT_ONLY`]) is refused a wrapper at compile time in +//! `wrap_init`. The worker has no per-direction state; the marker is zero-sized. //! //! # The wrapping function runs in place, without the shift register //! @@ -122,6 +123,7 @@ //! [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey use super::ModeNames; +use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -131,6 +133,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; /// The block length, in bytes, of the permutation under KW and KWP. SP 800-38F Sec 5.1: "For KW /// and KWP, the underlying block cipher shall be approved, and the block size shall be 128 bits." @@ -279,11 +282,17 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Wrapper<'k> - = Kw<'k, P> + = Kw<'k, P, Encrypting> where P: 'k; fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; Kw::from_perm(state) } } @@ -293,7 +302,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Unwrapper<'k> - = Kw<'k, P> + = Kw<'k, P, Decrypting> where P: 'k; @@ -305,28 +314,29 @@ where /// KW over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a /// [`KW`]-bound key hands out, borrowing the keyed permutation for `'k`. /// -/// There is no state beyond the borrow and no direction marker: both halves of the algorithm are -/// the [`KeyWrapper`] and [`KeyUnwrapper`] traits, which this type implements together. See the -/// module docs for usage and the security notes. -pub struct Kw<'k, P> +/// There is no state beyond the borrow; `Dir` is [`Encrypting`] for the wrapper, which alone +/// implements [`KeyWrapper`], and [`Decrypting`] for the unwrapper, which alone implements +/// [`KeyUnwrapper`]. See the module docs for usage and the security notes. +pub struct Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { perm: &'k P, + _dir: PhantomData

, } -impl<'k, P> Kw<'k, P> +impl<'k, P, Dir> Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { /// Over an already-keyed permutation, which the worker borrows: what [`KW`]'s mode impls /// build for a key's roles. pub(crate) fn from_perm(perm: &'k P) -> Self { - Self { perm } + Self { perm, _dir: PhantomData } } } -impl<'k, P> Algorithm for Kw<'k, P> +impl<'k, P, Dir> Algorithm for Kw<'k, P, Dir> where P: BlockPermutation + ModeNames, { @@ -336,7 +346,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P> KeyWrapper for Kw<'k, P> +impl<'k, P> KeyWrapper for Kw<'k, P, Encrypting> where P: BlockPermutation + ModeNames, { @@ -397,7 +407,7 @@ where } } -impl<'k, P> KeyUnwrapper for Kw<'k, P> +impl<'k, P> KeyUnwrapper for Kw<'k, P, Decrypting> where P: BlockPermutation + ModeNames, { diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index 7d77cb910..b36c19d5e 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -62,6 +62,7 @@ use super::ModeNames; use super::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; +use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -71,6 +72,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_zero_bytes}; use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; /// ICV2, the integrity check value of KWP (Algorithm 5, step 1): `0xA65959A6`. const ICV2: [u8; 4] = [0xA6, 0x59, 0x59, 0xA6]; @@ -125,11 +127,17 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Wrapper<'k> - = Kwp<'k, P> + = Kwp<'k, P, Encrypting> where P: 'k; fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + const { + assert!( + !P::DECRYPT_ONLY, + "this permutation decrypts only (BlockPermutation::DECRYPT_ONLY): no wrapper may be made over it" + ) + }; Kwp::from_perm(state) } } @@ -139,7 +147,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Unwrapper<'k> - = Kwp<'k, P> + = Kwp<'k, P, Decrypting> where P: 'k; @@ -149,27 +157,28 @@ where } /// KWP over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a -/// [`KWP`]-bound key hands out, borrowing the keyed permutation for `'k`, as -/// [`Kw`](super::kw::Kw) does. See the module docs. -pub struct Kwp<'k, P> +/// [`KWP`]-bound key hands out, borrowing the keyed permutation for `'k` and carrying its +/// direction in `Dir`, as [`Kw`](super::kw::Kw) does. See the module docs. +pub struct Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { perm: &'k P, + _dir: PhantomData, } -impl<'k, P> Kwp<'k, P> +impl<'k, P, Dir> Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { /// Over an already-keyed permutation, which the worker borrows: what [`KWP`]'s mode impls /// build for a key's roles. pub(crate) fn from_perm(perm: &'k P) -> Self { - Self { perm } + Self { perm, _dir: PhantomData } } } -impl<'k, P> Algorithm for Kwp<'k, P> +impl<'k, P, Dir> Algorithm for Kwp<'k, P, Dir> where P: BlockPermutation + ModeNames, { @@ -179,7 +188,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P> KeyWrapper for Kwp<'k, P> +impl<'k, P> KeyWrapper for Kwp<'k, P, Encrypting> where P: BlockPermutation + ModeNames, { @@ -252,7 +261,7 @@ where } } -impl<'k, P> KeyUnwrapper for Kwp<'k, P> +impl<'k, P> KeyUnwrapper for Kwp<'k, P, Decrypting> where P: BlockPermutation + ModeNames, { diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index ec8319835..276bef2b5 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -65,8 +65,10 @@ fn each_mode_reports_its_own_name() { check::>(Toy::CMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); - check::>(Toy::KW_ALG_NAME); - check::>(Toy::KWP_ALG_NAME); + check::>(Toy::KW_ALG_NAME); + check::>(Toy::KW_ALG_NAME); + check::>(Toy::KWP_ALG_NAME); + check::>(Toy::KWP_ALG_NAME); } /// The padding layer and the stream-cipher wrapper add no name of their own. diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs index 893277551..2fa9e2395 100644 --- a/crypto/cipher/tests/modes/kw_tests.rs +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -11,6 +11,7 @@ mod common; mod toy_key; use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; +use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -227,7 +228,7 @@ fn kwp_costs_nothing_extra_for_aligned_data() { #[test] fn sizes() { use core::mem::size_of; - assert_eq!(size_of::>(), size_of::<&Toy>()); - assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::>(), size_of::<&Toy>()); assert_eq!(size_of::(), size_of::()); } From 3509db5634c1029da24c37f4d21c8c930c489e76 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 7 Nov 2026 04:17:14 +1100 Subject: [PATCH 29/32] cipher, tdes: Tkw carries its direction in the type, so a two-key unwrapper cannot wrap PR #139 review: `TDES2Key`'s unwrapper was the same type as a wrapper and implemented `KeyWrapper`, so `create_unwrapper().wrap_out` compiled and produced two-key TKW-AE ciphertext (SP 800-131A Rev 2 Table 6: two-key wrapping is disallowed), through five routes the `DECRYPT_ONLY` check in `wrap_init` never saw. `Tkw<'k, P, Dir>` now takes the direction as `Kw` does: the unwrapper is `Tkw<'k, P, Decrypting>`, a type with no wrapping method, so every route is a type error rather than a gate to remember. `TDES_TKW` gains the `Dir` parameter; `TDES2_TKW` is the decrypting worker, and its docs gain the two `compile_fail` doctests the review asked for, on `create_unwrapper().wrap_out(..)` and on coercing the unwrapper to `&dyn KeyWrapper`. cargo test for cipher, aes, tdes and cli green, the new doctests among them; cargo doc warnings unchanged. Mutants (apollo, kw.rs, kwp.rs and tkw.rs in full, with the cipher, aes and tdes suites): 217 tested, 159 caught, 58 unviable, 0 missed, 0 timeouts; by file kw 58/20, kwp 59/18, tkw 42/20 caught/unviable. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/tkw.rs | 29 +++++++++++-------- crypto/tdes/src/tkw.rs | 45 +++++++++++++++++++++++++++-- crypto/tdes/tests/alg_name_tests.rs | 2 +- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/crypto/cipher/src/modes/tkw.rs b/crypto/cipher/src/modes/tkw.rs index 169ad0ba5..cc89e75bc 100644 --- a/crypto/cipher/src/modes/tkw.rs +++ b/crypto/cipher/src/modes/tkw.rs @@ -16,7 +16,8 @@ //! (Appendix A.3), and TDEA itself is a legacy algorithm; see the `bouncycastle-tdes` crate's //! security considerations. Two-key TDEA may not wrap at all: a permutation that decrypts only //! ([`BlockPermutation::DECRYPT_ONLY`]) refuses to make a wrapper at compile time, as the -//! encrypting modes refuse their encryptors, while its unwrapper works. +//! encrypting modes refuse their encryptors, while its unwrapper works -- and the unwrapper, +//! `Tkw<'k, P, Decrypting>`, is a distinct type with no wrapping method. //! //! # The length limits //! @@ -26,6 +27,7 @@ use super::ModeNames; use super::kw::{w, w_inv}; +use crate::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, @@ -35,6 +37,7 @@ use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; /// The block length, in bytes, of the permutation under TKW. SP 800-38F Sec 5.1: "For TKW, the /// underlying block cipher is specified to be TDEA, and the block size is therefore 64 bits". @@ -94,7 +97,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Wrapper<'k> - = Tkw<'k, P> + = Tkw<'k, P, Encrypting> where P: 'k; @@ -114,7 +117,7 @@ where P: ElectronicCodeBook + ModeNames + Clone, { type Unwrapper<'k> - = Tkw<'k, P> + = Tkw<'k, P, Decrypting> where P: 'k; @@ -124,28 +127,30 @@ where } /// TKW over any 64-bit permutation that impls [`BlockPermutation`]: the one-shot worker a -/// [`TKW`]-bound key hands out, borrowing the keyed permutation for `'k`, as -/// [`Kw`](super::kw::Kw) does. It reports the permutation's [`ModeNames::KW_ALG_NAME`], since a -/// permutation has one block length and so one key-wrap algorithm. See the module docs. -pub struct Tkw<'k, P> +/// [`TKW`]-bound key hands out, borrowing the keyed permutation for `'k` and carrying its +/// direction in `Dir`, as [`Kw`](super::kw::Kw) does. It reports the permutation's +/// [`ModeNames::KW_ALG_NAME`], since a permutation has one block length and so one key-wrap +/// algorithm. See the module docs. +pub struct Tkw<'k, P, Dir> where P: BlockPermutation + ModeNames, { perm: &'k P, + _dir: PhantomData, } -impl<'k, P> Tkw<'k, P> +impl<'k, P, Dir> Tkw<'k, P, Dir> where P: BlockPermutation + ModeNames, { /// Over an already-keyed permutation, which the worker borrows: what [`TKW`]'s mode impls /// build for a key's roles. pub(crate) fn from_perm(perm: &'k P) -> Self { - Self { perm } + Self { perm, _dir: PhantomData } } } -impl<'k, P> Algorithm for Tkw<'k, P> +impl<'k, P, Dir> Algorithm for Tkw<'k, P, Dir> where P: BlockPermutation + ModeNames, { @@ -155,7 +160,7 @@ where const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl<'k, P> KeyWrapper for Tkw<'k, P> +impl<'k, P> KeyWrapper for Tkw<'k, P, Encrypting> where P: BlockPermutation + ModeNames, { @@ -216,7 +221,7 @@ where } } -impl<'k, P> KeyUnwrapper for Tkw<'k, P> +impl<'k, P> KeyUnwrapper for Tkw<'k, P, Decrypting> where P: BlockPermutation + ModeNames, { diff --git a/crypto/tdes/src/tkw.rs b/crypto/tdes/src/tkw.rs index c9fea207b..82573b80b 100644 --- a/crypto/tdes/src/tkw.rs +++ b/crypto/tdes/src/tkw.rs @@ -2,6 +2,7 @@ //! which the crate root re-exports. use crate::hazmat::{TDES2Internal, TDESInternal}; +use bouncycastle_cipher::Decrypting; use bouncycastle_cipher::modes::Tkw; /// TDEA Key Wrap: the `Tkw` worker of `bouncycastle-cipher` over the three-key TDEA @@ -42,13 +43,16 @@ use bouncycastle_cipher::modes::Tkw; /// assert_eq!(kek.unwrap(&wrapped).expect("unwrapping"), [0x5Au8; 24]); /// ``` /// +/// `Dir` is [`Encrypting`](bouncycastle_cipher::Encrypting) for the wrapper and [`Decrypting`] +/// for the unwrapper, as for the cipher modes. +/// /// # Memory Usage /// /// A key is the engine's expanded schedule, as for the other modes (see the crate's table). A /// worker is the borrow of it, one pointer, and the wrapping function's scratch is one 8-byte /// block on the stack; the data is wrapped and unwrapped in the caller's buffers. #[allow(non_camel_case_types)] -pub type TDES_TKW<'k> = Tkw<'k, TDESInternal>; +pub type TDES_TKW<'k, Dir> = Tkw<'k, TDESInternal, Dir>; /// Two-key TDEA Key Wrap, unwrapping only: what a [`TDES2Key`](crate::TDES2Key) hands out /// through `KeyUnwrappingKey`. Two-key TDEA may not apply protection (SP 800-131A Rev 2 Table 1), @@ -70,6 +74,43 @@ pub type TDES_TKW<'k> = Tkw<'k, TDESInternal>; /// let _ = kek.wrap(&[0u8; 16]); /// ``` /// +/// The unwrapper the key hands out is `Tkw<'k, TDES2Internal, Decrypting>`, a type with no +/// wrapping method, so neither of these compiles either: +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// use bouncycastle_core::traits::KeyWrapper; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // error[E0599]: no method named `wrap_out` found for the unwrapper +/// let mut out = [0u8; 20]; +/// let _ = kek.create_unwrapper().wrap_out(&[0u8; 16], &mut out); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_tdes::TDES2Key; +/// use bouncycastle_cipher::modes::TKW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey}; +/// use bouncycastle_core::traits::KeyWrapper; +/// +/// let bytes: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let kek = TDES2Key::::from_key_material(&material).unwrap(); +/// +/// // error[E0277]: `KeyWrapper` is not implemented for the unwrapper +/// let unwrapper = kek.create_unwrapper(); +/// let _: &dyn KeyWrapper = &unwrapper; +/// ``` +/// /// Unwrapping works, with the ciphertext as it was received: /// /// ``` @@ -88,4 +129,4 @@ pub type TDES_TKW<'k> = Tkw<'k, TDESInternal>; /// assert!(matches!(kek.unwrap(&[0u8; 20]), Err(SymmetricCipherError::DecryptionFailed))); /// ``` #[allow(non_camel_case_types)] -pub type TDES2_TKW<'k> = Tkw<'k, TDES2Internal>; +pub type TDES2_TKW<'k> = Tkw<'k, TDES2Internal, Decrypting>; diff --git a/crypto/tdes/tests/alg_name_tests.rs b/crypto/tdes/tests/alg_name_tests.rs index 753dd3ccf..e6d6121a5 100644 --- a/crypto/tdes/tests/alg_name_tests.rs +++ b/crypto/tdes/tests/alg_name_tests.rs @@ -57,7 +57,7 @@ fn cbc() { #[test] fn tkw() { use SecurityStrength::*; - check::, TDES_TKW<'_>>("TDES_TKW", _112bit); + check::, TDES_TKW<'_, Decrypting>>("TDES_TKW", _112bit); check_decrypting::>("TDES2_TKW"); } From 608642ab9fe66713b74b84e3bda71dacadf33d15 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 7 Nov 2026 04:17:14 +1100 Subject: [PATCH 30/32] cli, tdes: tdes-tkw and tdes-cfb8 encrypt count the blocks TDEA processes; the wrap docs cite Table 6 PR #139 review. `tdes-tkw -d encrypt` had no input bound while every other three-key encrypting command refuses input past SP 800-67 Rev 2 Sec 3.4's 2^20 blocks, and `tdes-cfb8` applied the 8 MiB figure of the modes that pass each 8 bytes through TDEA once, although CFB8 enciphers a block per byte. The bound is now read as the blocks TDEA processes: the key-wrap runner takes an optional `InputLimit` checked on the whole input before the wrap runs, `tdes-tkw` passes `TDEA_LIMIT_TKW`, six calls per 4-byte semiblock (SP 800-38F Algorithm 7, s = 6(n - 1)), so 699,048 bytes, and `tdes-cfb8` passes `TDEA_LIMIT_CFB8`, 2^20 bytes. The `aes*-kw`/`kwp` commands pass no limit. The two commands' help and the shell test say so, and `test_tdes.sh` drives both bounds from either side. The TKW docs cited SP 800-131A Rev 2 Table 1, whose rows are encryption; the key-wrapping rows are Table 6, read from the PDF: two- key wrapping "Disallowed", three-key wrapping "Deprecated through 2023, Disallowed after 2023" unless other NIST guidance allows it, unwrapping under either "Legacy use". The tdes crate docs state it and say that three-key TKW wrapping is kept on the same terms as three-key encryption. cargo test -p cli 17 passed; test_tdes.sh 7 passed, the CFB8 and TKW cases at and one unit past each bound among them. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_kw_cmd.rs | 22 +++++++++++++-- cli/src/helpers/block_mode_helpers.rs | 26 +++++++++++++++-- cli/src/main.rs | 7 ++++- cli/src/tdes_cfb8_cmd.rs | 8 ++++-- cli/src/tdes_tkw_cmd.rs | 16 +++++++---- cli/tests/test_tdes.sh | 40 +++++++++++++++++++-------- crypto/cipher/src/modes/tkw.rs | 3 +- crypto/tdes/src/lib.rs | 12 +++++--- crypto/tdes/src/tkw.rs | 2 +- 9 files changed, 103 insertions(+), 33 deletions(-) diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs index b1e8cf15f..d7b5f4323 100644 --- a/cli/src/aes_kw_cmd.rs +++ b/cli/src/aes_kw_cmd.rs @@ -44,7 +44,7 @@ //! | bc-rust aes128-kw -d encrypt --key 000102030405060708090a0b0c0d0e0f -x //! ``` -use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::block_mode_helpers::{CipherDirection, InputLimit, load_key_object}; use crate::helpers::write_bytes_or_hex; use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::{KW, KWP}; @@ -62,6 +62,7 @@ pub(crate) fn aes128_kw_cmd( run::<_, 16>( direction, &load_key_object::, 16>(key, key_file, "AES-128-KW"), + None, output_hex, "AES-128-KW", ); @@ -76,6 +77,7 @@ pub(crate) fn aes192_kw_cmd( run::<_, 24>( direction, &load_key_object::, 24>(key, key_file, "AES-192-KW"), + None, output_hex, "AES-192-KW", ); @@ -90,6 +92,7 @@ pub(crate) fn aes256_kw_cmd( run::<_, 32>( direction, &load_key_object::, 32>(key, key_file, "AES-256-KW"), + None, output_hex, "AES-256-KW", ); @@ -104,6 +107,7 @@ pub(crate) fn aes128_kwp_cmd( run::<_, 16>( direction, &load_key_object::, 16>(key, key_file, "AES-128-KWP"), + None, output_hex, "AES-128-KWP", ); @@ -118,6 +122,7 @@ pub(crate) fn aes192_kwp_cmd( run::<_, 24>( direction, &load_key_object::, 24>(key, key_file, "AES-192-KWP"), + None, output_hex, "AES-192-KWP", ); @@ -132,6 +137,7 @@ pub(crate) fn aes256_kwp_cmd( run::<_, 32>( direction, &load_key_object::, 32>(key, key_file, "AES-256-KWP"), + None, output_hex, "AES-256-KWP", ); @@ -142,21 +148,25 @@ pub(crate) fn aes256_kwp_cmd( pub(crate) fn run( direction: &CipherDirection, kek: &K, + limit: Option<&InputLimit>, output_hex: bool, alg: &str, ) where K: KeyWrappingKey + KeyUnwrappingKey, { match direction { - CipherDirection::Encrypt => run_with(|input| kek.wrap(input), output_hex, alg), - CipherDirection::Decrypt => run_with(|input| kek.unwrap(input), output_hex, alg), + CipherDirection::Encrypt => run_with(|input| kek.wrap(input), limit, output_hex, alg), + CipherDirection::Decrypt => run_with(|input| kek.unwrap(input), None, output_hex, alg), } } /// Reads all of stdin, applies `op` to it and writes the result; the unwrap-only commands pass /// their key's `unwrap` alone, so that no wrapper is ever instantiated for their key type. +/// `limit`, for a wrapping command whose standard bounds what one key may protect, is checked +/// against the whole input before `op` runs. pub(crate) fn run_with( op: impl FnOnce(&[u8]) -> Result, SymmetricCipherError>, + limit: Option<&InputLimit>, output_hex: bool, alg: &str, ) { @@ -165,6 +175,12 @@ pub(crate) fn run_with( eprintln!("Error: couldn't read stdin: {e}"); exit(-1); } + if let Some(limit) = limit + && input.len() as u64 > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); + } match op(&input) { Ok(output) => write_bytes_or_hex(&output, output_hex), diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index eb3dc95cb..2add921fe 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -304,9 +304,11 @@ pub(crate) struct InputLimit { } /// SP 800-67 Rev 2 Sec 3.4: "One key bundle shall not be used to apply cryptographic protection -/// (e.g., encrypt) more than 2^20 64-bit data blocks", 8 MiB. The command is the one place that -/// sees a bundle's whole use within an invocation, so it holds the input to that; it cannot count -/// across invocations. +/// (e.g., encrypt) more than 2^20 64-bit data blocks", 8 MiB for a mode that passes each 8 bytes +/// of input through TDEA once (ECB, CBC, CFB64, CTR). The bound counts blocks the cipher +/// processes, so a mode that invokes TDEA more often per byte has a lower figure below. The +/// command is the one place that sees a bundle's whole use within an invocation, so it holds the +/// input to that; it cannot count across invocations. pub(crate) const TDEA_LIMIT: InputLimit = InputLimit { max_bytes: 1 << 23, message: "TDEA: one key bundle may protect at most 2^20 blocks (8 MiB; SP 800-67 Rev 2 Sec \ @@ -318,6 +320,24 @@ pub(crate) const TDEA_LIMIT: InputLimit = InputLimit { pub(crate) const TDEA_LIMIT_PADDED: InputLimit = InputLimit { max_bytes: (1 << 23) - 1, message: TDEA_LIMIT.message }; +/// The bound for CFB8, which enciphers one block per byte (SP 800-38A Sec 6.3, `s = 8`): 2^20 +/// blocks is 2^20 bytes, 1 MiB. +pub(crate) const TDEA_LIMIT_CFB8: InputLimit = InputLimit { + max_bytes: 1 << 20, + message: "TDEA: one key bundle may protect at most 2^20 blocks (SP 800-67 Rev 2 Sec 3.4), and \ + CFB8 enciphers one block per byte, so the input exceeds that at 1 MiB", +}; + +/// The bound for TKW, whose wrapping function makes six TDEA calls per 4-byte semiblock of +/// plaintext (SP 800-38F Algorithm 7, `s = 6(n - 1)`): 2^20 blocks is `2^20 / 6` semiblocks, +/// 699,048 bytes. +pub(crate) const TDEA_LIMIT_TKW: InputLimit = InputLimit { + max_bytes: (1 << 20) / 6 * 4, + message: "TDEA: one key bundle may protect at most 2^20 blocks (SP 800-67 Rev 2 Sec 3.4), and \ + TKW makes six TDEA calls per 4-byte block of input, so the input exceeds that at \ + 699,048 bytes", +}; + /// [`encrypt_stream`] under an [`InputLimit`]: the running input total is checked against the /// bound before each chunk is encrypted, so nothing past it is ever written. pub(crate) fn encrypt_stream_bounded( diff --git a/cli/src/main.rs b/cli/src/main.rs index 76bfdd4ba..edf25f187 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2557,6 +2557,9 @@ enum Subcommands { /// `decrypt` is a whole number of 4-byte blocks, at least 12 bytes. There is NO IV, and the /// whole input is read before anything is written. See `aes128-kw` for the rest. /// + /// `encrypt` refuses input past the SP 800-67 Rev 2 Sec 3.4 bound, counted as the blocks + /// TDEA processes: TKW makes six calls per 4-byte block of input, so at most 699,048 bytes. + /// /// The key is 24 bytes, KEY1 || KEY2 || KEY3, with three distinct component keys none of which /// is a weak DES key (SP 800-67 Rev 2 Sec 3.1 and 3.3.2). Two-key TDEA unwraps through /// `tdes2-tkw`. @@ -3050,7 +3053,9 @@ enum Subcommands { /// /// 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. + /// so prefer that unless a byte-granular mode is specifically required. That is also why + /// `encrypt` here refuses input past 2^20 bytes (1 MiB): the SP 800-67 Rev 2 Sec 3.4 bound + /// counts the blocks TDEA processes, one per byte in this mode. /// /// See `tdes-cfb` for the IV convention and input-length rule, and `tdes-cbc` for the key /// format and the warnings, which apply unchanged. diff --git a/cli/src/tdes_cfb8_cmd.rs b/cli/src/tdes_cfb8_cmd.rs index f853dcc6f..b7b8615c7 100644 --- a/cli/src/tdes_cfb8_cmd.rs +++ b/cli/src/tdes_cfb8_cmd.rs @@ -9,7 +9,9 @@ //! 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. +//! byte-granular mode is specifically required. The SP 800-67 Rev 2 Sec 3.4 bound counts the +//! blocks TDEA processes, so `encrypt` here refuses input past 2^20 bytes, 1 MiB, where the modes +//! that process one block per 8 bytes allow 8 MiB. //! //! # Warning //! @@ -19,7 +21,7 @@ //! TDEA is a legacy algorithm, disallowed for encryption by NIST SP 800-131A Rev 2 after 2023; see //! the `bouncycastle-tdes` crate docs. -use crate::helpers::block_mode_helpers::TDEA_LIMIT; +use crate::helpers::block_mode_helpers::TDEA_LIMIT_CFB8; use crate::helpers::block_mode_helpers::{CipherDirection, load_iv, load_key_object}; use crate::helpers::stream_mode_helpers::run_stream_mode_bounded; use bouncycastle::cipher::modes::CFB8; @@ -39,6 +41,6 @@ pub(crate) fn tdes_cfb8_cmd( &key, load_iv(iv, iv_file), output_hex, - Some(&TDEA_LIMIT), + Some(&TDEA_LIMIT_CFB8), ) } diff --git a/cli/src/tdes_tkw_cmd.rs b/cli/src/tdes_tkw_cmd.rs index 5fd3af47d..f99d3f93b 100644 --- a/cli/src/tdes_tkw_cmd.rs +++ b/cli/src/tdes_tkw_cmd.rs @@ -4,12 +4,17 @@ //! They are the `aes*-kw` command over TDEA (NIST SP 800-38F Sec 7, TKW), so everything in //! [`crate::aes_kw_cmd`] -- the whole-input reading, the absence of an IV, the fail-closed unwrap //! -- applies unchanged, at half the width: the input is a whole number of 4-byte semiblocks, at -//! least 8 bytes, and the output is 4 bytes longer. TKW is approved for legacy systems only -//! (SP 800-38F Sec 8), and two-key TDEA may not apply protection at all, so `tdes2-tkw` has no -//! `encrypt`, as the other `tdes2-*` commands have none. +//! least 8 bytes, and the output is 4 bytes longer. `encrypt` has the SP 800-67 Rev 2 Sec 3.4 +//! bound the other `tdes-*` encrypting commands have, counted as blocks TDEA processes: TW makes +//! six calls per 4-byte semiblock, so the input may be at most 699,048 bytes +//! ([`TDEA_LIMIT_TKW`]). TKW is approved for legacy systems only (SP 800-38F Sec 8), and two-key +//! TDEA may not wrap at all (SP 800-131A Rev 2 Table 6), so `tdes2-tkw` has no `encrypt`, as the +//! other `tdes2-*` commands have none. use crate::aes_kw_cmd::{run, run_with}; -use crate::helpers::block_mode_helpers::{CipherDirection, DecryptOnlyAction, load_key_object}; +use crate::helpers::block_mode_helpers::{ + CipherDirection, DecryptOnlyAction, TDEA_LIMIT_TKW, load_key_object, +}; use bouncycastle::cipher::modes::TKW; use bouncycastle::core::symmetric_key::KeyUnwrappingKey; use bouncycastle::tdes::{KEY_LEN, KEY_LEN_2KEY, TDES2Key, TDES3Key}; @@ -23,6 +28,7 @@ pub(crate) fn tdes_tkw_cmd( run::<_, KEY_LEN>( direction, &load_key_object::, KEY_LEN>(key, key_file, "TDES"), + Some(&TDEA_LIMIT_TKW), output_hex, "TDES-TKW", ); @@ -36,5 +42,5 @@ pub(crate) fn tdes2_tkw_cmd( ) { let DecryptOnlyAction::Decrypt = action; let kek = load_key_object::, KEY_LEN_2KEY>(key, key_file, "two-key TDES"); - run_with(|input| kek.unwrap(input), output_hex, "two-key TDES-TKW"); + run_with(|input| kek.unwrap(input), None, output_hex, "two-key TDES-TKW"); } diff --git a/cli/tests/test_tdes.sh b/cli/tests/test_tdes.sh index f718b7ee7..0329020a2 100755 --- a/cli/tests/test_tdes.sh +++ b/cli/tests/test_tdes.sh @@ -2,11 +2,13 @@ # The tdes-* subcommands' data limit, end to end through the binary. SP 800-67 Rev 2 Sec 3.4 # allows one key bundle 2^20 64-bit blocks (8 MiB) of protection; `encrypt` refuses input past # that with the usual `Error:` line, before any byte past the bound is processed. CBC's PKCS7 -# padding adds up to a block, so its bound is one byte lower; the stream modes take exactly -# 8 MiB. CTR never reaches it: its 6-byte nonce leaves a 16-bit counter, so the library's own -# keystream limit (2^16 blocks, 512 KiB) binds first, as `DataLimitExceeded`. Decryption is not -# limited. This lives here rather than in `tdes_cli_tests.rs` because 8 MiB through a debug build -# of the bit-sliced engine takes a minute or two per run, and this file makes five. +# padding adds up to a block, so its bound is one byte lower; CFB64 takes exactly 8 MiB. The +# bound counts the blocks TDEA processes, so CFB8 (one block per byte) stops at 1 MiB and TKW +# (six calls per 4-byte semiblock) at 699,048 bytes. CTR never reaches it: its 6-byte nonce +# leaves a 16-bit counter, so the library's own keystream limit (2^16 blocks, 512 KiB) binds +# first, as `DataLimitExceeded`. Decryption is not limited. This lives here rather than in +# `tdes_cli_tests.rs` because 8 MiB through a debug build of the bit-sliced engine takes a minute +# or two per run. source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" @@ -27,13 +29,27 @@ test_cbc_takes_a_message_that_pads_to_the_limit() { "$BC_RUST" tdes-cbc -d encrypt --key "$KEY" < <(zeros $((LIMIT - 1))) >/dev/null } -test_cfb_modes_refuse_one_byte_past_the_limit() { - local cmd - for cmd in tdes-cfb tdes-cfb8; do - expect_fail "$cmd: 2^20 blocks plus a byte" \ - "$BC_RUST" "$cmd" -d encrypt --key "$KEY" < <(zeros $((LIMIT + 1))) - assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" - done +test_cfb_refuses_one_byte_past_the_limit() { + expect_fail "tdes-cfb: 2^20 blocks plus a byte" \ + "$BC_RUST" tdes-cfb -d encrypt --key "$KEY" < <(zeros $((LIMIT + 1))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cfb8_counts_a_block_per_byte() { + expect_ok "tdes-cfb8: 2^20 bytes is 2^20 blocks" \ + "$BC_RUST" tdes-cfb8 -d encrypt --key "$KEY" < <(zeros $((1 << 20))) >/dev/null + expect_fail "tdes-cfb8: 2^20 bytes plus one" \ + "$BC_RUST" tdes-cfb8 -d encrypt --key "$KEY" < <(zeros $(((1 << 20) + 1))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_tkw_counts_six_blocks_per_semiblock() { + local bound=$(((1 << 20) / 6 * 4)) + expect_ok "tdes-tkw: $bound bytes is 2^20 blocks" \ + "$BC_RUST" tdes-tkw -d encrypt --key "$KEY" < <(zeros "$bound") >/dev/null + expect_fail "tdes-tkw: one semiblock more" \ + "$BC_RUST" tdes-tkw -d encrypt --key "$KEY" < <(zeros $((bound + 4))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" } test_ctr_runs_out_of_counter_first() { diff --git a/crypto/cipher/src/modes/tkw.rs b/crypto/cipher/src/modes/tkw.rs index cc89e75bc..a71cb7314 100644 --- a/crypto/cipher/src/modes/tkw.rs +++ b/crypto/cipher/src/modes/tkw.rs @@ -14,7 +14,8 @@ //! SP 800-38F Sec 8: TKW-AE and TKW-AD "are approved to support legacy systems but should not be //! used for new applications". Its forgery probability is 2^-32 per attempt against KW's 2^-64 //! (Appendix A.3), and TDEA itself is a legacy algorithm; see the `bouncycastle-tdes` crate's -//! security considerations. Two-key TDEA may not wrap at all: a permutation that decrypts only +//! security considerations. Two-key TDEA may not wrap at all (SP 800-131A Rev 2 Table 6): a +//! permutation that decrypts only //! ([`BlockPermutation::DECRYPT_ONLY`]) refuses to make a wrapper at compile time, as the //! encrypting modes refuse their encryptors, while its unwrapper works -- and the unwrapper, //! `Tkw<'k, P, Decrypting>`, is a distinct type with no wrapping method. diff --git a/crypto/tdes/src/lib.rs b/crypto/tdes/src/lib.rs index 0bc96dc82..3f521607d 100644 --- a/crypto/tdes/src/lib.rs +++ b/crypto/tdes/src/lib.rs @@ -220,16 +220,20 @@ //! 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. +//! "Disallowed". Its Table 6 says the same of key wrapping: two-key wrapping "Disallowed", +//! three-key wrapping "Deprecated through 2023, Disallowed after 2023" unless other NIST guidance +//! allows it, and unwrapping under either "Legacy use". SP 800-57 Part 1 Rev 5, Table 2, rates +//! 3TDEA at 112 bits of security, which is what [`TDESInternal`] reports as its maximum strength. //! //! This crate exists so that data and protocols that still depend on TDEA can be handled. Do not //! choose it for a new design; use AES from `bouncycastle-aes`. //! //! `BlockPermutation::DECRYPT_ONLY` records this library's policy, not NIST's status: it is set //! for two-key TDEA alone. Three-key TDEA encryption, which SP 800-131A Rev 2 also disallows -//! after 2023, stays available here because the OpenSSL provider this library serves exposes it; -//! a deployment that must not encrypt under TDEA should not use [`TDES3Key`]'s encrypting roles. +//! after 2023, stays available here because the OpenSSL provider this library serves exposes it, +//! and three-key TKW wrapping, the one SP 800-38F key wrap over TDEA, is kept on the same terms; +//! a deployment that must not encrypt or wrap under TDEA should not use [`TDES3Key`]'s +//! encrypting and wrapping roles. //! //! ## The 64-bit block, and the data limit //! diff --git a/crypto/tdes/src/tkw.rs b/crypto/tdes/src/tkw.rs index 82573b80b..ad9b98b2e 100644 --- a/crypto/tdes/src/tkw.rs +++ b/crypto/tdes/src/tkw.rs @@ -55,7 +55,7 @@ use bouncycastle_cipher::modes::Tkw; pub type TDES_TKW<'k, Dir> = Tkw<'k, TDESInternal, Dir>; /// Two-key TDEA Key Wrap, unwrapping only: what a [`TDES2Key`](crate::TDES2Key) hands out -/// through `KeyUnwrappingKey`. Two-key TDEA may not apply protection (SP 800-131A Rev 2 Table 1), +/// through `KeyUnwrappingKey`. Two-key TDEA may not wrap (SP 800-131A Rev 2 Table 6: "Disallowed"), /// and its engine decrypts only, so the key's `create_wrapper` and every wrapping one-shot refuse /// to compile, exactly as its `create_encryptor` does under the cipher modes: /// From c028af9ae919e7da0a7834d0646ad42952d9b11b Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 7 Nov 2026 04:18:12 +1100 Subject: [PATCH 31/32] cipher, core, cli, tdes, dev_scripts: the round-3 nits of the PR #139 review Six small items, none a behaviour change in the library: - `KeyStreamPolicy`'s doc said every `StreamCipher` comes from this crate's keystreams and named a resume route; since the ChaCha20 merge `hazmat::stream_cipher_over` builds one over an outside keystream, and since #173 nothing resumes. It now says what the unsealed `PermutationFreeKeyStream` marker is: the implementor's promise that no decrypt-only permutation is under the keystream. `DECRYPT_ONLY`'s doc loses its resume clause the same way and names every constructor of an encrypting worker, a key wrapper included, and cites Table 6 beside Table 1. - The `tdes-ctr` and `tdes2-ctr` nonce-file help said "no hex decoding", though `--hex-input` applies to it as to every other file; it now reads as the other IV files' help does. - The two `collapsible_if`s in the bounded stream helpers are let chains, so `cargo clippy -p cli -- -D warnings` fails only on the two lints that come from the release base. - `check_aes_ct_asm.sh` fails when a checked or control body is not found, where an empty body passed vacuously, and on thumbv6m each unbarriered control must show its own multiply, where the three were pooled; on rv32i, which has no multiplier and lowers the small constant multiplies to shifts, the pooled check stays, and the header says why. Both targets pass. - `tkw_reference_tests.rs` pins the TKW-AD lower bound: an 8-byte ciphertext and a 4-byte plaintext are `InvalidInputLength`, which the shared suite's bad lengths never reach. - `test_tdes.sh` now drives `tdes-ecb` at and one block past the bound, `tdes-cfb` and `tdes-ctr` at theirs, and the two-key ECB and CFB decrypts past it: 10 cases, all passing. cargo test for cipher, tdes and cli 324 passed; rustdoc warning-free. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/helpers/block_mode_helpers.rs | 10 ++--- cli/src/helpers/stream_mode_helpers.rs | 10 ++--- cli/src/main.rs | 18 ++++----- cli/tests/test_tdes.sh | 26 ++++++++++++- crypto/cipher/src/stream.rs | 12 +++--- .../core/src/hazmat/electronic_code_book.rs | 11 +++--- crypto/tdes/tests/tkw_reference_tests.rs | 25 ++++++++++++ dev_scripts/check_aes_ct_asm.sh | 39 ++++++++++++++++--- 8 files changed, 115 insertions(+), 36 deletions(-) diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index 2add921fe..3315cf269 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -360,11 +360,11 @@ pub(crate) fn encrypt_stream_bounded limit.max_bytes { - eprintln!("Error: {}", limit.message); - exit(-1); - } + if let Some(limit) = limit + && seen > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); } // Cannot fail: `out` has room for every whole block of `data` plus the fewer than // `BLOCK_LEN` bytes held over from the last call, and none of these modes has a per-IV diff --git a/cli/src/helpers/stream_mode_helpers.rs b/cli/src/helpers/stream_mode_helpers.rs index 7b32a3f44..f0f699fbe 100644 --- a/cli/src/helpers/stream_mode_helpers.rs +++ b/cli/src/helpers/stream_mode_helpers.rs @@ -63,11 +63,11 @@ pub(crate) fn encrypt_stream<'k, K, const KEY_LEN: usize, const INIT_DATA_LEN: u let mut seen: u64 = 0; stream(|data| { seen += data.len() as u64; - if let Some(limit) = limit { - if seen > limit.max_bytes { - eprintln!("Error: {}", limit.message); - exit(-1); - } + if let Some(limit) = limit + && seen > limit.max_bytes + { + eprintln!("Error: {}", limit.message); + exit(-1); } // Cannot fail: neither CFB nor CFB8 has a per-IV data limit. // CFB and CFB8 cannot fail here; CTR can, once its counter is exhausted, which is a real diff --git a/cli/src/main.rs b/cli/src/main.rs index edf25f187..a7518ae54 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3090,13 +3090,13 @@ enum Subcommands { /// 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. + /// or `--iv-file` (raw bytes; hex with --hex-input), the nonce is supplied instead and only + /// ciphertext is written or read; it MUST then be unique per message under one key bundle. The + /// remaining 2 bytes of the 8-byte counter block are the counter, so a single message is + /// limited to 65 536 blocks (512 KiB); longer input fails rather than repeating a counter + /// block. /// /// Input may be any length up to that limit: CTR is a stream cipher, so nothing is padded and /// the ciphertext is exactly as long as the plaintext. @@ -3124,8 +3124,8 @@ enum Subcommands { #[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. + /// A file containing the 6-byte nonce (raw bytes; hex with --hex-input). If both iv and + /// iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -3298,8 +3298,8 @@ enum Subcommands { #[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. + /// A file containing the 6-byte nonce (raw bytes; hex with --hex-input). If both iv and + /// iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, diff --git a/cli/tests/test_tdes.sh b/cli/tests/test_tdes.sh index 0329020a2..22a9a1bff 100755 --- a/cli/tests/test_tdes.sh +++ b/cli/tests/test_tdes.sh @@ -13,6 +13,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" KEY=0123456789abcdef23456789abcdef01456789abcdef0123 +KEY2=0123456789abcdef23456789abcdef01 LIMIT=$((1 << 23)) # `zeros N` writes N zero bytes. @@ -58,10 +59,33 @@ test_ctr_runs_out_of_counter_first() { assert_stderr_has "DataLimitExceeded" } +test_ecb_takes_the_limit_and_refuses_a_block_more() { + expect_ok "tdes-ecb: exactly 2^20 blocks" \ + "$BC_RUST" tdes-ecb -d encrypt --key "$KEY" < <(zeros "$LIMIT") >/dev/null + expect_fail "tdes-ecb: 2^20 blocks plus one" \ + "$BC_RUST" tdes-ecb -d encrypt --key "$KEY" < <(zeros $((LIMIT + 8))) + assert_stderr_has "Error: TDEA: one key bundle may protect at most 2^20 blocks" +} + +test_cfb_takes_exactly_the_limit() { + expect_ok "tdes-cfb: exactly 2^20 blocks" \ + "$BC_RUST" tdes-cfb -d encrypt --key "$KEY" < <(zeros "$LIMIT") >/dev/null +} + +test_ctr_takes_its_whole_counter_space() { + expect_ok "tdes-ctr: exactly 2^16 blocks" \ + "$BC_RUST" tdes-ctr -d encrypt --key "$KEY" < <(zeros $((1 << 19))) >/dev/null +} + test_decrypt_is_not_limited() { - # 2^20 + 1 blocks of ciphertext decrypt (ECB, no padding), since the limit is on protecting. + # 2^20 + 1 blocks of ciphertext decrypt (ECB, no padding; CFB with the IV read from the + # input), under either bundle, since the limit is on protecting. expect_ok "tdes-ecb decrypt past the limit" \ "$BC_RUST" tdes-ecb -d decrypt --key "$KEY" < <(zeros $((LIMIT + 8))) >/dev/null + expect_ok "tdes2-ecb decrypt past the limit" \ + "$BC_RUST" tdes2-ecb -d decrypt --key "$KEY2" < <(zeros $((LIMIT + 8))) >/dev/null + expect_ok "tdes2-cfb decrypt past the limit" \ + "$BC_RUST" tdes2-cfb -d decrypt --key "$KEY2" < <(zeros $((LIMIT + 16))) >/dev/null } run_all diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs index fe8f7410d..cc267f3a6 100644 --- a/crypto/cipher/src/stream.rs +++ b/crypto/cipher/src/stream.rs @@ -56,9 +56,10 @@ where /// Whether a keystream's permutation decrypts only (`BlockPermutation::DECRYPT_ONLY`), so that /// [`StreamCipher`] can refuse, at compile time, to be made in the encrypting direction over it, -/// resumed or otherwise. `KeyStream` itself is `dyn`-compatible and so carries no constant; this -/// sealed trait is implemented for the crate's keystream types alone, which is where every -/// `StreamCipher` comes from. +/// whatever constructor makes it. `KeyStream` itself is `dyn`-compatible and so carries no +/// constant; this sealed trait is implemented for the crate's keystream types. A keystream from +/// outside the crate reaches [`StreamCipher`] only through [`hazmat::stream_cipher_over`], whose +/// marker trait is the implementor's promise that no such permutation is under it. pub trait KeyStreamPolicy: sealed::Sealed { /// The permutation's `DECRYPT_ONLY`. const DECRYPT_ONLY: bool; @@ -308,8 +309,9 @@ pub mod hazmat { /// A keystream that runs over no [`BlockPermutation`](bouncycastle_core::hazmat::BlockPermutation), /// such as ChaCha20's, and so has no decrypt-only permutation for [`stream_cipher_over`] to - /// refuse. Implemented beside such a keystream's definition. The keystreams of this crate's - /// modes do not implement it, and no other crate can implement it for them. + /// refuse. Implemented beside such a keystream's definition, and deliberately unsealed: + /// implementing it is the implementor's promise that no permutation that decrypts only is + /// under the keystream. The keystreams of this crate's modes do not implement it. pub trait PermutationFreeKeyStream {} /// A [`StreamCipher`] over a keystream that is already constructed and positioned, for a diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs index ceb2815d9..2755ea21e 100644 --- a/crypto/core/src/hazmat/electronic_code_book.rs +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -36,14 +36,15 @@ pub trait BlockPermutation: Algorithm + Sized { fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); /// Whether this library allows the permutation to decrypt only: no encrypting worker may be - /// made over it, by a key's `create_encryptor`, by resuming a suspended state in the - /// encrypting direction, or otherwise. A mode checks this in an inline `const` wherever it - /// makes an encrypting worker, so the refusal is a compile error (reported after - /// monomorphization, so by `cargo build` or `cargo test` rather than `cargo check`). + /// made over it, by a key's `create_encryptor` or by any other constructor of one, a key + /// wrapper included. A mode checks this in an inline `const` in every such constructor, so + /// the refusal is a compile error (reported after monomorphization, so by `cargo build` or + /// `cargo test` rather than `cargo check`). /// /// This records the library's policy, not a standard's status. It is `true` for two-key TDEA /// alone, which NIST SP 800-131A Rev 2 Table 1 lists as "Disallowed" for encryption and - /// "Legacy use" for decryption. Three-key TDEA encryption, which the same table disallows + /// "Legacy use" for decryption, and whose key wrapping its Table 6 lists as "Disallowed" + /// too. Three-key TDEA encryption, which Table 1 disallows /// after 2023, keeps `false` because the OpenSSL provider this library serves still exposes /// it; the tdes crate's Security Considerations say so. /// diff --git a/crypto/tdes/tests/tkw_reference_tests.rs b/crypto/tdes/tests/tkw_reference_tests.rs index 4d9cd1602..be6f823ca 100644 --- a/crypto/tdes/tests/tkw_reference_tests.rs +++ b/crypto/tdes/tests/tkw_reference_tests.rs @@ -66,3 +66,28 @@ fn two_key_tkw_unwraps_what_algorithm_9_wrapped() { assert!(kek.unwrap(&ciphertext).is_err(), "{len} bytes, modified"); } } + +/// SP 800-38F Table 1: TKW-AD is defined on three or more semiblocks and TKW-AE on two or more. +/// The shared suite's bad lengths (0, one semiblock, one byte short) never reach the 8-byte +/// ciphertext or the 4-byte plaintext, which must be `InvalidInputLength`, not a run of the +/// algorithm ending in `DecryptionFailed`. +#[test] +fn tkw_refuses_a_two_semiblock_ciphertext_and_a_one_semiblock_plaintext() { + use bouncycastle_core::errors::SymmetricCipherError; + use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; + let kek = TDES3Key::::from_key_material(&material::()).unwrap(); + let mut out = [0u8; 16]; + assert!(matches!( + kek.create_unwrapper().unwrap_out(&[0u8; 8], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + kek.create_wrapper().wrap_out(&[0u8; 4], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + let kek2 = TDES2Key::::from_key_material(&material::()).unwrap(); + assert!(matches!( + kek2.create_unwrapper().unwrap_out(&[0u8; 8], &mut out), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); +} diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh index c110ca23d..f845e9cdb 100755 --- a/dev_scripts/check_aes_ct_asm.sh +++ b/dev_scripts/check_aes_ct_asm.sh @@ -3,9 +3,13 @@ # the TDES round function (`spread` in crypto/tdes/src/des.rs) compile to no multiply, call or # branch on the targets whose multiply is not fixed-latency. # Builds dev_scripts/aes_ct_harness with the nightly toolchain and `-Zbuild-std=core` (the -# targets have no shipped `core`), emits the assembly and greps the four `splat*` bodies: the -# barriered pair must be clean, and the plain pair must show the multiply the barrier prevents, -# so that a grep that matches nothing is noticed. Targets may be given as arguments. +# targets have no shipped `core`), emits the assembly and greps the checked bodies: the barriered +# `splat` pair and the TDES round function must be clean, every checked body must be found (an +# empty body would pass vacuously), and the unbarriered controls must show the multiply the +# barrier prevents, so that a grep that matches nothing is noticed -- each control on its own +# where the target has a multiplier (thumbv6m), pooled where it has none (rv32i lowers a small +# constant multiply to shifts, and only the 64-bit one calls `__muldi3`). Targets may be given as +# arguments. # # rustup toolchain install nightly --profile minimal --component rust-src # ./dev_scripts/check_aes_ct_asm.sh # thumbv6m-none-eabi riscv32i-unknown-none-elf @@ -57,7 +61,10 @@ for t in $TARGETS; do if [ "$kind" = all ]; then pat=$bad; what="multiply, call or branch"; else pat=$mul; what="multiply"; fi hits=$(body "$asm" "$f" | grep -cE "$pat" || true) n=$(body "$asm" "$f" | wc -l) - if [ "$hits" -ne 0 ]; then + if [ "$n" -eq 0 ]; then + echo "FAIL $t $f: not found in the assembly (renamed or inlined?), nothing was checked" + status=1 + elif [ "$hits" -ne 0 ]; then echo "FAIL $t $f: $hits $what line(s) in $n instructions" body "$asm" "$f" | grep -E "$pat" status=1 @@ -65,9 +72,29 @@ for t in $TARGETS; do echo "ok $t $f: no $what in $n instructions" fi done - control=$(for f in splat32_plain splat64_plain spread_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) + # The controls: the same code with the barrier off must show a multiply, or the grep proves + # nothing. Each control on its own where the target has a multiplier; pooled where it has none. + control=0 + for f in splat32_plain splat64_plain spread_plain; do + n=$(body "$asm" "$f" | wc -l) + hits=$(body "$asm" "$f" | grep -cE "$mul" || true) + if [ "$n" -eq 0 ]; then + echo "FAIL $t $f: control not found in the assembly" + status=1 + fi + case "$t" in + thumb*|arm*) + if [ "$hits" -eq 0 ]; then + echo "FAIL $t $f: the unbarriered body shows no multiply, so the grep proves nothing" + status=1 + else + echo "ok $t $f: control shows the multiply ($hits line(s))" + fi ;; + esac + control=$((control + hits)) + done if [ "$control" -eq 0 ]; then - echo "FAIL $t: the unbarriered bodies show no multiply, so the grep proves nothing" + echo "FAIL $t: no unbarriered body shows a multiply, so the grep proves nothing" status=1 else echo "ok $t control: the unbarriered bodies show the multiply ($control line(s))" From e719c94d8b301e9e5b57158f29889a1bfa70d718 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 7 Nov 2026 04:18:12 +1100 Subject: [PATCH 32/32] Release notes: TDES The 0.1.4 notes had no entry for this branch. One bullet names the engine, the modes and TKW, the two-key decrypt-only rule and the CLI commands with their SP 800-67 Rev 2 bound as it is now counted. Assisted-by: Claude:claude-mythos-5-1 --- alpha_0.1.4_release_notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/alpha_0.1.4_release_notes.md b/alpha_0.1.4_release_notes.md index 0030c06b3..bdf87fa74 100644 --- a/alpha_0.1.4_release_notes.md +++ b/alpha_0.1.4_release_notes.md @@ -3,6 +3,12 @@ ## Major features * CHACHA20 -- ChaCha20, Poly1305 and ChaCha20-Poly1305 (RFC 8439). +* TDES -- three-key TDEA (NIST SP 800-67 Rev 2) as a constant-time, table-free bit-sliced engine under `hazmat`, + with ECB, CBC, CFB64, CFB8 and CTR (SP 800-38A Appendix E) and TDEA Key Wrap (SP 800-38F TKW) through the key + role traits; two-key TDEA decrypts and unwraps only, which `BlockPermutation::DECRYPT_ONLY` makes a compile + error for every encrypting route; the `tdes-*` and `tdes2-*` CLI commands, whose `encrypt` refuses input past + SP 800-67 Rev 2 Sec 3.4's 2^20 blocks counted as the blocks TDEA processes (8 MiB; 1 MiB for CFB8, 699,048 + bytes for TKW). * AES Key Wrap -- AES_KW and AES_KWP (NIST SP 800-38F; RFC 3394 / RFC 5649) over any 128-bit block permutation, with the `KeyWrapper` / `KeyUnwrapper` core traits and the `aes*-kw` / `aes*-kwp` CLI commands. * ECDSA -- FIPS 186-5 signatures over P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1 and