From 9e91e903034f76d047392d07235dd9d36448ee8a Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 14 Oct 2025 10:00:13 +0300 Subject: [PATCH] feat: add Key56 and Half28 --- des/src/des.rs | 10 +-- des/src/key/half28.rs | 65 +++++++++++++++++++ des/src/key/key56.rs | 36 +++++++++++ des/src/key/mod.rs | 21 ++---- des/src/key/subkey.rs | 6 ++ des/src/key/subkeys.rs | 143 ++++++++++++++++++++--------------------- des/src/lib.rs | 3 +- des/src/utils.rs | 44 +++++++++++++ 8 files changed, 234 insertions(+), 94 deletions(-) create mode 100644 des/src/key/half28.rs create mode 100644 des/src/key/key56.rs create mode 100644 des/src/key/subkey.rs create mode 100644 des/src/utils.rs diff --git a/des/src/des.rs b/des/src/des.rs index c74f61e..3173b34 100644 --- a/des/src/des.rs +++ b/des/src/des.rs @@ -1,5 +1,5 @@ -use crate::key::subkeys::Subkeys; -use cipher_core::KeyLike; +use crate::key::Subkeys; +use cipher_core::{CryptoResult, KeyLike}; #[derive(Debug)] pub struct Des { @@ -7,8 +7,8 @@ pub struct Des { } impl Des { - #[must_use] - pub fn new(_key: impl KeyLike) -> Self { - todo!() + pub fn new(key: &impl KeyLike) -> CryptoResult { + let subkeys = Subkeys::from_key(key)?; + Ok(Self { subkeys }) } } diff --git a/des/src/key/half28.rs b/des/src/key/half28.rs new file mode 100644 index 0000000..c64900c --- /dev/null +++ b/des/src/key/half28.rs @@ -0,0 +1,65 @@ +use crate::key::secret_int; + +secret_int! { + /// 28-bit half (C or D), stored in lower 28 bits of u32. + pub struct Half28(u32, 28, 0x0FFF_FFFF); +} + +impl Half28 { + #[must_use] + pub const fn rotate_left(&self, amount: u8) -> Self { + 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) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(0x0F0C_CAAF, 0x0E19_955F, 1)] // C_1 + #[case(0x0E19_955F, 0x0C33_2ABF, 1)] // C_2 + #[case(0x0C33_2ABF, 0x00CC_AAFF, 2)] // C_3 + #[case(0x00CC_AAFF, 0x0332_ABFC, 2)] // C_4 + #[case(0x0332_ABFC, 0x0CCA_AFF0, 2)] // C_5 + #[case(0x0CCA_AFF0, 0x032A_BFC3, 2)] // C_6 + #[case(0x032A_BFC3, 0x0CAA_FF0C, 2)] // C_7 + #[case(0x0CAA_FF0C, 0x02AB_FC33, 2)] // C_8 + #[case(0x02AB_FC33, 0x0557_F866, 1)] // C_9 + #[case(0x0557_F866, 0x055F_E199, 2)] // C_10 + #[case(0x055F_E199, 0x057F_8665, 2)] // C_11 + #[case(0x057F_8665, 0x05FE_1995, 2)] // C_12 + #[case(0x05FE_1995, 0x07F8_6655, 2)] // C_13 + #[case(0x07F8_6655, 0x0FE1_9955, 2)] // C_14 + #[case(0x0FE1_9955, 0x0F86_6557, 2)] // C_15 + #[case(0x0F86_6557, 0x0F0C_CAAF, 1)] // C_16 + #[case(0x0556_678F, 0x0AAC_CF1E, 1)] // D_1 + #[case(0x0AAC_CF1E, 0x0559_9E3D, 1)] // D_2 + #[case(0x0559_9E3D, 0x0566_78F5, 2)] // D_3 + #[case(0x0566_78F5, 0x0599_E3D5, 2)] // D_4 + #[case(0x0599_E3D5, 0x0667_8F55, 2)] // D_5 + #[case(0x0667_8F55, 0x099E_3D55, 2)] // D_6 + #[case(0x099E_3D55, 0x0678_F556, 2)] // D_7 + #[case(0x0678_F556, 0x09E3_D559, 2)] // D_8 + #[case(0x09E3_D559, 0x03C7_AAB3, 1)] // D_9 + #[case(0x03C7_AAB3, 0x0F1E_AACC, 2)] // D_10 + #[case(0x0F1E_AACC, 0x0C7A_AB33, 2)] // D_11 + #[case(0x0C7A_AB33, 0x01EA_ACCF, 2)] // D_12 + #[case(0x01EA_ACCF, 0x07AA_B33C, 2)] // D_13 + #[case(0x07AA_B33C, 0x0EAA_CCF1, 2)] // D_14 + #[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(); + + assert_eq!( + result, expected, + "shift(0x{key:08X}, {amount}) should equal 0x{expected:08X}" + ); + } +} diff --git a/des/src/key/key56.rs b/des/src/key/key56.rs new file mode 100644 index 0000000..833c789 --- /dev/null +++ b/des/src/key/key56.rs @@ -0,0 +1,36 @@ +use crate::{key::half28::Half28, secret_int}; + +secret_int! { + /// 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) -> (Half28, Half28) { + let c = ((self.0 >> 28) & 0x0FFF_FFFF) as u32; + let d = (self.0 & 0x0FFF_FFFF) as u32; + (c.into(), d.into()) + } + + #[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) + } +} + +impl From<[Half28; 2]> for Key56 { + fn from(keys: [Half28; 2]) -> Self { + let [left, right] = keys; + Self::from_half28(&left, &right) + } +} + +impl From<&[Half28; 2]> for Key56 { + fn from(keys: &[Half28; 2]) -> Self { + let [left, right] = keys; + Self::from_half28(left, right) + } +} diff --git a/des/src/key/mod.rs b/des/src/key/mod.rs index 951536e..24cfd7b 100644 --- a/des/src/key/mod.rs +++ b/des/src/key/mod.rs @@ -1,19 +1,8 @@ +mod half28; +mod key56; mod secret_int; -pub mod subkeys; +mod subkey; +mod subkeys; use crate::secret_int; - -secret_int! { - /// A single DES round subkey (48 bits stored in lower bits of u64). - pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFF); -} - -secret_int! { - /// 56-bit key after PC-1 (lower 56 bits used). - pub struct Key56(u64, 56, 0x00FF_FFFF_FFFF_FFFF); -} - -secret_int! { - /// 28-bit half (C or D), stored in lower 28 bits of u32. - pub struct Half28(u32, 28, 0x0FFF_FFFF); -} +pub use subkeys::Subkeys; diff --git a/des/src/key/subkey.rs b/des/src/key/subkey.rs new file mode 100644 index 0000000..cff1bc2 --- /dev/null +++ b/des/src/key/subkey.rs @@ -0,0 +1,6 @@ +use crate::key::secret_int; + +secret_int! { + /// 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 d3bcc70..acfe3f1 100644 --- a/des/src/key/subkeys.rs +++ b/des/src/key/subkeys.rs @@ -1,4 +1,7 @@ -use crate::key::Subkey; +use crate::{ + key::{key56::Key56, subkey::Subkey}, + utils::permutate, +}; use cipher_core::{CryptoError, CryptoResult, KeyLike}; use std::fmt::Debug; @@ -49,39 +52,28 @@ impl Subkeys { let key_bytes = key.as_bytes(); let key_len = key_bytes.len(); - if key_len != 8 { - return Err(CryptoError::invalid_key_size(8, key_len)); - } + let key_arr = key_bytes + .try_into() + .map_err(|_| CryptoError::invalid_key_size(8, key_len))?; - // Produce 56 bits after PC-1 as two 28-bit halves C0 and D0 (MSB-first bit order) - let (c_bits, d_bits) = PC1.iter().enumerate().fold( - ([0; 28], [0; 28]), - |(mut c_bits, mut d_bits), (idx, &pos)| { - let bit = get_bit_be(key_bytes, pos); - if idx < 28 { - c_bits[idx] = bit; - } else { - d_bits[idx - 28] = bit; - } - (c_bits, d_bits) - }, - ); + let key_be = u64::from_be_bytes(key_arr); - let mut c = bits_to_u28(&c_bits); - let mut d = bits_to_u28(&d_bits); + let cd_56 = pc1(key_be); // 56-bit: C0 + D0 + let (c, d) = cd_56.split(); - let out = ROUND_ROTATIONS.iter().enumerate().fold( - [const { Subkey::zero() }; 16], - |mut acc, (round, &shifts)| { - c = rotate_left_28(c, shifts.into()); - d = rotate_left_28(d, shifts.into()); - let sub48 = pc2_from_cd(c, d); - acc[round] = Subkey::from(sub48); - acc - }, - ); + 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) + }) + .collect::>() + .try_into() + .expect("Exactly 16 subkeys expected"); - Ok(Self(out)) + Ok(Self(subkeys)) } pub(crate) fn as_u64_array(&self) -> [u64; 16] { @@ -95,50 +87,12 @@ impl Subkeys { } } -fn pc2_from_cd(c: u32, d: u32) -> u64 { - let combined: [u8; 56] = (0..28) - .flat_map(|idx| { - let bit_idx = 27 - idx; - [((c >> bit_idx) & 1) as u8, ((d >> bit_idx) & 1) as u8] - }) - .collect::>() - .try_into() - .expect(""); - - let out = PC2.iter().fold(0, |acc, &pos| { - let bit = u64::from(combined[(pos as usize).saturating_sub(1)]); - (acc << 1) | bit - }); - - out & 0xFFFF_FFFF_FFFF +fn pc1(key: u64) -> Key56 { + permutate(key, 64, 56, &PC1).into() } -const U28_MASK: u32 = 0x0FFF_FFFF; - -#[inline] -#[must_use] -const fn get_bit_be(bytes: &[u8], pos: u8) -> u8 { - let p = (pos as usize).saturating_sub(1); - let byte_idx = p / 8; - let bit_idx = 7 - (p % 8); - (bytes[byte_idx] >> bit_idx) & 1 -} - -#[inline] -#[must_use] -fn bits_to_u28(bits: &[u8; 28]) -> u32 { - let mut v = 0; - for &b in bits { - v = (v << 1) | (u32::from(b)); - } - v & U28_MASK -} - -#[inline] -#[must_use] -const fn rotate_left_28(v: u32, n: u32) -> u32 { - let v = v & U28_MASK; - ((v << n) | (v >> (28 - n))) & U28_MASK +fn pc2(key: &Key56) -> Subkey { + permutate(key.as_int(), 56, 48, &PC2).into() } impl Debug for Subkeys { @@ -152,3 +106,48 @@ impl Default for Subkeys { Self::new_empty() } } + +#[cfg(test)] +mod tests { + use super::*; + 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)] + fn pc1_permutaion_correct(#[case] key: u64, #[case] expected: u64) { + let result = pc1(key); + + assert_eq!( + result.as_int(), + expected, + "PC1 permutation failed. Expected {expected:08X}, got {:08X}", + result.as_int() + ); + } + + #[rstest] + #[case(0x00F0_CCAA_F556_678F, 0xCB3D_8B0E_17F5)] // K_0 + #[case(0x00E1_9955_FAAC_CF1E, 0x1B02_EFFC_7072)] // K_1 + #[case(0x00C3_32AB_F559_9E3D, 0x79AE_D9DB_C9E5)] // K_2 + #[case(0x000C_CAAF_F566_78F5, 0x55FC_8A42_CF99)] // K_3 + #[case(0x0033_2ABF_C599_E3D5, 0x72AD_D6DB_351D)] // K_4 + #[case(0x00CC_AAFF_0667_8F55, 0x7CEC_07EB_53A8)] // K_5 + #[case(0x0032_ABFC_399E_3D55, 0x63A5_3E50_7B2F)] // K_6 + #[case(0x00CA_AFF0_C678_F556, 0xEC84_B7F6_18BC)] // K_7 + #[case(0x002A_BFC3_39E3_D559, 0xF78A_3AC1_3BFB)] // K_8 + #[case(0x0055_7F86_63C7_AAB3, 0xE0DB_EBED_E781)] // K_9 + #[case(0x0055_FE19_9F1E_AACC, 0xB1F3_47BA_464F)] // K_10 + #[case(0x0057_F866_5C7A_AB33, 0x215F_D3DE_D386)] // K_11 + #[case(0x005F_E199_51EA_ACCF, 0x7571_F594_67E9)] // K_12 + #[case(0x007F_8665_57AA_B33C, 0x97C5_D1FA_BA41)] // K_13 + #[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"); + } +} diff --git a/des/src/lib.rs b/des/src/lib.rs index f2d37c8..0200aa4 100644 --- a/des/src/lib.rs +++ b/des/src/lib.rs @@ -1,5 +1,6 @@ pub(crate) mod constants; mod des; mod key; +pub(crate) mod utils; -pub use des::Des; +pub use {des::Des, key::Subkeys}; diff --git a/des/src/utils.rs b/des/src/utils.rs new file mode 100644 index 0000000..b4304b7 --- /dev/null +++ b/des/src/utils.rs @@ -0,0 +1,44 @@ +/// Generic bit permutation for DES specification. +/// DES uses 1-based positioning where bit 1 is the leftmost (MSB) and bit 64 is the rightmost (LSB). +/// This function handles this correctly for arbitrary input/output sizes. +/// +/// # Arguments +/// - `input` - The input value (treated as a bitfield of `input_bits` size) +/// - `input_bits` - Number of meaningful bits in the input (1-64) +/// - `output_bits` - Number of bits in the output (1-64) +/// - `position_table` - Table of 1-based positions (1 to `input_bits`) where each output bit comes from. +/// The table should have `output_bits` entries. For each entry i (0-based), `position_table`[i] indicates +/// which input bit (1-based) should go to the i-th output bit position (from MSB to LSB). +#[must_use] +pub fn permutate( + input: u64, + input_bit_amount: u64, + 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 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 + }) +}