From 76a808e1b371f13ffc1946d7dd0a78db085d7c49 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Fri, 17 Oct 2025 21:35:47 +0300 Subject: [PATCH] feat(core): add CipherOutput type --- cipher-core/src/lib.rs | 2 +- cipher-core/src/traits.rs | 10 ++--- cipher-core/src/types.rs | 78 +++++++++++++++++++++++++++++++++++ des/src/block/secret_block.rs | 41 ++++++++++++++++++ des/src/des.rs | 2 +- des/src/key/des_key.rs | 1 + des/src/lib.rs | 2 +- des/tests/des.rs | 19 +++++---- 8 files changed, 140 insertions(+), 15 deletions(-) diff --git a/cipher-core/src/lib.rs b/cipher-core/src/lib.rs index 4d15cd1..5746bb0 100644 --- a/cipher-core/src/lib.rs +++ b/cipher-core/src/lib.rs @@ -5,5 +5,5 @@ mod types; pub use { error::{CipherError, CipherResult}, traits::BlockCipher, - types::CipherAction, + types::{CipherAction, CipherOutput}, }; diff --git a/cipher-core/src/traits.rs b/cipher-core/src/traits.rs index a257466..4c29225 100644 --- a/cipher-core/src/traits.rs +++ b/cipher-core/src/traits.rs @@ -1,4 +1,4 @@ -use crate::{CipherAction, CipherError, CipherResult}; +use crate::{CipherAction, CipherError, CipherOutput, CipherResult}; /// Generic block cipher trait. /// @@ -13,7 +13,7 @@ pub trait BlockCipher: Sized { /// # Errors /// /// Returns `CipherError` if the transformation fails. - fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult>; + fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult; /// Transforms a block with validation. /// @@ -23,7 +23,7 @@ pub trait BlockCipher: Sized { /// # Errors /// /// Returns `CipherError::InvalidBlockSize` if `block.len() != BLOCK_SIZE`. - fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult> { + fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult { if block.len() != Self::BLOCK_SIZE { return Err(CipherError::invalid_block_size( Self::BLOCK_SIZE, @@ -38,7 +38,7 @@ pub trait BlockCipher: Sized { /// # Errors /// /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. - fn encrypt(&self, plaintext: &[u8]) -> CipherResult> { + fn encrypt(&self, plaintext: &[u8]) -> CipherResult { self.transform(plaintext, CipherAction::Encrypt) } @@ -47,7 +47,7 @@ pub trait BlockCipher: Sized { /// # Errors /// /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. - fn decrypt(&self, ciphertext: &[u8]) -> CipherResult> { + 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 index b7dbae7..29a6ed9 100644 --- a/cipher-core/src/types.rs +++ b/cipher-core/src/types.rs @@ -1,5 +1,83 @@ +use std::{ + fmt::{Binary, Display, LowerHex, Octal, UpperHex}, + ops::Deref, + str::from_utf8, +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CipherAction { Encrypt, Decrypt, } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CipherOutput(Vec); + +impl CipherOutput { + #[inline] + #[must_use] + pub fn new(value: &[u8]) -> Self { + Self(value.to_vec()) + } +} + +impl UpperHex for CipherOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for byte in &self.0 { + write!(f, "{byte:02X}")?; + } + Ok(()) + } +} + +impl LowerHex for CipherOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for byte in &self.0 { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +impl Octal for CipherOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for byte in &self.0 { + write!(f, "{byte:03o}")?; + } + Ok(()) + } +} + +impl Binary for CipherOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for byte in &self.0 { + write!(f, "{byte:08b}")?; + } + Ok(()) + } +} + +impl Display for CipherOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match from_utf8(&self.0) { + Ok(s) => f.write_str(s), + Err(_) => write!(f, "{self:X}"), + } + } +} + +impl Deref for CipherOutput { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for CipherOutput +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(value.into()) + } +} diff --git a/des/src/block/secret_block.rs b/des/src/block/secret_block.rs index 13ea19b..5942c46 100644 --- a/des/src/block/secret_block.rs +++ b/des/src/block/secret_block.rs @@ -21,6 +21,16 @@ macro_rules! secret_block { /// Mask to restrict the underlying integer to valid bits. pub const MASK: $int = $mask; + /// Calculate the number of hex digits needed for the bit width + const fn hex_width() -> usize { + ($bits as usize).div_ceil(4) + } + + /// Calculate the number of octal digits needed for the bit width + const fn octal_width() -> usize { + ($bits as usize).div_ceil(3) + } + #[inline] #[must_use] pub const fn new(value: $int) -> Self { @@ -36,6 +46,37 @@ macro_rules! secret_block { Self(v & Self::MASK) } } + + impl From<$name> for $int { + fn from(value: $name) -> $int { + value.0 + } + } + + impl std::fmt::UpperHex for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:0width$X}", self.0, width = Self::hex_width()) + } + } + + impl std::fmt::LowerHex for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:0width$x}", self.0, width = Self::hex_width()) + } + } + + impl std::fmt::Octal for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:0width$o}", self.0, width = Self::octal_width()) + } + } + + impl std::fmt::Binary for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:0width$b}", self.0, width = $bits) + } + } + }; // Helper: generate conversions_as based on type (@conversions_as u8) => { diff --git a/des/src/des.rs b/des/src/des.rs index 0a1fa5e..51bf0fb 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -41,7 +41,7 @@ impl BlockCipher for Des { &self, block: &[u8], action: cipher_core::CipherAction, - ) -> cipher_core::CipherResult> { + ) -> cipher_core::CipherResult { let block_arr: [u8; Self::BLOCK_SIZE] = block .try_into() .map_err(|_| CipherError::invalid_block_size(Self::BLOCK_SIZE, block.len()))?; diff --git a/des/src/key/des_key.rs b/des/src/key/des_key.rs index aa68dc1..5743c9c 100644 --- a/des/src/key/des_key.rs +++ b/des/src/key/des_key.rs @@ -4,6 +4,7 @@ use zeroize::ZeroizeOnDrop; /// 64-bit Key for DES #[derive(ZeroizeOnDrop)] pub struct Key([u8; 8]); + impl Key { #[inline] #[must_use] diff --git a/des/src/lib.rs b/des/src/lib.rs index 3dba8ea..1b6340d 100644 --- a/des/src/lib.rs +++ b/des/src/lib.rs @@ -4,4 +4,4 @@ mod des; mod key; pub(crate) mod utils; -pub use {des::Des, key::Subkeys}; +pub use des::Des; diff --git a/des/tests/des.rs b/des/tests/des.rs index fd701c0..b2360cf 100644 --- a/des/tests/des.rs +++ b/des/tests/des.rs @@ -1,4 +1,4 @@ -use cipher_core::BlockCipher; +use cipher_core::{BlockCipher, CipherOutput}; use claims::assert_ok; use des::Des; use rstest::rstest; @@ -120,9 +120,9 @@ fn encrypt_decrypt_roundtrip( 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")); + let ciphertext_u64 = cipher_block_to_u64(ciphertext); + let decrypted_u64 = cipher_block_to_u64(dectrypted); + let re_ciphertext_u64 = cipher_block_to_u64(re_ciphertext); assert_eq!( ciphertext_u64, expected_ciphertext, @@ -148,7 +148,7 @@ fn weak_keys(#[case] key: u64) { let ciphertext = assert_ok!(des.encrypt(&plaintext.to_be_bytes())); let decrypted = assert_ok!(des.decrypt(&ciphertext)); - let decrypted_u64 = u64::from_be_bytes(decrypted.try_into().expect("8 bytes")); + let decrypted_u64 = cipher_block_to_u64(decrypted); assert_eq!( decrypted_u64, plaintext, @@ -163,7 +163,7 @@ fn all_zero_paintext() { let plain = 0u64; let encrypted = assert_ok!(des.encrypt(&plain.to_be_bytes())); let decrypted = assert_ok!(des.decrypt(&encrypted)); - let decrypted_u64 = u64::from_be_bytes(decrypted.try_into().expect("8 bytes")); + let decrypted_u64 = cipher_block_to_u64(decrypted); assert_eq!(decrypted_u64, plain, "All-zero plaintext failed"); } @@ -174,7 +174,7 @@ fn all_one_paintext() { let plain = u64::MAX; let encrypted = assert_ok!(des.encrypt(&plain.to_be_bytes())); let decrypted = assert_ok!(des.decrypt(&encrypted)); - let decrypted_u64 = u64::from_be_bytes(decrypted.try_into().expect("8 bytes")); + let decrypted_u64 = cipher_block_to_u64(decrypted); assert_eq!(decrypted_u64, plain, "All-one plaintext failed"); } @@ -191,3 +191,8 @@ fn different_inputs() { "Encryption not deterministic for different inputs" ); } + +fn cipher_block_to_u64(block: CipherOutput) -> u64 { + let bytes = block.as_slice().try_into().expect("8 bytes"); + u64::from_be_bytes(bytes) +}