feat: add Key56 and Half28

This commit is contained in:
Kristofers Solo 2025-10-14 10:00:13 +03:00
parent e75eff8cd5
commit 9e91e90303
Signed by: kristoferssolo
GPG Key ID: 74FF8144483D82C8
8 changed files with 234 additions and 94 deletions

View File

@ -1,5 +1,5 @@
use crate::key::subkeys::Subkeys; use crate::key::Subkeys;
use cipher_core::KeyLike; use cipher_core::{CryptoResult, KeyLike};
#[derive(Debug)] #[derive(Debug)]
pub struct Des { pub struct Des {
@ -7,8 +7,8 @@ pub struct Des {
} }
impl Des { impl Des {
#[must_use] pub fn new(key: &impl KeyLike) -> CryptoResult<Self> {
pub fn new(_key: impl KeyLike) -> Self { let subkeys = Subkeys::from_key(key)?;
todo!() Ok(Self { subkeys })
} }
} }

65
des/src/key/half28.rs Normal file
View File

@ -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}"
);
}
}

36
des/src/key/key56.rs Normal file
View File

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

View File

@ -1,19 +1,8 @@
mod half28;
mod key56;
mod secret_int; mod secret_int;
pub mod subkeys; mod subkey;
mod subkeys;
use crate::secret_int; use crate::secret_int;
pub use subkeys::Subkeys;
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);
}

6
des/src/key/subkey.rs Normal file
View File

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

View File

@ -1,4 +1,7 @@
use crate::key::Subkey; use crate::{
key::{key56::Key56, subkey::Subkey},
utils::permutate,
};
use cipher_core::{CryptoError, CryptoResult, KeyLike}; use cipher_core::{CryptoError, CryptoResult, KeyLike};
use std::fmt::Debug; use std::fmt::Debug;
@ -49,39 +52,28 @@ impl Subkeys {
let key_bytes = key.as_bytes(); let key_bytes = key.as_bytes();
let key_len = key_bytes.len(); let key_len = key_bytes.len();
if key_len != 8 { let key_arr = key_bytes
return Err(CryptoError::invalid_key_size(8, key_len)); .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 key_be = u64::from_be_bytes(key_arr);
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 mut c = bits_to_u28(&c_bits); let cd_56 = pc1(key_be); // 56-bit: C0 + D0
let mut d = bits_to_u28(&d_bits); let (c, d) = cd_56.split();
let out = ROUND_ROTATIONS.iter().enumerate().fold( let subkeys = ROUND_ROTATIONS
[const { Subkey::zero() }; 16], .iter()
|mut acc, (round, &shifts)| { .map(|&shift_amount| {
c = rotate_left_28(c, shifts.into()); let cn = c.rotate_left(shift_amount); // C_(n-1) -> C_n
d = rotate_left_28(d, shifts.into()); let dn = d.rotate_left(shift_amount); // D_(n-1) -> D_n
let sub48 = pc2_from_cd(c, d); let combined = [cn, dn].into();
acc[round] = Subkey::from(sub48); pc2(&combined)
acc })
}, .collect::<Vec<Subkey>>()
); .try_into()
.expect("Exactly 16 subkeys expected");
Ok(Self(out)) Ok(Self(subkeys))
} }
pub(crate) fn as_u64_array(&self) -> [u64; 16] { pub(crate) fn as_u64_array(&self) -> [u64; 16] {
@ -95,50 +87,12 @@ impl Subkeys {
} }
} }
fn pc2_from_cd(c: u32, d: u32) -> u64 { fn pc1(key: u64) -> Key56 {
let combined: [u8; 56] = (0..28) permutate(key, 64, 56, &PC1).into()
.flat_map(|idx| {
let bit_idx = 27 - idx;
[((c >> bit_idx) & 1) as u8, ((d >> bit_idx) & 1) as u8]
})
.collect::<Vec<_>>()
.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
} }
const U28_MASK: u32 = 0x0FFF_FFFF; fn pc2(key: &Key56) -> Subkey {
permutate(key.as_int(), 56, 48, &PC2).into()
#[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
} }
impl Debug for Subkeys { impl Debug for Subkeys {
@ -152,3 +106,48 @@ impl Default for Subkeys {
Self::new_empty() 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");
}
}

View File

@ -1,5 +1,6 @@
pub(crate) mod constants; pub(crate) mod constants;
mod des; mod des;
mod key; mod key;
pub(crate) mod utils;
pub use des::Des; pub use {des::Des, key::Subkeys};

44
des/src/utils.rs Normal file
View File

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