refactor(des): use InputBlock trait

This commit is contained in:
Kristofers Solo 2025-11-04 10:03:20 +02:00
parent 211a7cefe6
commit db53ae2ee7
Signed by: kristoferssolo
GPG Key ID: 74FF8144483D82C8
16 changed files with 180 additions and 199 deletions

View File

@ -1,6 +1,7 @@
use std::num::ParseIntError;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum CipherError { pub enum CipherError {
/// Invalid key size for the cipher /// Invalid key size for the cipher
#[error("Invalid key size: expected {expected} bytes, got {actual}")] #[error("Invalid key size: expected {expected} bytes, got {actual}")]
@ -27,3 +28,33 @@ impl CipherError {
/// Type alias for clean Result types /// Type alias for clean Result types
pub type CipherResult<T> = core::result::Result<T, CipherError>; pub type CipherResult<T> = core::result::Result<T, CipherError>;
#[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(),
}
}
}

View File

@ -3,7 +3,7 @@ mod traits;
mod types; mod types;
pub use { pub use {
error::{CipherError, CipherResult}, error::{BlockError, CipherError, CipherResult},
traits::BlockCipher, traits::{BlockCipher, BlockParser, InputBlock},
types::{CipherAction, CipherOutput}, types::{CipherAction, Output},
}; };

View File

@ -1,4 +1,4 @@
use crate::{CipherAction, CipherError, CipherOutput, CipherResult}; use crate::{CipherAction, CipherError, CipherResult, Output};
/// Generic block cipher trait. /// Generic block cipher trait.
/// ///
@ -15,7 +15,7 @@ pub trait BlockCipher: Sized {
/// # Errors /// # Errors
/// ///
/// Returns `CipherError` if the transformation fails. /// Returns `CipherError` if the transformation fails.
fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult<CipherOutput>; fn transform_impl(&self, block: &[u8], action: CipherAction) -> CipherResult<Output>;
/// Transforms a block with validation. /// Transforms a block with validation.
/// ///
@ -25,7 +25,7 @@ pub trait BlockCipher: Sized {
/// # Errors /// # Errors
/// ///
/// Returns `CipherError::InvalidBlockSize` if `block.len() != BLOCK_SIZE`. /// Returns `CipherError::InvalidBlockSize` if `block.len() != BLOCK_SIZE`.
fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult<CipherOutput> { fn transform(&self, block: &[u8], action: CipherAction) -> CipherResult<Output> {
if block.len() != Self::BLOCK_SIZE { if block.len() != Self::BLOCK_SIZE {
return Err(CipherError::invalid_block_size( return Err(CipherError::invalid_block_size(
Self::BLOCK_SIZE, Self::BLOCK_SIZE,
@ -40,7 +40,7 @@ pub trait BlockCipher: Sized {
/// # Errors /// # Errors
/// ///
/// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes.
fn encrypt(&self, plaintext: &[u8]) -> CipherResult<CipherOutput> { fn encrypt(&self, plaintext: &[u8]) -> CipherResult<Output> {
self.transform(plaintext, CipherAction::Encrypt) self.transform(plaintext, CipherAction::Encrypt)
} }
@ -49,7 +49,7 @@ pub trait BlockCipher: Sized {
/// # Errors /// # Errors
/// ///
/// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes. /// Returns `CipherError::InvalidBlockSize` if the plaintext is not exactly `BLOCK_SIZE` bytes.
fn decrypt(&self, ciphertext: &[u8]) -> CipherResult<CipherOutput> { fn decrypt(&self, ciphertext: &[u8]) -> CipherResult<Output> {
self.transform(ciphertext, CipherAction::Decrypt) self.transform(ciphertext, CipherAction::Decrypt)
} }
} }

View File

@ -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<T: InputBlock>(pub T);
impl<T: InputBlock> Deref for BlockParser<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: InputBlock> DerefMut for BlockParser<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -0,0 +1,5 @@
mod block_cipher;
mod input_block;
pub use block_cipher::BlockCipher;
pub use input_block::{BlockParser, InputBlock};

View File

@ -0,0 +1,9 @@
mod output;
pub use output::Output;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CipherAction {
Encrypt,
Decrypt,
}

View File

@ -4,16 +4,10 @@ use std::{
str::from_utf8, str::from_utf8,
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CipherAction {
Encrypt,
Decrypt,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CipherOutput(Vec<u8>); pub struct Output(Vec<u8>);
impl CipherOutput { impl Output {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn new(value: &[u8]) -> Self { 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 { for byte in &self.0 {
write!(f, "{byte:02X}")?; 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 { for byte in &self.0 {
write!(f, "{byte:02x}")?; 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 { for byte in &self.0 {
write!(f, "{byte:03o}")?; 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 { for byte in &self.0 {
write!(f, "{byte:08b}")?; 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match from_utf8(&self.0) { match from_utf8(&self.0) {
Ok(s) => f.write_str(s), 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<u8>; type Target = Vec<u8>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl<T> From<T> for CipherOutput impl<T> From<T> for Output
where where
T: Into<Vec<u8>>, T: Into<Vec<u8>>,
{ {

View File

@ -1,5 +1,6 @@
use crate::{output::OutputFormat, value::Value}; use crate::output::OutputFormat;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use des::Block64;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
@ -14,12 +15,12 @@ pub struct Args {
pub algorithm: AlgorithmChoice, pub algorithm: AlgorithmChoice,
/// Key used for encryption/decryption. Can be a string or a path to a file /// 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)] #[arg(short, long, value_parser = Block64::from_str, required = true)]
pub key: Value, pub key: Block64,
/// The text to encrypt/decrypt. Can be a string or a path to a file /// 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)] #[arg(value_name = "TEXT", value_parser = Block64::from_str, required = true)]
pub text: Value, pub text: Block64,
/// Output format for decrypted data /// Output format for decrypted data
#[arg(short = 'f', long)] #[arg(short = 'f', long)]

View File

@ -1,13 +1,12 @@
use crate::{args::AlgorithmChoice, value::Value}; use crate::args::AlgorithmChoice;
use cipher_core::BlockCipher; use cipher_core::{BlockCipher, InputBlock};
use des::Des; use des::Des;
impl AlgorithmChoice { impl AlgorithmChoice {
#[must_use] #[must_use]
pub fn get_cipher(&self, key: Value) -> impl BlockCipher { pub fn get_cipher(&self, key: &impl InputBlock) -> impl BlockCipher {
let key = key.to_be_bytes();
match self { match self {
Self::Des => Des::from_key(&key), Self::Des => Des::from_key(key.as_bytes()),
Self::Aes => todo!("Must implement AES first"), Self::Aes => todo!("Must implement AES first"),
} }
} }

View File

@ -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),
}

View File

@ -1,8 +1,6 @@
mod args; mod args;
mod cipher; mod cipher;
mod error;
mod output; mod output;
mod value;
use crate::{ use crate::{
args::{Args, OperationChoice}, args::{Args, OperationChoice},
@ -23,12 +21,12 @@ fn main() -> color_eyre::Result<()> {
match operation { match operation {
OperationChoice::Encrypt => { OperationChoice::Encrypt => {
let cipher = algorithm.get_cipher(key); let cipher = algorithm.get_cipher(&key);
let ciphertext = cipher.encrypt(&text.to_be_bytes())?; let ciphertext = cipher.encrypt(&text.to_be_bytes())?;
println!("{ciphertext:016X}"); println!("{ciphertext:016X}");
} }
OperationChoice::Decrypt => { OperationChoice::Decrypt => {
let cipher = algorithm.get_cipher(key); let cipher = algorithm.get_cipher(&key);
let plaintext = cipher.decrypt(&text.to_be_bytes())?; let plaintext = cipher.decrypt(&text.to_be_bytes())?;
match output_format.unwrap_or_default() { match output_format.unwrap_or_default() {
OutputFormat::Binary => println!("{plaintext:064b}"), OutputFormat::Binary => println!("{plaintext:064b}"),

View File

@ -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<Value> for u64 {
fn from(value: Value) -> Self {
value.as_64()
}
}
impl From<u64> for Value {
fn from(value: u64) -> Self {
Self(value)
}
}
impl FromStr for Value {
type Err = ValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(num) = s.parse::<u64>() {
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<u64, ValueError> {
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<u64, ValueError> {
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<u64, ValueError> {
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)
}
}

View File

@ -1,9 +1,24 @@
use crate::block::{lr::LR, secret_block}; 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! { secret_block! {
pub struct Block64(u64, 64, 0xFFFF_FFFF_FFFF_FFFF); 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::<u64>().cast::<u8>(), 8) }
}
fn as_bytes_mut(&mut self) -> &mut [u8] {
unsafe { from_raw_parts_mut((&raw mut self.0).cast::<u64>().cast::<u8>(), 8) }
}
}
impl Block64 { impl Block64 {
#[inline] #[inline]
#[must_use] #[must_use]
@ -30,6 +45,68 @@ impl Block64 {
} }
} }
impl FromStr for Block64 {
type Err = BlockError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(parse_string_to_u64(s)?))
}
}
fn parse_string_to_u64(s: &str) -> Result<u64, BlockError> {
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<u64, BlockError> {
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<u64, BlockError> {
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 { impl From<[u8; 8]> for Block64 {
fn from(bytes: [u8; 8]) -> Self { fn from(bytes: [u8; 8]) -> Self {
Self::from_be_bytes(bytes) Self::from_be_bytes(bytes)

View File

@ -45,7 +45,7 @@ impl BlockCipher for Des {
&self, &self,
block: &[u8], block: &[u8],
action: cipher_core::CipherAction, action: cipher_core::CipherAction,
) -> cipher_core::CipherResult<cipher_core::CipherOutput> { ) -> cipher_core::CipherResult<cipher_core::Output> {
let block_arr: [u8; Self::BLOCK_SIZE] = block let block_arr: [u8; Self::BLOCK_SIZE] = block
.try_into() .try_into()
.map_err(|_| CipherError::invalid_block_size(Self::BLOCK_SIZE, block.len()))?; .map_err(|_| CipherError::invalid_block_size(Self::BLOCK_SIZE, block.len()))?;

View File

@ -4,4 +4,4 @@ mod des;
mod key; mod key;
pub(crate) mod utils; pub(crate) mod utils;
pub use des::Des; pub use {block::Block64, des::Des};

View File

@ -1,4 +1,4 @@
use cipher_core::{BlockCipher, CipherOutput}; use cipher_core::{BlockCipher, Output};
use claims::assert_ok; use claims::assert_ok;
use des::Des; use des::Des;
use rstest::rstest; 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"); let bytes = block.as_slice().try_into().expect("8 bytes");
u64::from_be_bytes(bytes) u64::from_be_bytes(bytes)
} }