diff --git a/cipher-core/src/error.rs b/cipher-core/src/error.rs index e3d9ef9..a0043b2 100644 --- a/cipher-core/src/error.rs +++ b/cipher-core/src/error.rs @@ -3,97 +3,26 @@ use thiserror::Error; #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum CipherError { /// Invalid key size for the cipher - #[error("invalid key size: expected {expected} bytes, got {actual}")] + #[error("Invalid key size: expected {expected} bytes, got {actual}")] InvalidKeySize { expected: usize, actual: usize }, /// Input data doesn't match the cipher's block size - #[error("invalid block size: expected {expected} bytes, got {actual}")] + #[error("Invalid block size: expected {expected} bytes, got {actual}")] InvalidBlockSize { expected: usize, actual: usize }, - - /// Decryption detected invalid padding - #[error("invalid padding detected during decryption")] - InvalidPadding, - - /// Input length not valid for unpadded operation - #[error("invalid plaintext length: {actual} bytes (must be multiple of block size)")] - InvalidPlaintextLength { actual: usize }, - - /// General size validation failure - #[error("size mismatch: expected {expected} bytes, got {actual}")] - InvalidSize { expected: usize, actual: usize }, } impl CipherError { - /// Creates a key size error #[inline] #[must_use] pub const fn invalid_key_size(expected: usize, actual: usize) -> Self { Self::InvalidKeySize { expected, actual } } - /// Creates a block size error #[inline] #[must_use] pub const fn invalid_block_size(expected: usize, actual: usize) -> Self { Self::InvalidBlockSize { expected, actual } } - - /// Creates an invalid padding error - #[inline] - #[must_use] - pub const fn invalid_padding() -> Self { - Self::InvalidPadding - } - - /// Creates a plaintext length error - #[inline] - #[must_use] - pub const fn invalid_plaintext_length(actual: usize) -> Self { - Self::InvalidPlaintextLength { actual } - } - - /// Returns true if this is a key size error - #[inline] - #[must_use] - pub const fn is_key_error(&self) -> bool { - matches!(self, Self::InvalidKeySize { .. }) - } - - /// Returns true if this is a block size error - #[inline] - #[must_use] - pub const fn is_block_error(&self) -> bool { - matches!(self, Self::InvalidBlockSize { .. }) - } - - /// Returns true if this is a size-related error - #[must_use] - pub const fn is_size_error(&self) -> bool { - self.is_key_error() || self.is_block_error() || matches!(self, Self::InvalidSize { .. }) - } - - /// Returns the expected size for size-related errors - #[must_use] - pub const fn expected_size(&self) -> Option { - match self { - Self::InvalidKeySize { expected, .. } - | Self::InvalidBlockSize { expected, .. } - | Self::InvalidSize { expected, .. } => Some(*expected), - _ => None, - } - } - - /// Returns the actual size for size-related errors - #[must_use] - pub const fn actual_size(&self) -> Option { - match self { - Self::InvalidKeySize { actual, .. } - | Self::InvalidBlockSize { actual, .. } - | Self::InvalidSize { actual, .. } - | Self::InvalidPlaintextLength { actual } => Some(*actual), - Self::InvalidPadding => None, - } - } } /// Type alias for clean Result types diff --git a/cipher-core/src/traits.rs b/cipher-core/src/traits.rs index 53609ec..a257466 100644 --- a/cipher-core/src/traits.rs +++ b/cipher-core/src/traits.rs @@ -1,10 +1,28 @@ use crate::{CipherAction, CipherError, CipherResult}; +/// Generic block cipher trait. +/// +/// Implements the standard encrypt/decrypt interface for block ciphers. +/// Implementers define `transform_impl` to handle the core algorithm, +/// while `transform` provides validation and convenience wrappers. pub trait BlockCipher: Sized { const BLOCK_SIZE: usize; + /// Core cipher transformation (must be implemented by concrete types). + /// + /// # Errors + /// + /// Returns `CipherError` if the transformation fails. fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult>; + /// Transforms a block with validation. + /// + /// Validates that the block size matches `BLOCK_SIZE` before delegating + /// to `transform_impl`. + /// + /// # Errors + /// + /// Returns `CipherError::InvalidBlockSize` if `block.len() != BLOCK_SIZE`. fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult> { if block.len() != Self::BLOCK_SIZE { return Err(CipherError::invalid_block_size( @@ -15,9 +33,20 @@ pub trait BlockCipher: Sized { self.transform_impl(block, action) } + /// Encrypts a single block. + /// + /// # Errors + /// + /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. fn encrypt(&self, plaintext: &[u8]) -> CipherResult> { self.transform(plaintext, CipherAction::Encrypt) } + + /// Decrypts a single block. + /// + /// # Errors + /// + /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. fn decrypt(&self, ciphertext: &[u8]) -> CipherResult> { self.transform(ciphertext, CipherAction::Decrypt) } diff --git a/des/src/block/block48.rs b/des/src/block/block48.rs index 5fa3473..47b909b 100644 --- a/des/src/block/block48.rs +++ b/des/src/block/block48.rs @@ -45,27 +45,27 @@ impl BitXor for Block48 { impl BitXor for Block48 { type Output = Self; fn bitxor(self, rhs: Subkey) -> Self::Output { - Self(self.0 ^ rhs.as_int()) + Self(self.0 ^ rhs.as_u64()) } } impl BitXor<&Subkey> for Block48 { type Output = Self; fn bitxor(self, rhs: &Subkey) -> Self::Output { - Self(self.0 ^ rhs.as_int()) + Self(self.0 ^ rhs.as_u64()) } } impl BitXor for &Block48 { type Output = Block48; fn bitxor(self, rhs: Subkey) -> Self::Output { - Block48(self.0 ^ rhs.as_int()) + Block48(self.0 ^ rhs.as_u64()) } } impl BitXor<&Subkey> for &Block48 { type Output = Block48; fn bitxor(self, rhs: &Subkey) -> Self::Output { - Block48(self.0 ^ rhs.as_int()) + Block48(self.0 ^ rhs.as_u64()) } } diff --git a/des/src/des.rs b/des/src/des.rs index fb7745b..0a1fa5e 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -4,16 +4,33 @@ use crate::{ key::{Key, Subkey, Subkeys}, utils::permutate, }; -use cipher_core::{BlockCipher, CipherAction, CipherError, CipherResult}; +use cipher_core::{BlockCipher, CipherAction, CipherError}; pub struct Des { subkeys: Subkeys, } impl Des { - pub fn new(key: impl Into) -> CipherResult { - let subkeys = Subkeys::from_key(&key.into())?; - Ok(Self { subkeys }) + /// Creates a new DES cipher with the given key. + /// + /// # Arguments + /// + /// - `key` - An 8-byte key (64 bits). Any type implementing `Into` is accepted + /// (e.g., `&[u8; 8]`, `u64`). + /// + /// # Errors + /// + /// `CipherError::InvalidKeySize` if the key is not exactly 8 bytes. + /// + /// # Example + /// + /// ``` + /// use des::Des; + /// let des = Des::new(0x1334_5779_9BBC_DFF1u64).expect("Valid key"); + /// ``` + pub fn new(key: impl Into) -> Self { + let subkeys = Subkeys::from_key(&key.into()); + Self { subkeys } } } diff --git a/des/src/key/cd56.rs b/des/src/key/cd56.rs index 0fbc5ee..2f59851 100644 --- a/des/src/key/cd56.rs +++ b/des/src/key/cd56.rs @@ -8,11 +8,10 @@ pub struct CD56 { } impl CD56 { - pub fn new(c: impl Into, d: impl Into) -> Self { - Self { - c: c.into(), - d: d.into(), - } + #[inline] + #[must_use] + pub const fn new(c: Half28, d: Half28) -> Self { + Self { c, d } } pub fn rotate_left(&mut self, amount: u8) { diff --git a/des/src/key/half28.rs b/des/src/key/half28.rs index c64900c..fa3ee76 100644 --- a/des/src/key/half28.rs +++ b/des/src/key/half28.rs @@ -1,6 +1,6 @@ -use crate::key::secret_int; +use crate::key::secret_key; -secret_int! { +secret_key! { /// 28-bit half (C or D), stored in lower 28 bits of u32. pub struct Half28(u32, 28, 0x0FFF_FFFF); } @@ -11,15 +11,14 @@ impl Half28 { let value = self.0; let main_shifted = (value << amount) & Self::MASK; let wrapped_bits = (value >> (28 - amount)) & ((1 << amount) - 1); - Self::from_int(main_shifted | wrapped_bits) + Self::from_u32(main_shifted | wrapped_bits) } } #[cfg(test)] mod tests { - use rstest::rstest; - use super::*; + use rstest::rstest; #[rstest] #[case(0x0F0C_CAAF, 0x0E19_955F, 1)] // C_1 @@ -55,7 +54,7 @@ mod tests { #[case(0x0EAA_CCF1, 0x0AAB_33C7, 2)] // D_15 #[case(0x0AAB_33C7, 0x0556_678F, 1)] // D_16 fn half28_rotation(#[case] key: u32, #[case] expected: u32, #[case] amount: u8) { - let result = Half28::from_int(key).rotate_left(amount).as_int(); + let result = Half28::from_u32(key).rotate_left(amount).as_u32(); assert_eq!( result, expected, diff --git a/des/src/key/key56.rs b/des/src/key/key56.rs index dfab83d..356440a 100644 --- a/des/src/key/key56.rs +++ b/des/src/key/key56.rs @@ -1,26 +1,26 @@ use crate::{ key::{cd56::CD56, half28::Half28}, - secret_int, + secret_key, }; -secret_int! { +secret_key! { /// 56-bit key after PC-1 (lower 56 bits used). pub struct Key56(u64, 56, 0x00FF_FFFF_FFFF_FFFF); } impl Key56 { #[must_use] - pub fn split(&self) -> CD56 { + pub const fn split(&self) -> CD56 { let c = ((self.0 >> 28) & 0x0FFF_FFFF) as u32; let d = (self.0 & 0x0FFF_FFFF) as u32; - CD56::new(c, d) + CD56::new(Half28::from_u32(c), Half28::from_u32(d)) } #[must_use] - pub fn from_half28(left: &Half28, right: &Half28) -> Self { - let left = u64::from(left.as_int()); - let right = u64::from(right.as_int()); - Self::from_int((left << 28) | right) + pub const fn from_half28(left: &Half28, right: &Half28) -> Self { + let left = left.as_u64(); + let right = right.as_u64(); + Self::from_u64((left << 28) | right) } } diff --git a/des/src/key/mod.rs b/des/src/key/mod.rs index ba2f96b..6db9f79 100644 --- a/des/src/key/mod.rs +++ b/des/src/key/mod.rs @@ -2,9 +2,9 @@ mod cd56; mod des_key; mod half28; mod key56; -mod secret_int; +mod secret_key; mod subkey; mod subkeys; -use crate::secret_int; +use crate::secret_key; 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 deleted file mode 100644 index c184285..0000000 --- a/des/src/key/secret_int.rs +++ /dev/null @@ -1,77 +0,0 @@ -/// Macro to generate a masked, zeroizable integer wrapper type. -/// -/// Usage: -/// ``` -/// secret_int! { -/// /// docs... -/// pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFFu64); -/// } -/// ``` -/// -/// Optional `clone` token enables an explicit Clone impl: -/// ``` -/// secret_int! { pub struct Foo(u32, 28, 0x0FFF_FFFFu32, clone); } -/// ``` -#[macro_export] -macro_rules! secret_int { - ( - $(#[$meta:meta])* - $vis:vis struct $name:ident ( $int:ty, $bits:expr, $mask:expr $(, $opt:ident )? ); -) => { - $(#[$meta])* - #[derive(::zeroize::ZeroizeOnDrop, Default, Eq)] - #[zeroize(drop)] - $vis struct $name($int); - - impl $name { - /// Mask to restrict the underlying integer to valid bits. - pub const MASK: $int = $mask; - - /// Create from the given integer. - #[inline] - #[must_use] - pub const fn from_int(key: $int) -> Self { - Self(key & Self::MASK) - } - - /// Return as masked integer value; - #[inline] - #[must_use] - pub const fn as_int(&self) -> $int { - self.0 & Self::MASK - } - } - - // Optionally add Clone if requested explicitly (discouraged for secrets) - secret_int!(@maybe_add_clone $( $opt )? ; $name, $int); - - impl ::std::fmt::Debug for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - f.write_str(concat!(stringify!($name), "[REDACTED]")) - } - } - - impl From<$int> for $name { - fn from(v: $int) -> Self { - Self::from_int(v) - } - } - - impl PartialEq for $name { - fn eq(&self, other: &Self) -> bool { - self.as_int() == other.as_int() - } - } - }; - - // helper arm: create Clone impl only when "clone" token present - (@maybe_add_clone clone ; $name:ident, $int:ty) => { - impl Clone for $name { - fn clone(&self) -> Self { - // explicit clone - intentionally duplicating secret - Self::from_int(self.as_int()) - } - } - }; - (@maybe_add_clone ; $name:ident, $int:ty) => {}; -} diff --git a/des/src/key/secret_key.rs b/des/src/key/secret_key.rs new file mode 100644 index 0000000..319a545 --- /dev/null +++ b/des/src/key/secret_key.rs @@ -0,0 +1,126 @@ +/// Macro to generate a masked, zeroizable integer wrapper type. +/// +/// Usage: +/// ``` +/// secret_key! { +/// /// docs... +/// pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFFu64); +/// } +/// ``` +#[macro_export] +macro_rules! secret_key { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident ( $int:tt, $bits:expr, $mask:expr ); +) => { + $(#[$meta])* + #[derive(::zeroize::ZeroizeOnDrop, Default)] + #[zeroize(drop)] + $vis struct $name($int); + + impl $name { + /// Mask to restrict the underlying integer to valid bits. + pub const MASK: $int = $mask; + + secret_key!(@conversions_as $int); + secret_key!(@conversions_from $int $int); + } + + impl ::std::fmt::Debug for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + f.write_str(concat!(stringify!($name), "[REDACTED]")) + } + } + + impl From<$int> for $name { + fn from(v: $int) -> Self { + Self(v & Self::MASK) + } + } + }; + // Helper: generate conversions_as based on type + (@conversions_as u8) => { + /// Return value as u8 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn as_u8(&self) -> u8 { + self.0 as u8 + } + + secret_key!(@conversions_as u16); + }; + (@conversions_as u16) => { + /// Return value as u16 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn as_u16(&self) -> u16 { + self.0 as u16 + } + + secret_key!(@conversions_as u32); + }; + (@conversions_as u32) => { + /// Return value as u32 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn as_u32(&self) -> u32 { + self.0 as u32 + } + + secret_key!(@conversions_as u64); + }; + (@conversions_as u64) => { + /// Return value as u64 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn as_u64(&self) -> u64 { + self.0 as u64 + } + }; + (@conversions_from u8 $int:tt) => { + /// Create value from u8 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn from_u8(key: u8) -> Self { + Self(key as $int & Self::MASK) + } + }; + (@conversions_from u16 $int:tt) => { + /// Create value from u16 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn from_u16(key: u16) -> Self { + Self(key as $int & Self::MASK) + } + + secret_key!(@conversions_from u8 $int); + }; + (@conversions_from u32 $int:tt) => { + /// Create value from u32 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn from_u32(key: u32) -> Self { + Self(key as $int & Self::MASK) + } + + secret_key!(@conversions_from u16 $int); + }; + (@conversions_from u64 $int:tt) => { + /// Create value from u64 + #[allow(dead_code)] + #[inline] + #[must_use] + pub const fn from_u64(key: u64) -> Self { + Self(key & Self::MASK) + } + + secret_key!(@conversions_from u32 $int); + } +} diff --git a/des/src/key/subkey.rs b/des/src/key/subkey.rs index 1f8f49c..e6004a0 100644 --- a/des/src/key/subkey.rs +++ b/des/src/key/subkey.rs @@ -1,6 +1,6 @@ -use crate::key::secret_int; +use crate::key::secret_key; -secret_int! { +secret_key! { /// A single DES round subkey (48 bits stored in lower bits of u64). pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFF); } diff --git a/des/src/key/subkeys.rs b/des/src/key/subkeys.rs index 6ff5d47..cc8edee 100644 --- a/des/src/key/subkeys.rs +++ b/des/src/key/subkeys.rs @@ -3,8 +3,8 @@ use crate::{ key::{Key, cd56::CD56, key56::Key56, subkey::Subkey}, utils::permutate, }; -use cipher_core::CipherResult; use std::{ + array, fmt::Debug, iter::Rev, ops::Index, @@ -12,79 +12,52 @@ use std::{ }; /// Container for all 16 round subkeys; zeroized on drop. +#[derive(Default)] pub struct Subkeys([Subkey; 16]); impl Subkeys { - #[inline] + /// Generates 16 round subkeys from the given key. #[must_use] - pub const fn new_empty() -> Self { - Self([const { Subkey::zero() }; 16]) - } - - #[inline] - #[must_use] - pub const fn as_array(&self) -> &[Subkey; 16] { - &self.0 - } - - #[inline] - #[must_use] - pub fn get(&self, idx: usize) -> Option<&Subkey> { - self.0.get(idx) - } - - /// # Errors - /// # Panics - pub fn from_key(key: &Key) -> CipherResult { + pub fn from_key(key: &Key) -> Self { let mut cd56 = pc1(key).split(); // 56-bit: C0 + D0 - let subkeys = ROUND_ROTATIONS - .iter() - .map(|&shift_amount| { - cd56.rotate_left(shift_amount); - pc2(&cd56) - }) - .collect::>() - .try_into() - .expect("Exactly 16 subkeys expected"); + let subkeys = array::from_fn(|idx| { + cd56.rotate_left(ROUND_ROTATIONS[idx]); + pc2(&cd56) + }); - Ok(Self(subkeys)) + Self(subkeys) } - /// Borrowing forward iterator. + /// Returns an iterator over the subkeys. pub fn iter(&self) -> Iter<'_, Subkey> { self.0.iter() } - /// Borrowing reverse iterator. + /// Returns a reverse iterator over the subkeys. pub fn iter_rev(&self) -> Rev> { self.0.iter().rev() } - /// Mutable iterator if you need it. + /// Returns a mutable iterator over the subkeys. 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 - } } +/// Initial permutation (PC-1): 64-bit -> 56-bit. #[inline] #[must_use] fn pc1(key: &Key) -> Key56 { permutate(key.as_u64(), 64, 56, &PC1).into() } +/// Compression permutation (PC-2): 56-bit -> 48-bit. #[inline] #[must_use] fn pc2(cd: &CD56) -> Subkey { let key56 = Key56::from(cd); - permutate(key56.as_int(), 56, 48, &PC2).into() + permutate(key56.as_u64(), 56, 48, &PC2).into() } impl<'a> IntoIterator for &'a Subkeys { @@ -116,12 +89,6 @@ impl Debug for Subkeys { } } -impl Default for Subkeys { - fn default() -> Self { - Self::new_empty() - } -} - #[cfg(test)] mod tests { use super::*; @@ -132,7 +99,7 @@ mod tests { #[rstest] #[case(TEST_KEY, 0x00F0_CCAA_F556_678F)] fn pc1_permutaion_correct(#[case] key: u64, #[case] expected: u64) { - let result = pc1(&key.into()).as_int(); + let result = pc1(&key.into()).as_u64(); assert_eq!( result, expected, "PC1 permutation failed. Expected {expected:08X}, got {result:08X}", @@ -159,7 +126,7 @@ mod tests { #[case(0x00F0_CCAA_F556_678F, 0xCB3D_8B0E_17F5)] // K_16 fn pc2_permutaion(#[case] before: u64, #[case] expected: u64) { let key56 = Key56::from(before).split(); - let result = pc2(&key56).as_int(); + let result = pc2(&key56).as_u64(); assert_eq!( result, expected, "PC2 permutation failed. Expected {expected:016X}, got {result:016X}" diff --git a/des/tests/des.rs b/des/tests/des.rs index 7f2789b..dfc8778 100644 --- a/des/tests/des.rs +++ b/des/tests/des.rs @@ -114,7 +114,7 @@ fn encrypt_decrypt_roundtrip( #[case] expected_ciphertext: u64, #[case] key: u64, ) { - let des = assert_ok!(Des::new(key), "Valid DES key"); + let des = Des::new(key); let ciphertext = assert_ok!(des.encrypt(&plaintext.to_be_bytes())); let dectrypted = assert_ok!(des.decrypt(&ciphertext));