From 617530564118db65f97ee420e2f5e7beb38459a9 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Fri, 17 Oct 2025 17:14:43 +0300 Subject: [PATCH] feat(des): implement basic encryption --- cipher-core/src/error.rs | 6 +- cipher-core/src/lib.rs | 8 +- cipher-core/src/traits.rs | 200 +++-------------------------------- cipher-core/src/types.rs | 5 + des/src/block/block32.rs | 47 +++++++++ des/src/block/block48.rs | 71 +++++++++++++ des/src/block/block6.rs | 36 +++++++ des/src/block/block64.rs | 76 ++++++++++++++ des/src/block/lr.rs | 43 ++++++++ des/src/block/mod.rs | 7 ++ des/src/constants.rs | 17 +++ des/src/des.rs | 212 +++++++++++++++++++++++++++++++++++++- des/src/key/cd56.rs | 34 ++++++ des/src/key/des_key.rs | 55 ++++++++++ des/src/key/key56.rs | 23 +++-- des/src/key/mod.rs | 4 +- des/src/key/secret_int.rs | 7 -- des/src/key/subkey.rs | 7 ++ des/src/key/subkeys.rs | 141 ++++++++++++++----------- des/src/lib.rs | 1 + des/src/utils.rs | 22 +--- des/tests/des.rs | 139 +++++++++++++++++++++++++ 22 files changed, 866 insertions(+), 295 deletions(-) create mode 100644 cipher-core/src/types.rs create mode 100644 des/src/block/block32.rs create mode 100644 des/src/block/block48.rs create mode 100644 des/src/block/block6.rs create mode 100644 des/src/block/block64.rs create mode 100644 des/src/block/lr.rs create mode 100644 des/src/block/mod.rs create mode 100644 des/src/key/cd56.rs create mode 100644 des/src/key/des_key.rs create mode 100644 des/tests/des.rs diff --git a/cipher-core/src/error.rs b/cipher-core/src/error.rs index 8f4ebdb..e3d9ef9 100644 --- a/cipher-core/src/error.rs +++ b/cipher-core/src/error.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] -pub enum CryptoError { +pub enum CipherError { /// Invalid key size for the cipher #[error("invalid key size: expected {expected} bytes, got {actual}")] InvalidKeySize { expected: usize, actual: usize }, @@ -23,7 +23,7 @@ pub enum CryptoError { InvalidSize { expected: usize, actual: usize }, } -impl CryptoError { +impl CipherError { /// Creates a key size error #[inline] #[must_use] @@ -97,4 +97,4 @@ impl CryptoError { } /// Type alias for clean Result types -pub type CryptoResult = core::result::Result; +pub type CipherResult = core::result::Result; diff --git a/cipher-core/src/lib.rs b/cipher-core/src/lib.rs index 6a637f8..4d15cd1 100644 --- a/cipher-core/src/lib.rs +++ b/cipher-core/src/lib.rs @@ -1,5 +1,9 @@ mod error; mod traits; +mod types; -pub use error::{CryptoError, CryptoResult}; -pub use traits::{BlockCipher, BlockLike, CipherContext, KeyInit, KeyLike, StreamCipher}; +pub use { + error::{CipherError, CipherResult}, + traits::BlockCipher, + types::CipherAction, +}; diff --git a/cipher-core/src/traits.rs b/cipher-core/src/traits.rs index 6def9c3..53609ec 100644 --- a/cipher-core/src/traits.rs +++ b/cipher-core/src/traits.rs @@ -1,192 +1,24 @@ -use crate::CryptoResult; +use crate::{CipherAction, CipherError, CipherResult}; -/// Minimal trait describing a fixed-size block-like value. -/// -/// Concrete block types (e.g. `Block`) should implement this. -pub trait BlockLike: Sized + Copy + Clone { - /// Size of the block in bytes. - const SIZE: usize; - - /// Create from exactly SIZE bytes. - /// - /// # Errors - /// - /// Returns a `CryptoError::InvalidBlockSize` (or equivalent) when - /// `bytes.len() != Self::SIZE`. - fn from_bytes(bytes: &[u8]) -> CryptoResult; - - /// Immutable view of the underlying bytes. - fn as_bytes(&self) -> &[u8]; - - /// Mutable view of underlying bytes. - fn as_bytes_mut(&mut self) -> &mut [u8]; - - /// Create a zeroed block value. - fn zeroed() -> Self; -} - -/// Minimal trait describing a fixed-size key-like value. -/// -/// Concrete key types (e.g. `Secret`) should implement this. -pub trait KeyLike: Sized { - /// Size of the key in bytes. - const SIZE: usize; - - /// Create key from exactly SIZE bytes. - /// - /// # Errors - /// - /// Returns a `CryptoError::InvalidKeySize` when `bytes.len() != Self::SIZE`. - fn from_bytes(bytes: &[u8]) -> CryptoResult; - /// Immutable view of the key bytes. - fn as_bytes(&self) -> &[u8]; -} - -/// Core block-cipher trait using [`KeyLike`] and [`BlockLike`]. -/// -/// The primary (performance-oriented) methods are the in-place variants. -/// Default owned-returning methods are implemented in terms of the in-place -/// ones and therefore require `BlockLike: Copy`. pub trait BlockCipher: Sized { - /// Key and Block concrete associated types - type Key: KeyLike; - type Block: BlockLike; + const BLOCK_SIZE: usize; - /// Construct a cipher instance from a key. - /// - /// # Errors - /// - /// Returns a `CryptoError` if the key is invalid for the cipher. - fn new(key: Self::Key) -> CryptoResult; + fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult>; - /// Encrypt in-place (primary implementation target). - /// - /// # Errors - /// - /// Returns a `CryptoError` on failure (e.g. internal state issues). - fn encrypt_inplace(&self, block: &mut Self::Block) -> CryptoResult<()>; - - /// Decrypt in-place (primary implementation target). - /// - /// # Errors - /// - /// Returns a `CryptoError` on failure (e.g. invalid padding after decrypt). - fn decrypt_inplace(&self, block: &mut Self::Block) -> CryptoResult<()>; - - /// Encrypt returning a new block. - /// - /// Default implementation copies the input block and calls - /// `encrypt_inplace`. - /// - /// # Errors - /// - /// Propagates errors returned by `encrypt_inplace`. - fn encrypt(&self, block: &Self::Block) -> CryptoResult { - let mut out = *block; - self.encrypt_inplace(&mut out)?; - Ok(out) + fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult> { + if block.len() != Self::BLOCK_SIZE { + return Err(CipherError::invalid_block_size( + Self::BLOCK_SIZE, + block.len(), + )); + } + self.transform_impl(block, action) } - /// Decrypt returning a new block. - /// - /// Default implementation copies the input block and calls - /// `decrypt_inplace`. - /// - /// # Errors - /// - /// Propagates errors returned by `decrypt_inplace`. - fn decrypt(&self, block: &Self::Block) -> CryptoResult { - let mut out = *block; - self.decrypt_inplace(&mut out)?; - Ok(out) - } -} - -/// Helper trait: initialize a cipher from raw key bytes. -/// -/// The default implementation converts the slice into `Self::Key` then calls -/// `Self::new`. Implementations may override to perform custom validation. -pub trait KeyInit: BlockCipher + Sized { - /// Construct the cipher from raw key bytes. - /// - /// # Errors - /// - /// Returns `CryptoError::InvalidKeySize` if the slice length doesn't match - fn new_from_slice(key_bytes: &[u8]) -> CryptoResult { - let key = ::from_bytes(key_bytes)?; - Self::new(key) - } -} - -/// Stream-like cipher/mode trait (CTR, OFB, stream ciphers). -/// -/// Implementations apply keystream to arbitrary-length buffers in-place. -pub trait StreamCipher { - /// XOR keystream with `data` in-place (encrypt == decrypt). - fn apply_keystream(&mut self, data: &mut [u8]); -} - -/// Small convenience wrapper that stores a cipher and forwards single-block -/// operations using the associated Block type. -pub struct CipherContext -where - C: BlockCipher, -{ - cipher: C, -} - -impl CipherContext -where - C: BlockCipher, -{ - /// Wrap an existing cipher instance. - pub const fn new(cipher: C) -> Self { - Self { cipher } - } - - /// Encrypt a block, returning a new block. - /// - /// # Errors - /// - /// Propagates errors from the cipher. - pub fn encrypt(&self, block: &C::Block) -> CryptoResult { - self.cipher.encrypt(block) - } - - /// Encrypt a block in-place. - /// - /// # Errors - /// - /// Propagates errors from the cipher. /// Encrypt a block in-place. - pub fn encrypt_inplace(&self, block: &mut C::Block) -> CryptoResult<()> { - self.cipher.encrypt_inplace(block) - } - - /// Decrypt a block, returning a new block. - /// - /// # Errors - /// - /// Propagates errors from the cipher. /// Decrypt a block, returning a new block. - pub fn decrypt(&self, block: &C::Block) -> CryptoResult { - self.cipher.decrypt(block) - } - - /// Decrypt a block in-place. - /// - /// # Errors - /// - /// Propagates errors from the cipher. /// Decrypt a block in-place. - pub fn decrypt_inplace(&self, block: &mut C::Block) -> CryptoResult<()> { - self.cipher.decrypt_inplace(block) - } - - /// Access underlying cipher - pub const fn cipher(&self) -> &C { - &self.cipher - } - - /// Consume and return underlying cipher - pub fn into_inner(self) -> C { - self.cipher + fn encrypt(&self, plaintext: &[u8]) -> CipherResult> { + self.transform(plaintext, CipherAction::Encrypt) + } + fn decrypt(&self, ciphertext: &[u8]) -> CipherResult> { + self.transform(ciphertext, CipherAction::Decrypt) } } diff --git a/cipher-core/src/types.rs b/cipher-core/src/types.rs new file mode 100644 index 0000000..b7dbae7 --- /dev/null +++ b/cipher-core/src/types.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CipherAction { + Encrypt, + Decrypt, +} diff --git a/des/src/block/block32.rs b/des/src/block/block32.rs new file mode 100644 index 0000000..d68f01d --- /dev/null +++ b/des/src/block/block32.rs @@ -0,0 +1,47 @@ +use std::ops::BitXor; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Block32(u32); + +impl Block32 { + const MASK: u32 = 0xFFFF_FFFF; + + #[inline] + #[must_use] + pub const fn new(value: u32) -> Self { + Self(value) + } + + #[inline] + #[must_use] + pub const fn as_u32(self) -> u32 { + self.0 + } + + #[inline] + #[must_use] + pub const fn as_u64(self) -> u64 { + self.0 as u64 + } +} + +impl From for Block32 { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for Block32 { + fn from(value: u64) -> Self { + let mask = u64::from(Self::MASK); + let value = u32::try_from(value & mask).unwrap_or_default(); + Self(value) + } +} + +impl BitXor for Block32 { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} diff --git a/des/src/block/block48.rs b/des/src/block/block48.rs new file mode 100644 index 0000000..5fa3473 --- /dev/null +++ b/des/src/block/block48.rs @@ -0,0 +1,71 @@ +use std::{array, ops::BitXor}; + +use crate::{block::Block6, key::Subkey}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Block48(u64); + +impl Block48 { + const MASK: u64 = 0xFFFF_FFFF_FFFF; + #[inline] + #[must_use] + pub const fn new(value: u64) -> Self { + Self(value & Self::MASK) + } + + #[inline] + #[must_use] + pub const fn as_u64(self) -> u64 { + self.0 + } + + #[must_use] + pub fn as_block6_array(self) -> [Block6; 8] { + array::from_fn(|idx| { + let start_bit = 42 - (u8::try_from(idx).expect("8-bit number") * 6); // S-box 0: bit 42, S-box 7: bit 5 + let six_bits = u8::try_from((self.0 >> start_bit) & 0x3F).expect("6-bit number"); + Block6::new(six_bits) + }) + } +} + +impl From for Block48 { + fn from(value: u64) -> Self { + Self::new(value) + } +} + +impl BitXor for Block48 { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self::Output { + Self(self.0 ^ rhs.0) + } +} + +impl BitXor for Block48 { + type Output = Self; + fn bitxor(self, rhs: Subkey) -> Self::Output { + Self(self.0 ^ rhs.as_int()) + } +} + +impl BitXor<&Subkey> for Block48 { + type Output = Self; + fn bitxor(self, rhs: &Subkey) -> Self::Output { + Self(self.0 ^ rhs.as_int()) + } +} + +impl BitXor for &Block48 { + type Output = Block48; + fn bitxor(self, rhs: Subkey) -> Self::Output { + Block48(self.0 ^ rhs.as_int()) + } +} + +impl BitXor<&Subkey> for &Block48 { + type Output = Block48; + fn bitxor(self, rhs: &Subkey) -> Self::Output { + Block48(self.0 ^ rhs.as_int()) + } +} diff --git a/des/src/block/block6.rs b/des/src/block/block6.rs new file mode 100644 index 0000000..cf0797a --- /dev/null +++ b/des/src/block/block6.rs @@ -0,0 +1,36 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Block6(u8); + +impl Block6 { + const MASK: u8 = 0x3F; + + #[inline] + #[must_use] + pub const fn new(value: u8) -> Self { + Self(value & Self::MASK) + } + + #[inline] + #[must_use] + pub const fn zero() -> Self { + Self(0) + } + + #[inline] + #[must_use] + pub const fn as_u8(self) -> u8 { + self.0 + } + + #[inline] + #[must_use] + pub const fn to_row(self) -> usize { + ((self.0 >> 5) << 1 | (self.0 & 1)) as usize + } + + #[inline] + #[must_use] + pub const fn to_col(self) -> usize { + ((self.0 >> 1) & 0xF) as usize + } +} diff --git a/des/src/block/block64.rs b/des/src/block/block64.rs new file mode 100644 index 0000000..0bb784f --- /dev/null +++ b/des/src/block/block64.rs @@ -0,0 +1,76 @@ +use crate::block::lr::LR; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Block64(u64); + +impl Block64 { + #[inline] + #[must_use] + pub const fn new(value: u64) -> Self { + Self(value) + } + + #[inline] + #[must_use] + pub const fn as_u64(self) -> u64 { + self.0 + } + + #[inline] + #[must_use] + pub const fn from_be_bytes(bytes: [u8; 8]) -> Self { + Self(u64::from_be_bytes(bytes)) + } + + #[inline] + #[must_use] + pub const fn to_le_bytes(self) -> [u8; 8] { + self.0.to_le_bytes() + } + + #[inline] + #[must_use] + pub const fn to_be_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + + #[inline] + #[must_use] + pub fn split_lr(self) -> LR { + self.into() + } +} + +impl From for Block64 { + fn from(value: u64) -> Self { + Self::new(value) + } +} + +impl From for LR { + fn from(block: Block64) -> Self { + let left = (block.0 >> 32) as u32; + let right = (block.0 & 0xFFFF_FFFF) as u32; + Self::new(left, right) + } +} + +impl From<&Block64> for LR { + fn from(block: &Block64) -> Self { + let left = (block.0 >> 32) as u32; + let right = (block.0 & 0xFFFF_FFFF) as u32; + Self::new(left, right) + } +} + +impl From for Vec { + fn from(value: Block64) -> Self { + value.0.to_le_bytes().to_vec() + } +} + +impl From<&Block64> for Vec { + fn from(value: &Block64) -> Self { + value.0.to_le_bytes().to_vec() + } +} diff --git a/des/src/block/lr.rs b/des/src/block/lr.rs new file mode 100644 index 0000000..054f626 --- /dev/null +++ b/des/src/block/lr.rs @@ -0,0 +1,43 @@ +use crate::block::{block32::Block32, block64::Block64}; +use std::mem::swap; + +#[derive(Debug, Clone, Copy)] +pub struct LR { + pub(crate) left: Block32, + pub(crate) right: Block32, +} + +impl LR { + #[must_use] + pub fn new(left: impl Into, right: impl Into) -> Self { + Self { + left: left.into(), + right: right.into(), + } + } + + #[inline] + pub const fn swap(&mut self) { + swap(&mut self.left, &mut self.right); + } + + #[inline] + #[must_use] + pub const fn left(self) -> Block32 { + self.left + } + + #[inline] + #[must_use] + pub const fn right(self) -> Block32 { + self.right + } +} + +impl From for Block64 { + fn from(lr: LR) -> Self { + let left = lr.left.as_u64() << 32; + let right = lr.right.as_u64(); + Self::new(left | right) + } +} diff --git a/des/src/block/mod.rs b/des/src/block/mod.rs new file mode 100644 index 0000000..0a64b52 --- /dev/null +++ b/des/src/block/mod.rs @@ -0,0 +1,7 @@ +mod block32; +mod block48; +mod block6; +mod block64; +mod lr; + +pub use {block6::Block6, block32::Block32, block48::Block48, block64::Block64, lr::LR}; diff --git a/des/src/constants.rs b/des/src/constants.rs index add1c88..ef9d17c 100644 --- a/des/src/constants.rs +++ b/des/src/constants.rs @@ -1,4 +1,21 @@ // DES Constants (from FIPS 46-3 spec) + +/// Key Permutation table (64 to 56 bits). +pub 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, +]; + +/// Compression Permutation table (56 to 48 bits). +pub 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, +]; + +/// Number of Key Bits Shifted per Round +pub const ROUND_ROTATIONS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]; + /// Initial Permutation (IP) table. pub const IP: [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, diff --git a/des/src/des.rs b/des/src/des.rs index 3173b34..e191cf8 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -1,14 +1,216 @@ -use crate::key::Subkeys; -use cipher_core::{CryptoResult, KeyLike}; +use crate::{ + block::{Block32, Block48, Block64, LR}, + constants::{E_BOX, FP, IP, P_BOX, S_BOXES}, + key::{Key, Subkey, Subkeys}, + utils::permutate, +}; +use cipher_core::{BlockCipher, CipherAction, CipherError, CipherResult}; -#[derive(Debug)] pub struct Des { subkeys: Subkeys, } impl Des { - pub fn new(key: &impl KeyLike) -> CryptoResult { - let subkeys = Subkeys::from_key(key)?; + pub fn new(key: impl Into) -> CipherResult { + let subkeys = Subkeys::from_key(&key.into())?; Ok(Self { subkeys }) } } + +impl BlockCipher for Des { + const BLOCK_SIZE: usize = 8; + + fn transform_impl( + &self, + block: &[u8], + action: cipher_core::CipherAction, + ) -> cipher_core::CipherResult> { + let block_arr: [u8; Self::BLOCK_SIZE] = block + .try_into() + .map_err(|_| CipherError::invalid_block_size(Self::BLOCK_SIZE, block.len()))?; + let block64 = Block64::from_be_bytes(block_arr); + let permutated_block = ip(block64); + + let result = match action { + CipherAction::Encrypt => feistel_rounds(permutated_block, self.subkeys.iter()), + CipherAction::Decrypt => feistel_rounds(permutated_block, self.subkeys.iter_rev()), + }; + + let result = fp(result); + + Ok(result.into()) + } +} + +#[inline] +#[must_use] +fn ip(block: Block64) -> Block64 { + permutate(block.as_u64(), 64, 64, &IP).into() +} + +#[must_use] +fn feistel_rounds<'a, I>(block: Block64, subkeys: I) -> Block64 +where + I: Iterator, +{ + let mut lr = LR::from(block); + + for subkey in subkeys { + feistel(&mut lr, subkey); + } + lr.into() +} + +fn feistel(lr: &mut LR, subkey: &Subkey) { + let tmp = lr.right; + lr.right = lr.left ^ f_function(lr.right, subkey); + lr.left = tmp; +} + +#[must_use] +fn f_function(right: Block32, subkey: &Subkey) -> Block32 { + let expanded = expansion_permutation(right); + let xored = expanded ^ subkey; + let sboxed = s_box_substitution(xored); + p_box_permutation(sboxed) +} + +#[inline] +#[must_use] +fn expansion_permutation(right: Block32) -> Block48 { + permutate(right.as_u64(), 32, 48, &E_BOX).into() +} + +#[must_use] +fn s_box_substitution(block: Block48) -> Block32 { + let six_bit_blocks = block.as_block6_array(); + S_BOXES + .iter() + .zip(six_bit_blocks.iter()) + .enumerate() + .fold(0, |acc, (idx, (s_box, block6))| { + let row = block6.to_row(); + let col = block6.to_col(); + + let sbox_value = s_box[row][col]; + let shift_amount = (7 - idx) * 4; + + acc | (u32::from(sbox_value) << shift_amount) + }) + .into() +} + +#[inline] +#[must_use] +fn p_box_permutation(block: Block32) -> Block32 { + permutate(block.as_u64(), 32, 32, &P_BOX).into() +} + +#[inline] +#[must_use] +fn fp(result: Block64) -> Block64 { + permutate(result.as_u64(), 64, 64, &FP).into() +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + const TEST_PLAINTEXT: u64 = 0x0123_4567_89AB_CDEF; + + #[rstest] + #[case(TEST_PLAINTEXT, 0xCC00_CCFF_F0AA_F0AA)] + fn initial_permutation(#[case] input: u64, #[case] expected: u64) { + let result = ip(input.into()).as_u64(); + assert_eq!( + result, expected, + "Initial permutation failed. Expected 0x{expected:016X}, got 0x{result:016X}" + ); + } + + #[rstest] + #[case(0xF0AA_F0AA, 0x7A15_557A_1555)] // Round 1 + #[case(0xEF4A_6544, 0x75EA_5430_AA09)] // Round 2 + #[case(0xCC01_7709, 0xE580_02BA_E853)] // Round 3 + #[case(0xA25C_0BF4, 0x5042_F805_7FA9)] // Round 4 + #[case(0x7722_0045, 0xBAE9_0400_020A)] // Round 5 + #[case(0x8A4F_A637, 0xC542_5FD0_C1AF)] // Round 6 + #[case(0xE967_CD69, 0xF52B_0FE5_AB53)] // Round 7 + #[case(0x064A_BA10, 0x00C2_555F_40A0)] // Round 8 + #[case(0xD569_4B90, 0x6AAB_52A5_7CA1)] // Round 9 + #[case(0x247C_C67A, 0x1083_F960_C3F4)] // Round 10 + #[case(0xB7D5_D7B2, 0x5AFE_ABEA_FDA5)] // Round 11 + #[case(0xC578_3C78, 0x60AB_F01F_83F1)] // Round 12 + #[case(0x75BD_1858, 0x3ABD_FA8F_02F0)] // Round 13 + #[case(0x18C3_155A, 0x0F16_068A_AAF4)] // Round 14 + #[case(0xC28C_960D, 0xE054_594A_C05B)] // Round 15 + #[case(0x4342_3234, 0x206A_041A_41A8)] // Round 16 + fn permutation_expansion(#[case] block: u32, #[case] expected: u64) { + let result = expansion_permutation(block.into()).as_u64(); + assert_eq!( + result, expected, + "Expansion permutaion failed. Expected {expected:016X}, got {result:016X}" + ); + } + + #[rstest] + #[case(0x6117_BA86_6527, 0x5C82_B597)] // Round 1 + #[case(0x0C44_8DEB_63EC, 0xF8D0_3AAE)] // Round 2 + #[case(0xB07C_88F8_27CA, 0x2710_E16F)] // Round 3 + #[case(0x22EF_2EDE_4AB4, 0x21ED_9F3A)] // Round 4 + #[case(0xC605_03EB_51A2, 0x50C8_31EB)] // Round 5 + #[case(0xA6E7_6180_BA80, 0x41F3_4C3D)] // Round 6 + #[case(0x19AF_B813_B3EF, 0x1075_40AD)] // Round 7 + #[case(0xF748_6F9E_7B5B, 0x6C18_7CAE)] // Round 8 + #[case(0x8A70_B948_9B20, 0x110C_5777)] // Round 9 + #[case(0xA170_BEDA_85BB, 0xDA04_5275)] // Round 10 + #[case(0x7BA1_7834_2E23, 0x7305_D101)] // Round 11 + #[case(0x15DA_058B_E418, 0x7B8B_2635)] // Round 12 + #[case(0xAD78_2B75_B8B1, 0x9AD1_8B4F)] // Round 13 + #[case(0x5055_B178_4DCE, 0x6479_9AF1)] // Round 14 + #[case(0x5FC5_D477_FF51, 0xB2E8_8D3C)] // Round 15 + #[case(0xEB57_8F14_565D, 0xA783_2429)] // Round 16 + fn sbox_subsitution(#[case] block: u64, #[case] expected: u32) { + let result = s_box_substitution(block.into()).as_u32(); + assert_eq!( + result, expected, + "S-BOX substituion failed. Expected {expected:08X}, got {result:08X}" + ); + } + + #[rstest] + #[case(0x5C82_B597, 0x234A_A9BB)] // Round 1 + #[case(0xF8D0_3AAE, 0x3CAB_87A3)] // Round 2 + #[case(0x2710_E16F, 0x4D16_6EB0)] // Round 3 + #[case(0x21ED_9F3A, 0xBB23_774C)] // Round 4 + #[case(0x50C8_31EB, 0x2813_ADC3)] // Round 5 + #[case(0x41F3_4C3D, 0x9E45_CD2C)] // Round 6 + #[case(0x1075_40AD, 0x8C05_1C27)] // Round 7 + #[case(0x6C18_7CAE, 0x3C0E_86F9)] // Round 8 + #[case(0x110C_5777, 0x2236_7C6A)] // Round 9 + #[case(0xDA04_5275, 0x62BC_9C22)] // Round 10 + #[case(0x7305_D101, 0xE104_FA02)] // Round 11 + #[case(0x7B8B_2635, 0xC268_CFEA)] // Round 12 + #[case(0x9AD1_8B4F, 0xDDBB_2922)] // Round 13 + #[case(0x6479_9AF1, 0xB731_8E55)] // Round 14 + #[case(0xB2E8_8D3C, 0x5B81_276E)] // Round 15 + #[case(0xA783_2429, 0xC8C0_4F98)] // Round 16 + fn permuation_pbox(#[case] block: u32, #[case] expected: u32) { + let result = p_box_permutation(block.into()).as_u32(); + assert_eq!( + result, expected, + "P-BOX permutation failed. Expected {expected:08X}, got {result:08X}" + ); + } + + #[rstest] + #[case(0x0A4C_D995_4342_3234, 0x85E8_1354_0F0A_B405)] + fn final_permutation(#[case] input: u64, #[case] expected: u64) { + let result = fp(input.into()).as_u64(); + assert_eq!( + result, expected, + "Final permutation failed. Expected 0x{expected:016X}, got 0x{result:016X}" + ); + } +} diff --git a/des/src/key/cd56.rs b/des/src/key/cd56.rs new file mode 100644 index 0000000..0fbc5ee --- /dev/null +++ b/des/src/key/cd56.rs @@ -0,0 +1,34 @@ +use crate::key::{half28::Half28, key56::Key56}; +use zeroize::ZeroizeOnDrop; + +#[derive(ZeroizeOnDrop)] +pub struct CD56 { + pub c: Half28, + pub d: Half28, +} + +impl CD56 { + pub fn new(c: impl Into, d: impl Into) -> Self { + Self { + c: c.into(), + d: d.into(), + } + } + + pub fn rotate_left(&mut self, amount: u8) { + self.c = self.c.rotate_left(amount); + self.d = self.d.rotate_left(amount); + } +} + +impl From for Key56 { + fn from(value: CD56) -> Self { + Self::from_half28(&value.c, &value.d) + } +} + +impl From<&CD56> for Key56 { + fn from(value: &CD56) -> Self { + Self::from_half28(&value.c, &value.d) + } +} diff --git a/des/src/key/des_key.rs b/des/src/key/des_key.rs new file mode 100644 index 0000000..f849fdf --- /dev/null +++ b/des/src/key/des_key.rs @@ -0,0 +1,55 @@ +use std::fmt::Debug; +use zeroize::ZeroizeOnDrop; + +/// 64-bit Key for DES +#[derive(ZeroizeOnDrop)] +pub struct Key([u8; 8]); +impl Key { + #[inline] + #[must_use] + pub const fn from_array(bytes: [u8; 8]) -> Self { + Self(bytes) + } + + #[inline] + #[must_use] + pub const fn as_array(&self) -> &[u8; 8] { + &self.0 + } + + #[inline] + #[must_use] + pub const fn as_u64(&self) -> u64 { + u64::from_be_bytes(self.0) + } +} + +impl From<[u8; 8]> for Key { + fn from(bytes: [u8; 8]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 8] { + fn from(key: Key) -> Self { + key.0 + } +} + +impl AsRef<[u8]> for Key { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From for Key { + fn from(key: u64) -> Self { + Self(key.to_le_bytes()) + } +} + +impl Debug for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Key([REDACTED])") + } +} diff --git a/des/src/key/key56.rs b/des/src/key/key56.rs index 833c789..dfab83d 100644 --- a/des/src/key/key56.rs +++ b/des/src/key/key56.rs @@ -1,4 +1,7 @@ -use crate::{key::half28::Half28, secret_int}; +use crate::{ + key::{cd56::CD56, half28::Half28}, + secret_int, +}; secret_int! { /// 56-bit key after PC-1 (lower 56 bits used). @@ -7,10 +10,10 @@ secret_int! { impl Key56 { #[must_use] - pub fn split(&self) -> (Half28, Half28) { + pub fn split(&self) -> CD56 { let c = ((self.0 >> 28) & 0x0FFF_FFFF) as u32; let d = (self.0 & 0x0FFF_FFFF) as u32; - (c.into(), d.into()) + CD56::new(c, d) } #[must_use] @@ -21,16 +24,14 @@ impl Key56 { } } -impl From<[Half28; 2]> for Key56 { - fn from(keys: [Half28; 2]) -> Self { - let [left, right] = keys; - Self::from_half28(&left, &right) +impl From for CD56 { + fn from(key56: Key56) -> Self { + key56.split() } } -impl From<&[Half28; 2]> for Key56 { - fn from(keys: &[Half28; 2]) -> Self { - let [left, right] = keys; - Self::from_half28(left, right) +impl From<&Key56> for CD56 { + fn from(key56: &Key56) -> Self { + key56.split() } } diff --git a/des/src/key/mod.rs b/des/src/key/mod.rs index 24cfd7b..ba2f96b 100644 --- a/des/src/key/mod.rs +++ b/des/src/key/mod.rs @@ -1,3 +1,5 @@ +mod cd56; +mod des_key; mod half28; mod key56; mod secret_int; @@ -5,4 +7,4 @@ mod subkey; mod subkeys; use crate::secret_int; -pub use subkeys::Subkeys; +pub use {des_key::Key, subkey::Subkey, subkeys::Subkeys}; diff --git a/des/src/key/secret_int.rs b/des/src/key/secret_int.rs index b358ae6..c184285 100644 --- a/des/src/key/secret_int.rs +++ b/des/src/key/secret_int.rs @@ -24,8 +24,6 @@ macro_rules! secret_int { $vis struct $name($int); impl $name { - /// Number of meaningful bits. - pub const BITS: usize = $bits; /// Mask to restrict the underlying integer to valid bits. pub const MASK: $int = $mask; @@ -42,11 +40,6 @@ macro_rules! secret_int { pub const fn as_int(&self) -> $int { self.0 & Self::MASK } - - /// Zero value. - pub const fn zero() -> Self { - Self(0) - } } // Optionally add Clone if requested explicitly (discouraged for secrets) diff --git a/des/src/key/subkey.rs b/des/src/key/subkey.rs index cff1bc2..1f8f49c 100644 --- a/des/src/key/subkey.rs +++ b/des/src/key/subkey.rs @@ -4,3 +4,10 @@ secret_int! { /// A single DES round subkey (48 bits stored in lower bits of u64). pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFF); } + +impl Subkey { + /// Zero value. + pub const fn zero() -> Self { + Self(0) + } +} diff --git a/des/src/key/subkeys.rs b/des/src/key/subkeys.rs index acfe3f1..6ff5d47 100644 --- a/des/src/key/subkeys.rs +++ b/des/src/key/subkeys.rs @@ -1,25 +1,15 @@ use crate::{ - key::{key56::Key56, subkey::Subkey}, + constants::{PC1, PC2, ROUND_ROTATIONS}, + key::{Key, cd56::CD56, key56::Key56, subkey::Subkey}, utils::permutate, }; -use cipher_core::{CryptoError, CryptoResult, KeyLike}; -use std::fmt::Debug; - -/// Key Permutation table (64 to 56 bits). -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, -]; - -/// Compression Permutation table (56 to 48 bits). -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, -]; - -/// Number of Key Bits Shifted per Round -const ROUND_ROTATIONS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]; +use cipher_core::CipherResult; +use std::{ + fmt::Debug, + iter::Rev, + ops::Index, + slice::{Iter, IterMut}, +}; /// Container for all 16 round subkeys; zeroized on drop. pub struct Subkeys([Subkey; 16]); @@ -37,37 +27,22 @@ impl Subkeys { &self.0 } - #[inline] - pub fn set(&mut self, idx: usize, sk: Subkey) { - self.0[idx] = sk; - } - #[inline] #[must_use] pub fn get(&self, idx: usize) -> Option<&Subkey> { self.0.get(idx) } - pub fn from_key(key: &impl KeyLike) -> CryptoResult { - let key_bytes = key.as_bytes(); - let key_len = key_bytes.len(); - - let key_arr = key_bytes - .try_into() - .map_err(|_| CryptoError::invalid_key_size(8, key_len))?; - - let key_be = u64::from_be_bytes(key_arr); - - let cd_56 = pc1(key_be); // 56-bit: C0 + D0 - let (c, d) = cd_56.split(); + /// # Errors + /// # Panics + pub fn from_key(key: &Key) -> CipherResult { + let mut cd56 = pc1(key).split(); // 56-bit: C0 + D0 let subkeys = ROUND_ROTATIONS .iter() .map(|&shift_amount| { - let cn = c.rotate_left(shift_amount); // C_(n-1) -> C_n - let dn = d.rotate_left(shift_amount); // D_(n-1) -> D_n - let combined = [cn, dn].into(); - pc2(&combined) + cd56.rotate_left(shift_amount); + pc2(&cd56) }) .collect::>() .try_into() @@ -76,23 +51,63 @@ impl Subkeys { Ok(Self(subkeys)) } - pub(crate) fn as_u64_array(&self) -> [u64; 16] { - self.0 - .iter() - .enumerate() - .fold([0; 16], |mut out, (idx, sk)| { - out[idx] = sk.as_int(); - out - }) + /// Borrowing forward iterator. + pub fn iter(&self) -> Iter<'_, Subkey> { + self.0.iter() + } + + /// Borrowing reverse iterator. + pub fn iter_rev(&self) -> Rev> { + self.0.iter().rev() + } + + /// Mutable iterator if you need it. + pub fn iter_mut(&mut self) -> IterMut<'_, Subkey> { + self.0.iter_mut() + } + + /// Consume `self` and return a new `Subkeys` with reversed order. + #[must_use] + pub const fn reversed(mut self) -> Self { + self.0.reverse(); + self } } -fn pc1(key: u64) -> Key56 { - permutate(key, 64, 56, &PC1).into() +#[inline] +#[must_use] +fn pc1(key: &Key) -> Key56 { + permutate(key.as_u64(), 64, 56, &PC1).into() } -fn pc2(key: &Key56) -> Subkey { - permutate(key.as_int(), 56, 48, &PC2).into() +#[inline] +#[must_use] +fn pc2(cd: &CD56) -> Subkey { + let key56 = Key56::from(cd); + permutate(key56.as_int(), 56, 48, &PC2).into() +} + +impl<'a> IntoIterator for &'a Subkeys { + type Item = &'a Subkey; + type IntoIter = Iter<'a, Subkey>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Subkeys { + type Item = &'a mut Subkey; + type IntoIter = IterMut<'a, Subkey>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl Index for Subkeys { + type Output = Subkey; + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } } impl Debug for Subkeys { @@ -113,18 +128,14 @@ mod tests { use rstest::rstest; const TEST_KEY: u64 = 0x1334_5779_9BBC_DFF1; - const TEST_PC1_RESULT: u64 = 0x00F0_CCAA_F556_678F; #[rstest] - #[case(TEST_KEY, TEST_PC1_RESULT)] + #[case(TEST_KEY, 0x00F0_CCAA_F556_678F)] fn pc1_permutaion_correct(#[case] key: u64, #[case] expected: u64) { - let result = pc1(key); - + let result = pc1(&key.into()).as_int(); assert_eq!( - result.as_int(), - expected, - "PC1 permutation failed. Expected {expected:08X}, got {:08X}", - result.as_int() + result, expected, + "PC1 permutation failed. Expected {expected:08X}, got {result:08X}", ); } @@ -146,8 +157,12 @@ mod tests { #[case(0x00FE_1995_5EAA_CCF1, 0x5F43_B7F2_E73A)] // K_14 #[case(0x00F8_6655_7AAB_33C7, 0xBF91_8D3D_3F0A)] // K_15 #[case(0x00F0_CCAA_F556_678F, 0xCB3D_8B0E_17F5)] // K_16 - fn pc2_permutaion(#[case] before: u64, #[case] after: u64) { - let result = pc2(&before.into()); - assert_eq!(result.as_int(), after, "PC2 permutation failed"); + fn pc2_permutaion(#[case] before: u64, #[case] expected: u64) { + let key56 = Key56::from(before).split(); + let result = pc2(&key56).as_int(); + assert_eq!( + result, expected, + "PC2 permutation failed. Expected {expected:016X}, got {result:016X}" + ); } } diff --git a/des/src/lib.rs b/des/src/lib.rs index 0200aa4..3dba8ea 100644 --- a/des/src/lib.rs +++ b/des/src/lib.rs @@ -1,3 +1,4 @@ +mod block; pub(crate) mod constants; mod des; mod key; diff --git a/des/src/utils.rs b/des/src/utils.rs index b4304b7..ae51a13 100644 --- a/des/src/utils.rs +++ b/des/src/utils.rs @@ -16,29 +16,13 @@ pub fn permutate( output_bit_amount: u64, position_table: &[u8], ) -> u64 { - debug_assert!(position_table.len() as u64 == output_bit_amount); - debug_assert!(output_bit_amount <= 64); - debug_assert!(input_bit_amount <= 64); - position_table .iter() .enumerate() .fold(0, |acc, (idx, &input_pos_1based)| { - // Convert 1-based DES position to 0-based input position (MSB first) - let input_pos_from_msb_0based = u64::from(input_pos_1based).saturating_sub(1); - let input_bit_pos = input_bit_amount - .saturating_sub(1) - .saturating_sub(input_pos_from_msb_0based); - - // Extract bit from input + let input_bit_pos = input_bit_amount - u64::from(input_pos_1based); let bit_value = (input >> input_bit_pos) & 1; - - // Extract bit from u64 at the correct position - let output_bit_pos = output_bit_amount - .saturating_sub(1) - .saturating_sub(idx as u64); - - let shifted_bit = bit_value << output_bit_pos; - acc | shifted_bit + let output_bit_pos = output_bit_amount - 1 - (idx as u64); + acc | (bit_value << output_bit_pos) }) } diff --git a/des/tests/des.rs b/des/tests/des.rs new file mode 100644 index 0000000..996582b --- /dev/null +++ b/des/tests/des.rs @@ -0,0 +1,139 @@ +use cipher_core::BlockCipher; +use claims::assert_ok; +use des::Des; +use rstest::rstest; + +const TEST_KEY: u64 = 0x1334_5779_9BBC_DFF1; +const TEST_PLAINTEXT: u64 = 0x0123_4567_89AB_CDEF; +const TEST_CIPHERTEXT: u64 = 0x85E8_1354_0F0A_B405; + +#[rstest] +#[case(TEST_PLAINTEXT, TEST_CIPHERTEXT, TEST_KEY)] +#[case(0xC6F9_3D51_0757_A8FE, 0x3FDF_8951_0F38_A29B, 0x0655_A407_0623_10E3)] +#[case(0x4230_ECB4_5CD4_C924, 0xB7D1_E902_48E5_51CA, 0xAF3B_75E7_6DAD_49C8)] +#[case(0xE277_0441_8EE8_AE59, 0x4687_4321_3707_A4C7, 0x2FD7_2C95_90E1_712E)] +#[case(0x0BB5_293D_2EA8_1028, 0xB91E_FD54_0D09_FB25, 0xC495_4CCA_1BF1_8FE9)] +#[case(0xEE0C_8BED_F695_AB5F, 0x60F8_EE47_069F_989A, 0x350E_304F_E0F1_253D)] +#[case(0xF737_BBEB_28AA_31E1, 0xF2C1_27AD_7122_66A6, 0xA7F7_E1E3_CD93_F95D)] +#[case(0x1497_1184_AD78_EE19, 0x10C6_89E2_60BA_4B8E, 0x78D0_0A8E_63B1_4B5D)] +#[case(0x0F67_735D_621A_52C5, 0x1617_C981_8A62_7760, 0xAA48_FE46_0CB0_D436)] +#[case(0x81DD_1F9B_2E46_2061, 0xF2D1_E793_EB75_664A, 0x9901_A819_87CA_4782)] +#[case(0xEDBB_18A4_3E6D_8547, 0xD6A2_2519_CC7A_0D3C, 0x8297_9633_08D3_EBEE)] +#[case(0xF8B9_B259_5140_7D92, 0x16D7_B2DA_E12B_D8E7, 0x097A_23AF_A6E1_D116)] +#[case(0x1544_4A64_DA36_DAFC, 0xFA77_0F17_2532_AA14, 0x4F22_671C_8797_BDD1)] +#[case(0xEB95_CC8B_258D_D5C8, 0xBD6D_1004_24D4_8B48, 0xA886_0128_721F_FBB7)] +#[case(0xF1C2_A5DE_F978_0E45, 0x212D_A009_B107_EB47, 0x2383_25F5_E681_B9E5)] +#[case(0x21CA_7D34_9158_6F61, 0x75DE_1ED4_C454_3E04, 0xB353_ECE0_0C1D_C4AD)] +#[case(0xC314_4A6F_EAA8_3D91, 0xD98E_150A_66EB_BC33, 0xAC4F_E060_2751_2FF4)] +#[case(0xE845_420B_7C46_DBBB, 0x2E4D_9248_2585_44FF, 0xE997_37D4_27AE_B5F9)] +#[case(0x0284_5E3F_2EDC_4809, 0x0D22_6DC1_532F_3E4A, 0xD339_51A2_9FD6_C3DF)] +#[case(0x0471_1CC5_0218_A7B8, 0x0F34_EA09_A837_33C7, 0xBFC2_3D34_930A_D36D)] +#[case(0xE696_EBC5_7577_B9C4, 0x5F54_48A0_670D_2B41, 0x2200_AA0C_3F42_226C)] +#[case(0xCF61_8B74_BE29_1BD8, 0x254C_D92C_A2FB_043B, 0x36E6_82D4_9799_C6B3)] +#[case(0x7E18_6E5A_8E95_7527, 0x4462_82B2_1A70_0BDB, 0x4510_A566_7793_F488)] +#[case(0x2352_0EA6_C837_E224, 0x7C6C_616F_0726_F45E, 0xCCE7_EC8E_DFD6_2C95)] +#[case(0x802F_22E9_FAE5_71FE, 0x9532_CC17_3FD6_CAEF, 0x3070_C587_0D54_2905)] +#[case(0xE1AE_534B_DF81_0003, 0xCEFE_8EA8_40A0_A830, 0x0F58_5495_20E3_BDDD)] +#[case(0xD074_AD28_E24B_10B4, 0x4CA2_31ED_48FA_F5ED, 0x5D04_DEF0_4DB7_C05A)] +#[case(0x7D6D_7F72_8247_D60A, 0xB350_CE16_4A60_E35D, 0x82A2_5738_1871_519F)] +#[case(0x922D_5265_A0C1_54FC, 0xB615_C26C_8624_27A8, 0x07B3_9AFE_02EC_FBC9)] +#[case(0x7A38_2A2D_1ED0_1EC7, 0x96C4_819F_5BF1_099C, 0x6AF2_B49D_2EB0_055D)] +#[case(0x0E9B_3302_66FD_964C, 0x4F42_A745_0D30_20DB, 0x8AFA_83AA_3158_858D)] +#[case(0x6872_A230_96B8_0143, 0x2DC8_6E8E_00F1_7D2E, 0x660E_D819_E232_5E0F)] +#[case(0x0FF9_554F_AEAA_DDE6, 0x7AA3_B1D9_22B8_0BF1, 0x9B32_8887_35DE_BB5D)] +#[case(0x00FB_729A_2201_5603, 0x2506_2232_CF4B_E95C, 0x7104_806D_7257_2CB7)] +#[case(0x2E1C_C29A_E40B_4CCD, 0xFC32_0B3E_6058_59AE, 0x4541_D83F_E726_7CD2)] +#[case(0x4336_2A6C_CE6E_CA97, 0xB2C8_FDFE_0A47_2A81, 0xD730_064E_E820_3C33)] +#[case(0x03FB_C27D_74F3_4AC0, 0x4EEC_87D2_9024_5136, 0x361E_1CC7_4E4E_434F)] +#[case(0x5830_0AAC_75D9_FD76, 0x9C41_87B5_BAF5_FA65, 0x4929_7C9B_86E9_ECFC)] +#[case(0x6224_F54C_2C0E_34C4, 0x7002_F5D4_E72B_C942, 0x91B9_3300_0209_EEB0)] +#[case(0x12FE_DF40_FA59_9757, 0x2D24_8A52_638A_CAA4, 0x11D4_7CC2_683B_41A6)] +#[case(0x463C_FBD3_EBED_A824, 0x9CE8_D4F9_521F_E37D, 0xFF05_82BC_587C_747E)] +#[case(0x66A8_196A_3212_B1CB, 0x24B2_CDE9_6BD0_43D1, 0xFA30_B9CE_04F6_53F2)] +#[case(0xC235_5E16_B36F_A90B, 0x56B6_6B72_5995_4B0C, 0x00CE_4F24_5AB0_F566)] +#[case(0x2497_9DE0_EAB2_724C, 0xF868_B877_0B46_2BD1, 0x62A5_FCF5_4F91_2784)] +#[case(0x6356_B043_0AF2_858B, 0x6743_750C_91C7_2828, 0x113B_96B8_2748_CCBF)] +#[case(0x9AE0_16CF_9F33_B1B9, 0x3CEB_D74D_CACC_1721, 0x7B8E_6CAF_FCF4_4E3F)] +#[case(0x0284_35F7_B1BC_4866, 0x5780_E4F5_90C3_8ABF, 0x3D67_EA7B_9BB1_0A48)] +#[case(0xC654_D706_82CF_BC74, 0x2BE2_1130_FD89_6F81, 0x7891_2150_1135_CD28)] +#[case(0x1E59_779F_E127_197B, 0x3960_BE8A_4475_41EC, 0x49E0_8524_68BF_FF70)] +#[case(0x1133_3FE5_F5B6_E8A1, 0x341E_2C24_9E3C_33EC, 0x3A0E_8C24_36FD_CB7E)] +#[case(0x8ABC_A73D_578D_3D58, 0xE0F3_AEFA_24AC_72D8, 0x3D09_4F8F_B36B_2049)] +#[case(0x13D0_E14D_1B09_D4D0, 0xFD12_4B52_065E_9B1C, 0x0DAF_83A5_B78B_D17A)] +#[case(0xB95A_AA93_1E18_D4F2, 0x2E7F_D3E0_1537_4051, 0x21AE_1AF0_DF76_10E7)] +#[case(0x086B_DA84_4066_F31E, 0xAD08_AEBC_32B1_D7CF, 0xA56A_E683_333C_7C7C)] +#[case(0x3728_381E_85C2_E4CD, 0x0C63_24A6_6E9F_5BDB, 0xCEA9_9BFE_6644_87D2)] +#[case(0x531B_541A_095A_A042, 0x7F3B_CA73_FF24_3276, 0x63FF_C197_52E3_591A)] +#[case(0xF3B0_C406_3FD6_CEF2, 0x8EE3_2764_B52A_9608, 0xAF1E_2FC7_7631_5A9C)] +#[case(0xB804_7091_8C20_F2F9, 0x610D_74A9_5D2E_5D04, 0xF03D_BBB8_B4B5_259A)] +#[case(0x745C_E01E_C299_5117, 0x7224_AD1D_3CB6_CFAD, 0xC06D_2C9B_8AC1_8FC5)] +#[case(0x36A2_D3C1_10B9_8ECD, 0xB4FC_F673_35A2_A6AD, 0x88C8_8DEF_D4F6_7DE4)] +#[case(0x1BAB_8249_68A6_F8D4, 0x5C98_AA0A_09E2_CAFB, 0xD8EC_0A6D_76B3_874C)] +#[case(0x9F2E_8659_16C3_2FE4, 0x8C0F_B383_CCBC_48BF, 0x5821_47BE_1321_F990)] +#[case(0x6B97_A57F_E2D1_6610, 0xA3A9_9BB3_1874_E7A6, 0x85ED_1A64_3CDC_3DBD)] +#[case(0x1F58_1813_046C_9976, 0xBAF3_B9F6_BED0_557B, 0x7B98_AAC7_F7CD_D742)] +#[case(0xB9E3_CDF0_B644_5B66, 0xC19E_9B37_15FF_9DC9, 0x9B97_837A_CEDE_EBC8)] +#[case(0xD2F7_4580_0662_407E, 0xA61C_888C_93C5_D990, 0x8FFE_C142_1E0E_A2AE)] +#[case(0xC257_8AE8_B7D1_4A0C, 0x3E30_CAE2_4D01_C06E, 0x90D5_78F0_87BE_D051)] +#[case(0x0E4D_192E_5C18_55BC, 0xCF7E_E266_0BD0_241F, 0xC946_5D9E_8051_3EA8)] +#[case(0xB8C8_823F_8919_5087, 0x42E5_989B_91E4_5B1F, 0x3EC9_0C5F_9542_B379)] +#[case(0xA100_840A_C4F9_FF14, 0x7847_1AE8_EA4D_8582, 0x3E4D_E0AE_BADC_DF35)] +#[case(0x4F8D_2946_5ED5_6262, 0x3185_4E24_A13A_8C53, 0xB256_D848_5014_11DE)] +#[case(0x5767_670D_87F1_FBBA, 0xF892_A9F3_1F76_6F45, 0xDDC9_0B98_F555_6EC9)] +#[case(0xF3BF_F8CB_AADF_D721, 0xB3F4_3D4C_B949_BD28, 0xDED3_DF1E_28B6_AC81)] +#[case(0xC63C_CDE5_88BB_17DB, 0x64A4_6861_E0D8_9377, 0x4317_56A0_9A71_8206)] +#[case(0xDF15_2CCF_DDC8_ACE1, 0x47F3_FE5A_9CA3_6720, 0x17AA_23E0_2E70_76DA)] +#[case(0x09CE_FF80_D62B_A1F3, 0xD706_DEF3_6B3D_9AA8, 0x52D8_119A_736E_D25A)] +#[case(0x6F5C_650B_8C57_370B, 0xEA60_1EBE_B37B_E6B7, 0xE6FD_5BEE_E9F7_89FB)] +#[case(0x4409_4AB7_9244_7C0B, 0x6489_004E_EDD7_65EE, 0x27DB_193D_4410_4800)] +#[case(0x61EE_5199_5804_30FF, 0xE3B1_F245_EEF5_7A8C, 0x8943_B7F5_0EBC_D4AE)] +#[case(0x6F39_1336_84A1_CB12, 0x7D45_A03A_D0C0_2B7C, 0x0FD7_AF51_4C4D_630E)] +#[case(0x7E84_F4DD_2A64_D8A0, 0x3271_8C71_68E5_D4B8, 0x8C5B_0274_F1E1_49F5)] +#[case(0xB14B_F9D2_FD61_626C, 0xCA2F_A9C5_E35D_BD5E, 0xE9C2_47B6_7B90_CB78)] +#[case(0x7315_721B_8072_C12E, 0xCBA1_3DE3_91A3_8CBC, 0xCABD_F1D8_F8C9_7F36)] +#[case(0x9D03_5D2F_FF2E_03F8, 0xA945_5400_A9F9_EC23, 0xF9CD_D65B_5A90_3491)] +#[case(0x7AC5_1308_FBB8_8ADF, 0xB1BA_F5FB_B4E0_968B, 0x87B3_45FF_336E_2C36)] +#[case(0xFA3A_4A1A_1732_C319, 0xC157_E3B9_6D1E_98B8, 0xB691_0EEF_5239_2E0B)] +#[case(0x3B5F_3343_9D31_3650, 0xA207_535D_9E2A_34E9, 0x0932_5791_B0EA_B667)] +#[case(0x72CE_CFD2_29AF_B46D, 0x7DCC_33A1_8CFC_BDC4, 0x2EB8_4231_BADA_C0CD)] +#[case(0xA0C6_7EA3_7D59_BA9E, 0x67F2_D204_2570_7DF3, 0x48B6_42F7_8149_BEAE)] +#[case(0xFB63_1637_53AF_FE64, 0x2345_9280_7EE3_4BB5, 0x2910_9B12_6637_5CF8)] +#[case(0xCFC5_0524_89ED_B7F8, 0xE4E3_E7E9_4B51_86D3, 0xB5CF_CD1E_62A3_C916)] +#[case(0x5C82_EB3D_13D6_DA31, 0x7960_9644_E8F8_D9DC, 0x7BF8_500F_95AF_A68B)] +#[case(0xBB0C_417D_CC7A_0EF3, 0x7B15_FEC1_253A_C2D9, 0xB463_988E_A3B7_F9E4)] +#[case(0xC3B8_37D6_5170_011B, 0x1B23_EF3B_4853_D5C1, 0x7652_8A30_9374_056C)] +#[case(0xE5E0_5D4F_262B_E8B1, 0x4683_AF44_7BA4_1944, 0x0A85_FD6F_ED35_3C99)] +#[case(0x547A_21D5_61C7_A98F, 0xFA1B_8BE3_9447_7DC5, 0x6B20_A7AE_40D8_C631)] +#[case(0x735F_1D3A_17FF_3F12, 0x0C14_C532_CC28_846D, 0xE1B9_6382_D363_4E1B)] +#[case(0x87D2_05D5_16DA_CEA7, 0x5CE9_0A9E_6938_9B59, 0x6683_8ACA_6C09_2897)] +#[case(0x342E_98E4_E3DF_DE9F, 0x1DC2_022E_DE59_79F0, 0x33E1_BD93_DFB6_F058)] +#[case(0x1524_18ED_94BF_6739, 0x0BF0_7F9A_A463_30B8, 0x4781_2ED0_129E_BB99)] +#[case(0xA9CD_C911_F01F_E7C8, 0x8777_9C21_AD54_E116, 0x3E9D_7D47_8FFF_F687)] +fn encrypt_decrypt_roundtrip( + #[case] plaintext: u64, + #[case] expected_ciphertext: u64, + #[case] key: u64, +) { + let des = assert_ok!(Des::new(key), "Valid DES key"); + + let ciphertext = assert_ok!(des.encrypt(&plaintext.to_le_bytes())); + let dectrypted = assert_ok!(des.decrypt(&ciphertext)); + let re_ciphertext = assert_ok!(des.encrypt(&dectrypted)); + + let ciphertext_u64 = u64::from_be_bytes(ciphertext.try_into().expect("8 bytes")); + let decrypted_u64 = u64::from_be_bytes(dectrypted.try_into().expect("8 bytes")); + let re_ciphertext_u64 = u64::from_be_bytes(re_ciphertext.try_into().expect("8 bytes")); + + assert_eq!( + ciphertext_u64, expected_ciphertext, + "Encyption failed. Expected 0x{expected_ciphertext:016X}, got 0x{decrypted_u64:016X}" + ); + assert_eq!( + decrypted_u64, plaintext, + "Decyption failed. Expected 0x{plaintext:016X}, got 0x{decrypted_u64:016X}" + ); + assert_eq!( + re_ciphertext_u64, expected_ciphertext, + "Re-encyption failed. Expected 0x{expected_ciphertext:016X}, got 0x{re_ciphertext_u64:016X}" + ); +}