From db53ae2ee710040d6ac9000ed19e87a3287f6c89 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 4 Nov 2025 10:03:20 +0200 Subject: [PATCH] refactor(des): use InputBlock trait --- cipher-core/src/error.rs | 33 ++++- cipher-core/src/lib.rs | 6 +- .../src/{traits.rs => traits/block_cipher.rs} | 10 +- cipher-core/src/traits/input_block.rs | 24 ++++ cipher-core/src/traits/mod.rs | 5 + cipher-core/src/types/mod.rs | 9 ++ cipher-core/src/{types.rs => types/output.rs} | 24 ++-- cli/src/args.rs | 11 +- cli/src/cipher.rs | 9 +- cli/src/error.rs | 26 ---- cli/src/main.rs | 6 +- cli/src/value.rs | 131 ------------------ des/src/block/block64.rs | 77 ++++++++++ des/src/des.rs | 2 +- des/src/lib.rs | 2 +- des/tests/des.rs | 4 +- 16 files changed, 180 insertions(+), 199 deletions(-) rename cipher-core/src/{traits.rs => traits/block_cipher.rs} (85%) create mode 100644 cipher-core/src/traits/input_block.rs create mode 100644 cipher-core/src/traits/mod.rs create mode 100644 cipher-core/src/types/mod.rs rename cipher-core/src/{types.rs => types/output.rs} (78%) delete mode 100644 cli/src/error.rs delete mode 100644 cli/src/value.rs diff --git a/cipher-core/src/error.rs b/cipher-core/src/error.rs index a0043b2..d97e325 100644 --- a/cipher-core/src/error.rs +++ b/cipher-core/src/error.rs @@ -1,6 +1,7 @@ +use std::num::ParseIntError; use thiserror::Error; -#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum CipherError { /// Invalid key size for the cipher #[error("Invalid key size: expected {expected} bytes, got {actual}")] @@ -27,3 +28,33 @@ impl CipherError { /// Type alias for clean Result types pub type CipherResult = core::result::Result; + +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum BlockError { + /// Input data is empty + #[error("Inputed block is empty")] + EmptyBlock, + + /// Parse error + #[error("Error parsing block")] + ParseError(#[from] ParseIntError), + + /// Byte size length + #[error("Invalid byte string length: expected no more than 8, found {0}")] + InvalidByteStringLength(usize), + + /// String to int conversion error + #[error("String-to-{typ} conversion error: {err}")] + ConversionError { typ: String, err: String }, +} + +impl BlockError { + #[inline] + #[must_use] + pub fn conversion_error(typ: &str, err: &str) -> Self { + Self::ConversionError { + typ: typ.into(), + err: err.into(), + } + } +} diff --git a/cipher-core/src/lib.rs b/cipher-core/src/lib.rs index 5746bb0..309d950 100644 --- a/cipher-core/src/lib.rs +++ b/cipher-core/src/lib.rs @@ -3,7 +3,7 @@ mod traits; mod types; pub use { - error::{CipherError, CipherResult}, - traits::BlockCipher, - types::{CipherAction, CipherOutput}, + error::{BlockError, CipherError, CipherResult}, + traits::{BlockCipher, BlockParser, InputBlock}, + types::{CipherAction, Output}, }; diff --git a/cipher-core/src/traits.rs b/cipher-core/src/traits/block_cipher.rs similarity index 85% rename from cipher-core/src/traits.rs rename to cipher-core/src/traits/block_cipher.rs index 7ad1add..dd91a45 100644 --- a/cipher-core/src/traits.rs +++ b/cipher-core/src/traits/block_cipher.rs @@ -1,4 +1,4 @@ -use crate::{CipherAction, CipherError, CipherOutput, CipherResult}; +use crate::{CipherAction, CipherError, CipherResult, Output}; /// Generic block cipher trait. /// @@ -15,7 +15,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. /// @@ -25,7 +25,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, @@ -40,7 +40,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) } @@ -49,7 +49,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/traits/input_block.rs b/cipher-core/src/traits/input_block.rs new file mode 100644 index 0000000..136ad99 --- /dev/null +++ b/cipher-core/src/traits/input_block.rs @@ -0,0 +1,24 @@ +use std::ops::{Deref, DerefMut}; + +pub trait InputBlock: Sized { + const BLOCK_SIZE: usize; + + fn as_bytes(&self) -> &[u8]; + fn as_bytes_mut(&mut self) -> &mut [u8]; +} + +#[derive(Debug, Clone)] +pub struct BlockParser(pub T); + +impl Deref for BlockParser { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BlockParser { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/cipher-core/src/traits/mod.rs b/cipher-core/src/traits/mod.rs new file mode 100644 index 0000000..bd35846 --- /dev/null +++ b/cipher-core/src/traits/mod.rs @@ -0,0 +1,5 @@ +mod block_cipher; +mod input_block; + +pub use block_cipher::BlockCipher; +pub use input_block::{BlockParser, InputBlock}; diff --git a/cipher-core/src/types/mod.rs b/cipher-core/src/types/mod.rs new file mode 100644 index 0000000..531d918 --- /dev/null +++ b/cipher-core/src/types/mod.rs @@ -0,0 +1,9 @@ +mod output; + +pub use output::Output; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CipherAction { + Encrypt, + Decrypt, +} diff --git a/cipher-core/src/types.rs b/cipher-core/src/types/output.rs similarity index 78% rename from cipher-core/src/types.rs rename to cipher-core/src/types/output.rs index 29a6ed9..4d12d0a 100644 --- a/cipher-core/src/types.rs +++ b/cipher-core/src/types/output.rs @@ -4,16 +4,10 @@ use std::{ str::from_utf8, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CipherAction { - Encrypt, - Decrypt, -} - #[derive(Debug, Clone, PartialEq, Eq)] -pub struct CipherOutput(Vec); +pub struct Output(Vec); -impl CipherOutput { +impl Output { #[inline] #[must_use] pub fn new(value: &[u8]) -> Self { @@ -21,7 +15,7 @@ impl CipherOutput { } } -impl UpperHex for CipherOutput { +impl UpperHex for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for byte in &self.0 { write!(f, "{byte:02X}")?; @@ -30,7 +24,7 @@ impl UpperHex for CipherOutput { } } -impl LowerHex for CipherOutput { +impl LowerHex for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for byte in &self.0 { write!(f, "{byte:02x}")?; @@ -39,7 +33,7 @@ impl LowerHex for CipherOutput { } } -impl Octal for CipherOutput { +impl Octal for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for byte in &self.0 { write!(f, "{byte:03o}")?; @@ -48,7 +42,7 @@ impl Octal for CipherOutput { } } -impl Binary for CipherOutput { +impl Binary for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for byte in &self.0 { write!(f, "{byte:08b}")?; @@ -57,7 +51,7 @@ impl Binary for CipherOutput { } } -impl Display for CipherOutput { +impl Display for Output { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match from_utf8(&self.0) { Ok(s) => f.write_str(s), @@ -66,14 +60,14 @@ impl Display for CipherOutput { } } -impl Deref for CipherOutput { +impl Deref for Output { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for CipherOutput +impl From for Output where T: Into>, { diff --git a/cli/src/args.rs b/cli/src/args.rs index e90220c..8a0f178 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -1,5 +1,6 @@ -use crate::{output::OutputFormat, value::Value}; +use crate::output::OutputFormat; use clap::{Parser, ValueEnum}; +use des::Block64; use std::str::FromStr; #[derive(Debug, Clone, Parser)] @@ -14,12 +15,12 @@ pub struct Args { pub algorithm: AlgorithmChoice, /// Key used for encryption/decryption. Can be a string or a path to a file - #[arg(short, long, value_parser = Value::from_str, required = true)] - pub key: Value, + #[arg(short, long, value_parser = Block64::from_str, required = true)] + pub key: Block64, /// The text to encrypt/decrypt. Can be a string or a path to a file - #[arg(value_name = "TEXT", value_parser = Value::from_str, required = true)] - pub text: Value, + #[arg(value_name = "TEXT", value_parser = Block64::from_str, required = true)] + pub text: Block64, /// Output format for decrypted data #[arg(short = 'f', long)] diff --git a/cli/src/cipher.rs b/cli/src/cipher.rs index 96e3616..4061fa0 100644 --- a/cli/src/cipher.rs +++ b/cli/src/cipher.rs @@ -1,13 +1,12 @@ -use crate::{args::AlgorithmChoice, value::Value}; -use cipher_core::BlockCipher; +use crate::args::AlgorithmChoice; +use cipher_core::{BlockCipher, InputBlock}; use des::Des; impl AlgorithmChoice { #[must_use] - pub fn get_cipher(&self, key: Value) -> impl BlockCipher { - let key = key.to_be_bytes(); + pub fn get_cipher(&self, key: &impl InputBlock) -> impl BlockCipher { match self { - Self::Des => Des::from_key(&key), + Self::Des => Des::from_key(key.as_bytes()), Self::Aes => todo!("Must implement AES first"), } } diff --git a/cli/src/error.rs b/cli/src/error.rs deleted file mode 100644 index a8efbfc..0000000 --- a/cli/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ValueError { - #[error("String contains no content")] - EmptyString, - - #[error("File '{0}' contains no content")] - EmptyFile(PathBuf), - - #[error("Failed to find file '{0}'. File does not exist")] - MissingFile(PathBuf), - - #[error("Failed to read file '{0}'. Cannot read file contents")] - FileReadingError(PathBuf), - - #[error("Invalid number format: {0}")] - InvalidFormat(String), - - #[error("Invalid byte string length: expected no more than 8, found {0}")] - InvalidByteStringLength(usize), - - #[error("String-to-u64 conversion error: {0}")] - ConversionError(String), -} diff --git a/cli/src/main.rs b/cli/src/main.rs index 252676d..cab70de 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,8 +1,6 @@ mod args; mod cipher; -mod error; mod output; -mod value; use crate::{ args::{Args, OperationChoice}, @@ -23,12 +21,12 @@ fn main() -> color_eyre::Result<()> { match operation { OperationChoice::Encrypt => { - let cipher = algorithm.get_cipher(key); + let cipher = algorithm.get_cipher(&key); let ciphertext = cipher.encrypt(&text.to_be_bytes())?; println!("{ciphertext:016X}"); } OperationChoice::Decrypt => { - let cipher = algorithm.get_cipher(key); + let cipher = algorithm.get_cipher(&key); let plaintext = cipher.decrypt(&text.to_be_bytes())?; match output_format.unwrap_or_default() { OutputFormat::Binary => println!("{plaintext:064b}"), diff --git a/cli/src/value.rs b/cli/src/value.rs deleted file mode 100644 index 3a15dd3..0000000 --- a/cli/src/value.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::error::ValueError; -use std::{ - fmt::{Display, LowerHex, UpperHex}, - fs::read_to_string, - path::PathBuf, - str::FromStr, -}; - -#[derive(Debug, Clone, Copy)] -pub struct Value(u64); - -impl Value { - #[inline] - #[must_use] - pub const fn as_64(self) -> u64 { - self.0 - } - - #[inline] - #[must_use] - pub const fn to_be_bytes(self) -> [u8; 8] { - self.0.to_be_bytes() - } -} - -impl From for u64 { - fn from(value: Value) -> Self { - value.as_64() - } -} - -impl From for Value { - fn from(value: u64) -> Self { - Self(value) - } -} - -impl FromStr for Value { - type Err = ValueError; - fn from_str(s: &str) -> Result { - if let Ok(num) = s.parse::() { - return Ok(Self(num)); - } - - let path = PathBuf::from(s); - if path.exists() && path.is_file() { - if let Ok(contents) = read_to_string(&path) { - let value = parse_string_to_u64(&contents)?; - return Ok(Self(value)); - } - return Err(ValueError::FileReadingError(path)); - } - - let value = parse_string_to_u64(s)?; - Ok(Self(value)) - } -} - -fn parse_string_to_u64(s: &str) -> Result { - let trimmed = s.trim(); - - if trimmed.is_empty() { - return Err(ValueError::EmptyString); - } - - // Hexadecimal with 0x/0X prefix - if let Some(hex_str) = trimmed - .strip_prefix("0X") - .or_else(|| trimmed.strip_prefix("0x")) - { - return parse_radix(hex_str, 16, "Hex"); - } - - // Binary with 0b/0B prefix - if let Some(bin_str) = trimmed - .strip_prefix("0b") - .or_else(|| trimmed.strip_prefix("0B")) - { - return parse_radix(bin_str, 2, "Binary"); - } - - // 8-character ASCII string conversion to u64 - if trimmed.len() > 8 { - return Err(ValueError::InvalidByteStringLength(trimmed.len())); - } - - ascii_string_to_u64(trimmed) -} - -fn parse_radix(s: &str, radix: u32, name: &str) -> Result { - let trimmed = s.trim_start_matches('0'); - if trimmed.is_empty() { - return Ok(0); - } - - u64::from_str_radix(trimmed, radix) - .map_err(|e| ValueError::InvalidFormat(format!("{name} parsing failed: {e}"))) -} - -fn ascii_string_to_u64(s: &str) -> Result { - if !s.is_ascii() { - return Err(ValueError::ConversionError( - "String contains non-ASCII characters".into(), - )); - } - - let mut bytes = [0; 8]; - for (idx, byte) in s.bytes().enumerate() { - bytes[idx] = byte; - } - - Ok(u64::from_be_bytes(bytes)) -} - -impl Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:0b}", self.0) - } -} - -impl UpperHex for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016X}", self.0) - } -} - -impl LowerHex for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016x}", self.0) - } -} diff --git a/des/src/block/block64.rs b/des/src/block/block64.rs index 9da15d4..23482c6 100644 --- a/des/src/block/block64.rs +++ b/des/src/block/block64.rs @@ -1,9 +1,24 @@ use crate::block::{lr::LR, secret_block}; +use cipher_core::{BlockError, InputBlock}; +use std::{ + slice::{from_raw_parts, from_raw_parts_mut}, + str::FromStr, +}; secret_block! { pub struct Block64(u64, 64, 0xFFFF_FFFF_FFFF_FFFF); } +impl InputBlock for Block64 { + const BLOCK_SIZE: usize = 64; + fn as_bytes(&self) -> &[u8] { + unsafe { from_raw_parts((&raw const self.0).cast::().cast::(), 8) } + } + fn as_bytes_mut(&mut self) -> &mut [u8] { + unsafe { from_raw_parts_mut((&raw mut self.0).cast::().cast::(), 8) } + } +} + impl Block64 { #[inline] #[must_use] @@ -30,6 +45,68 @@ impl Block64 { } } +impl FromStr for Block64 { + type Err = BlockError; + fn from_str(s: &str) -> Result { + Ok(Self(parse_string_to_u64(s)?)) + } +} + +fn parse_string_to_u64(s: &str) -> Result { + let trimmed = s.trim(); + + if trimmed.is_empty() { + return Err(BlockError::EmptyBlock); + } + + // Hexadecimal with 0x/0X prefix + if let Some(hex_str) = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + { + return parse_radix(hex_str, 16); + } + // Binary with 0b/0B prefix + if let Some(bin_str) = trimmed + .strip_prefix("0b") + .or_else(|| trimmed.strip_prefix("0B")) + { + return parse_radix(bin_str, 2); + } + + // 8-character ASCII string conversion to u64 + if trimmed.len() > 8 { + return Err(BlockError::InvalidByteStringLength(trimmed.len())); + } + + ascii_string_to_u64(trimmed) +} + +fn parse_radix(s: &str, radix: u32) -> Result { + let trimmed = s.trim_start_matches('0'); + if trimmed.is_empty() { + return Ok(0); + } + + u64::from_str_radix(trimmed, radix).map_err(BlockError::from) +} + +fn ascii_string_to_u64(s: &str) -> Result { + if !s.is_ascii() { + return Err(BlockError::conversion_error( + "u64", + "String contains non-ASCII characters", + )); + } + + let mut bytes = [0; 8]; + for (idx, byte) in s.bytes().enumerate() { + bytes[idx] = byte; + } + + Ok(u64::from_be_bytes(bytes)) +} + impl From<[u8; 8]> for Block64 { fn from(bytes: [u8; 8]) -> Self { Self::from_be_bytes(bytes) diff --git a/des/src/des.rs b/des/src/des.rs index 6d8ed73..af70774 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -45,7 +45,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/lib.rs b/des/src/lib.rs index 1b6340d..a1a5cdb 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; +pub use {block::Block64, des::Des}; diff --git a/des/tests/des.rs b/des/tests/des.rs index b2360cf..420f2cc 100644 --- a/des/tests/des.rs +++ b/des/tests/des.rs @@ -1,4 +1,4 @@ -use cipher_core::{BlockCipher, CipherOutput}; +use cipher_core::{BlockCipher, Output}; use claims::assert_ok; use des::Des; use rstest::rstest; @@ -192,7 +192,7 @@ fn different_inputs() { ); } -fn cipher_block_to_u64(block: CipherOutput) -> u64 { +fn cipher_block_to_u64(block: Output) -> u64 { let bytes = block.as_slice().try_into().expect("8 bytes"); u64::from_be_bytes(bytes) }