From e5315367271f4e596bae681b889d89561be8a745 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:00:27 +1000 Subject: [PATCH 01/23] 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 211cb9220..1a41e262f 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 8d2ae6e1fd399f38b6dc4650f8847856a4b1af9f Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:00:28 +1000 Subject: [PATCH 02/23] 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 cc3b9ec71..1c2d6369b 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -71,11 +71,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 @@ -271,7 +268,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, @@ -285,6 +282,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 @@ -295,7 +293,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| { @@ -310,7 +308,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, @@ -345,7 +343,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. @@ -382,7 +380,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 fb72d187c08a230dc05409716edf3a7550baaf49 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 03/23] 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 48ea3fcf7d4e48ce45e0b0e78bc5d3be76d8fae7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 04/23] 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 ff57e12db62a8f5698c2e05887ab4b32895506ce Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 8 Sep 2026 18:42:32 +1000 Subject: [PATCH 05/23] 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 | 2 + mem_usage_benches/src/bench_tdes_mem_usage.rs | 106 +++++ mem_usage_benches/src/lib.rs | 1 + src/lib.rs | 1 + 35 files changed, 3943 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 2de6cf6ec..b629b6b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } bouncycastle-sm3 = { path = "./crypto/sm3" } +bouncycastle-tdes = { path = "./crypto/tdes" } bouncycastle-utils = { path = "./crypto/utils" } @@ -75,3 +76,4 @@ bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true bouncycastle-sm3.workspace = true +bouncycastle-tdes.workspace = true diff --git a/cli/src/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 804cbe57a..384bb3812 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -34,3 +34,5 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" +name = "bench_tdes_mem_usage" +path = "src/bench_tdes_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 8e86dc3c5..3d0bda0ed 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -5,3 +5,4 @@ mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha2_mem_usage; mod bench_sha3_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 a7430f14dd07c30df960de62e9e2a2909b300d15 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 19 Sep 2026 10:14:31 +1000 Subject: [PATCH 06/23] 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 63ca623b8d8f436c9ed3a530dfc09b80a19f9250 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 19 Sep 2026 10:47:56 +1000 Subject: [PATCH 07/23] 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 f585745f357bc1f1087e66899a4047cc817cddb1 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 20 Sep 2026 11:59:36 +1000 Subject: [PATCH 08/23] 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 d9e778113d1b4db64559ed63dc931bc197995eec Mon Sep 17 00:00:00 2001 From: David Hook Date: Mon, 28 Sep 2026 20:28:52 +1000 Subject: [PATCH 09/23] 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 ++- mem_usage_benches/Cargo.toml | 2 ++ 13 files changed, 103 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)". diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index 384bb3812..cbf29efe0 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -34,5 +34,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] 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" From 0f1ff89606e746df1e225629ab358bf58fba2cbd Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 29 Sep 2026 10:36:12 +1000 Subject: [PATCH 10/23] 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 938791667875b3a06280eb78cd60e955eea793d5 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 1 Oct 2026 15:28:09 +1000 Subject: [PATCH 11/23] 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 2633b4d762aca49afc050148507a132e5a318db2 Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 1 Oct 2026 16:20:32 +1000 Subject: [PATCH 12/23] 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 494715b7ce1e870856aa8e6e15a40df42a2cf8a4 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 24 Oct 2026 21:43:44 +1100 Subject: [PATCH 13/23] 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 1f21fe5bcb47b0377c3f1366a73b82112fe81358 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 24 Oct 2026 22:00:22 +1100 Subject: [PATCH 14/23] 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 | 319 +++++++++++++++++- 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, 1834 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 1c2d6369b..0ba7a140c 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -85,6 +85,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` (hex) or `--key-file` (binary or hex), and checks its length. /// /// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the diff --git a/cli/src/main.rs b/cli/src/main.rs index c443f6fd3..427b28740 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -16,12 +16,22 @@ 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::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; use crate::sha2_cmd::SHA2Variant; 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)] @@ -1697,6 +1707,283 @@ enum Subcommands { x: bool, }, + /// Triple DES (three-key TDEA, NIST SP 800-67 Rev 2) in CBC mode (SP 800-38A Sec 6.2), + /// 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. There is deliberately no `--iv` flag. + /// + /// Input must be a whole number of 8-byte blocks: CBC is defined only on whole blocks and this + /// command applies no padding, so unaligned input is rejected rather than padded. + /// + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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. There is + /// deliberately no `--iv` flag. + /// + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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. 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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; the rest must + /// be a whole number of 8-byte blocks and is decrypted without unpadding. + /// + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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) and the + /// any-length input rule. + TDES2_CFB { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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) and the + /// 512 KiB per-message limit. + TDES2_CTR { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// 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. @@ -2034,6 +2321,36 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } + Some(Subcommands::TDES_CBC { direction, key, key_file, x }) => { + tdes_cbc_cmd::tdes_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES_CFB { direction, key, key_file, x }) => { + tdes_cfb_cmd::tdes_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES_CFB8 { direction, key, key_file, x }) => { + tdes_cfb8_cmd::tdes_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::TDES_CTR { direction, key, key_file, x }) => { + tdes_ctr_cmd::tdes_ctr_cmd(direction, key, key_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, x }) => { + tdes2_cbc_cmd::tdes2_cbc_cmd(action, key, key_file, *x); + } + Some(Subcommands::TDES2_CFB { action, key, key_file, x }) => { + tdes2_cfb_cmd::tdes2_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::TDES2_CFB8 { action, key, key_file, x }) => { + tdes2_cfb8_cmd::tdes2_cfb8_cmd(action, key, key_file, *x); + } + Some(Subcommands::TDES2_CTR { action, key, key_file, x }) => { + tdes2_ctr_cmd::tdes2_ctr_cmd(action, key, key_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, customization, x }) => { sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); } 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 133d7c9d42df5e007c003dd5621b0ee32666156d Mon Sep 17 00:00:00 2001 From: David Hook Date: Sun, 25 Oct 2026 18:33:57 +1100 Subject: [PATCH 15/23] 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 fc0547e82d55a14cf2b9293bc36ff5da8eedddb3 Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 19:58:28 +1100 Subject: [PATCH 16/23] 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 2d83b84caa9f3ffb6e9282d3f4372b0d2bea75eb Mon Sep 17 00:00:00 2001 From: David Hook Date: Wed, 28 Oct 2026 19:58:28 +1100 Subject: [PATCH 17/23] 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/main.rs | 162 ++++++++++++---- 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 ++++++++++++++++++++++++++++++++++++ 12 files changed, 586 insertions(+), 92 deletions(-) create mode 100644 cli/tests/tdes_cli_tests.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 427b28740..6c46fad59 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1707,16 +1707,19 @@ enum Subcommands { x: bool, }, - /// Triple DES (three-key TDEA, NIST SP 800-67 Rev 2) in CBC mode (SP 800-38A Sec 6.2), - /// 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. + /// 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. There is deliberately no `--iv` flag. + /// directly in a pipeline. With `--iv` (hex) or `--iv-file` (raw bytes), 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 must be a whole number of 8-byte blocks: CBC is defined only on whole blocks and this - /// command applies no padding, so unaligned input is rejected rather than padded. + /// 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. @@ -1728,6 +1731,7 @@ enum Subcommands { /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. TDES_CBC { + #[arg(short, long)] direction: CipherDirection, /// The 24-byte TDES key bundle in hex. @@ -1740,6 +1744,16 @@ enum Subcommands { #[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, 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, @@ -1752,14 +1766,16 @@ enum Subcommands { /// 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. There is - /// deliberately no `--iv` flag. + /// output; on `decrypt` it is read back from the first 8 bytes of the input. With `--iv` (hex) + /// or `--iv-file` (raw bytes), 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. @@ -1772,6 +1788,16 @@ enum Subcommands { #[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, 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, @@ -1787,6 +1813,7 @@ enum Subcommands { /// 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. @@ -1799,6 +1826,16 @@ enum Subcommands { #[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, 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, @@ -1808,9 +1845,11 @@ enum Subcommands { /// 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. 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. + /// 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. @@ -1820,6 +1859,7 @@ enum Subcommands { /// 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. @@ -1832,6 +1872,16 @@ enum Subcommands { #[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, @@ -1854,6 +1904,7 @@ enum Subcommands { /// /// See `tdes-cbc` for the key format. TDES_ECB { + #[arg(short, long)] direction: CipherDirection, /// The 24-byte TDES key bundle in hex. @@ -1875,8 +1926,8 @@ enum Subcommands { /// 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; the rest must - /// be a whole number of 8-byte blocks and is decrypted without unpadding. + /// 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). @@ -1884,6 +1935,7 @@ enum Subcommands { /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. TDES2_CBC { + #[arg(short = 'd', long = "direction")] action: DecryptOnlyAction, /// The 16-byte two-key TDES bundle in hex. @@ -1896,15 +1948,26 @@ enum Subcommands { #[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, 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 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) and the - /// any-length input rule. + /// 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. @@ -1917,6 +1980,16 @@ enum Subcommands { #[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, 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, @@ -1925,6 +1998,7 @@ enum Subcommands { /// 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. @@ -1937,15 +2011,26 @@ enum Subcommands { #[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, 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 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) and the - /// 512 KiB per-message limit. + /// 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. @@ -1958,6 +2043,16 @@ enum Subcommands { #[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, @@ -1967,6 +2062,7 @@ enum Subcommands { /// 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. @@ -2321,32 +2417,32 @@ fn run() { Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } - Some(Subcommands::TDES_CBC { direction, key, key_file, x }) => { - tdes_cbc_cmd::tdes_cbc_cmd(direction, key, key_file, *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, x }) => { - tdes_cfb_cmd::tdes_cfb_cmd(direction, key, key_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, x }) => { - tdes_cfb8_cmd::tdes_cfb8_cmd(direction, key, key_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, x }) => { - tdes_ctr_cmd::tdes_ctr_cmd(direction, key, key_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, x }) => { - tdes2_cbc_cmd::tdes2_cbc_cmd(action, 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, x }) => { - tdes2_cfb_cmd::tdes2_cfb_cmd(action, key, key_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, x }) => { - tdes2_cfb8_cmd::tdes2_cfb8_cmd(action, key, key_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, x }) => { - tdes2_ctr_cmd::tdes2_ctr_cmd(action, key, key_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); 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 2fe44bfa396c3a72fe290cf96f3aeadf9bc66f56 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 01:34:57 +1100 Subject: [PATCH 18/23] 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 1cf33b102f467601ab18fc9ed4f7f69612841a21 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 01:34:57 +1100 Subject: [PATCH 19/23] 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 cabef7903835b000a5453d14c5873b151b3d5159 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 03:30:42 +1100 Subject: [PATCH 20/23] 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 f663a18120ea2ec99718f79ecebc46a16d5a196b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 13:09:56 +1100 Subject: [PATCH 21/23] 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 8389606754dbc210eb0eb772688ff2473af33c4b Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 13:34:12 +1100 Subject: [PATCH 22/23] 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 09a7620f194e3dd817007111c48e016b2d81a00b Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 5 Nov 2026 06:36:02 +1100 Subject: [PATCH 23/23] cli: the TDES commands' help takes the AES wording for the input rule -- key-bundle and IV files are raw bytes, or hex under the global --hex-input (as PR #142's second round) Assisted-by: Claude:claude-fable-5-1 --- cli/src/main.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 5ed53d8be..e4b5ffb0a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2375,7 +2375,7 @@ enum Subcommands { /// /// 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), the IV is supplied + /// 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`. /// @@ -2401,7 +2401,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte TDES key bundle, in binary or hex. + /// 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, @@ -2411,7 +2411,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2429,7 +2429,7 @@ enum Subcommands { /// /// 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), the IV is supplied instead and only ciphertext is written or + /// 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 @@ -2445,7 +2445,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte TDES key bundle, in binary or hex. + /// 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, @@ -2455,7 +2455,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2483,7 +2483,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte TDES key bundle, in binary or hex. + /// 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, @@ -2493,7 +2493,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2529,7 +2529,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte TDES key bundle, in binary or hex. + /// 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, @@ -2574,7 +2574,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte TDES key bundle, in binary or hex. + /// 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, @@ -2605,7 +2605,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte two-key TDES bundle, in binary or hex. + /// 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, @@ -2615,7 +2615,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2637,7 +2637,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte two-key TDES bundle, in binary or hex. + /// 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, @@ -2647,7 +2647,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2668,7 +2668,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte two-key TDES bundle, in binary or hex. + /// 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, @@ -2678,7 +2678,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 8-byte IV, as raw bytes exactly as they are (no hex decoding). + /// 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, @@ -2700,7 +2700,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte two-key TDES bundle, in binary or hex. + /// 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, @@ -2732,7 +2732,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte two-key TDES bundle, in binary or hex. + /// 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,