mirror of
https://github.com/kristoferssolo/des-rs.git
synced 2026-02-25 04:58:17 +00:00
refactor: use workspace
This commit is contained in:
15
des-lib/Cargo.toml
Normal file
15
des-lib/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "des-lib"
|
||||
version = "0.1.0"
|
||||
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
claims.workspace = true
|
||||
rand.workspace = true
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
102
des-lib/src/constants.rs
Normal file
102
des-lib/src/constants.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// DES Constants (from FIPS 46-3 spec)
|
||||
/// Initial Permutation (IP) table.
|
||||
pub const IP: [u8; 64] = [
|
||||
58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6,
|
||||
64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61,
|
||||
53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
||||
];
|
||||
|
||||
/// Inverse Initial Permutation (FP) table.
|
||||
pub const FP: [u8; 64] = [
|
||||
40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30,
|
||||
37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
|
||||
34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25,
|
||||
];
|
||||
|
||||
/// Key Permutation table (64 to 56 bits).
|
||||
pub const PC1_TABLE: [u8; 56] = [
|
||||
57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60,
|
||||
52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29,
|
||||
21, 13, 5, 28, 20, 12, 4,
|
||||
];
|
||||
|
||||
/// Compression Permutation table (56 to 48 bits).
|
||||
pub const PC2_TABLE: [u8; 48] = [
|
||||
14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52,
|
||||
31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32,
|
||||
];
|
||||
|
||||
/// Number of Key Bits Shifted per Round
|
||||
pub const ROUND_ROTATIONS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
|
||||
|
||||
/// Expansion permutation (32 to 48 bits).
|
||||
pub const E_BOX: [u8; 48] = [
|
||||
32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18,
|
||||
19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1,
|
||||
];
|
||||
|
||||
/// P-box permutation (32 bits).
|
||||
pub const P_BOX: [u8; 32] = [
|
||||
16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19,
|
||||
13, 30, 6, 22, 11, 4, 25,
|
||||
];
|
||||
|
||||
/// S-boxes: 8 boxes, each 4x16 (row x col) -> 0-15.
|
||||
pub const S_BOXES: [[[u8; 16]; 4]; 8] = [
|
||||
// S1
|
||||
[
|
||||
[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
|
||||
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
|
||||
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
|
||||
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
|
||||
],
|
||||
// S2
|
||||
[
|
||||
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10],
|
||||
[3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5],
|
||||
[0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15],
|
||||
[13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
|
||||
],
|
||||
// S3
|
||||
[
|
||||
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8],
|
||||
[13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1],
|
||||
[13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7],
|
||||
[1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
|
||||
],
|
||||
// S4
|
||||
[
|
||||
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15],
|
||||
[13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9],
|
||||
[10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4],
|
||||
[3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
|
||||
],
|
||||
// S5
|
||||
[
|
||||
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9],
|
||||
[14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6],
|
||||
[4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14],
|
||||
[11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
|
||||
],
|
||||
// S6
|
||||
[
|
||||
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11],
|
||||
[10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8],
|
||||
[9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6],
|
||||
[4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
|
||||
],
|
||||
// S7
|
||||
[
|
||||
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1],
|
||||
[13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6],
|
||||
[1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2],
|
||||
[6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
|
||||
],
|
||||
// S8
|
||||
[
|
||||
[13, 2, 8, 4, 6, 15, 11, 1, 19, 9, 3, 14, 5, 0, 12, 7],
|
||||
[1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2],
|
||||
[7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8],
|
||||
[2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],
|
||||
],
|
||||
];
|
||||
478
des-lib/src/lib.rs
Normal file
478
des-lib/src/lib.rs
Normal file
@@ -0,0 +1,478 @@
|
||||
mod constants;
|
||||
|
||||
use crate::constants::{E_BOX, IP, PC1_TABLE, PC2_TABLE, P_BOX, ROUND_ROTATIONS, S_BOXES};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Des {
|
||||
pub subkeys: [u64; 16],
|
||||
}
|
||||
|
||||
impl Des {
|
||||
/// Create a new DES instance from a 64-bit key (8 bytes).
|
||||
#[must_use]
|
||||
pub fn new(key: u64) -> Self {
|
||||
let subkeys = generate_subkeys(key);
|
||||
Self { subkeys }
|
||||
}
|
||||
|
||||
/// Encrypt a 64-bit block.
|
||||
#[must_use]
|
||||
pub fn encrypt(&self, block: u64) -> u64 {
|
||||
self.des(block, true)
|
||||
}
|
||||
|
||||
/// Decrypt a 64-bit block.
|
||||
#[must_use]
|
||||
pub fn decrypt(&self, block: u64) -> u64 {
|
||||
self.des(block, false)
|
||||
}
|
||||
|
||||
/// Core DES function: encrypt if forward=true, else decrypt.
|
||||
#[must_use]
|
||||
fn des(&self, block: u64, forward: bool) -> u64 {
|
||||
let permutated_block = ip(block);
|
||||
|
||||
let (left, right) = if forward {
|
||||
process_feistel_rounds(permutated_block, &self.subkeys)
|
||||
} else {
|
||||
let reversed_subkeys = self.subkeys.iter().rev().copied().collect::<Vec<_>>();
|
||||
process_feistel_rounds(permutated_block, &reversed_subkeys)
|
||||
};
|
||||
|
||||
let combined = concatenate_halves(right, left, 64);
|
||||
fp(combined)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reduces 64 bits to 56-bit key by applying PC-1 permutation.
|
||||
/// Selects 56 specific bits (ignoring 8 parity bits) and permutes them.
|
||||
///
|
||||
/// Accounts for DES specification's big-endian bit numbering (1-64, MSB first)
|
||||
/// versus Rust u64's little-endian bit numbering (0-63, LSB first).
|
||||
#[must_use]
|
||||
pub fn pc1(key: u64) -> u64 {
|
||||
permutate(key, 64, 56, &PC1_TABLE)
|
||||
}
|
||||
|
||||
/// Compression permuation
|
||||
/// Reduces 56-bits to 48-bit key
|
||||
#[must_use]
|
||||
pub fn pc2(key: u64) -> u64 {
|
||||
let key_56 = key & 0x00FF_FFFF_FFFF_FFFF;
|
||||
permutate(key_56, 56, 48, &PC2_TABLE)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
const fn split_block(key: u64) -> (u32, u32) {
|
||||
let is_56_bit = (key >> 56) == 0;
|
||||
|
||||
if is_56_bit {
|
||||
let masked = key & 0x00FF_FFFF_FFFF_FFFF;
|
||||
let left = (masked >> 28) & 0x0FFF_FFFF;
|
||||
let right = masked & 0x0FFF_FFFF;
|
||||
return (left as u32, right as u32);
|
||||
}
|
||||
|
||||
let left = (key >> 32) & 0xFFFF_FFFF;
|
||||
let right = key & 0xFFFF_FFFF;
|
||||
(left as u32, right as u32)
|
||||
}
|
||||
|
||||
/// Circulary shifts 28-bit number left by `shift`.
|
||||
#[must_use]
|
||||
const fn shift(key: u32, shift: u8) -> u32 {
|
||||
const MASK: u32 = 0x0FFF_FFFF;
|
||||
let value = key & MASK; // 28-bits
|
||||
|
||||
if shift == 0 {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Circular left shift formula:
|
||||
// (value << shift) gets the main shifted portion
|
||||
// (value >> (28 - shift)) gets the bits that wrapped around
|
||||
let main_shifted = (value << shift) & MASK;
|
||||
let wrapped_bits = (value >> (28 - shift)) & ((1 << shift) - 1);
|
||||
(main_shifted | wrapped_bits) & MASK
|
||||
}
|
||||
|
||||
/// Concatenates two `input_bits`-bit numbers into 2*`input_bits`-bit number
|
||||
#[must_use]
|
||||
fn concatenate_halves(left: u32, right: u32, input_bits: u32) -> u64 {
|
||||
(u64::from(left) << input_bits) | u64::from(right)
|
||||
}
|
||||
|
||||
/// Generate 16 subkeys from the 64-bit key.
|
||||
fn generate_subkeys(key: u64) -> [u64; 16] {
|
||||
let reduced_key = pc1(key); // C_0, D_0
|
||||
let (mut left, mut right) = split_block(reduced_key);
|
||||
|
||||
ROUND_ROTATIONS
|
||||
.iter()
|
||||
.map(|&shift_amount| {
|
||||
left = shift(left, shift_amount); // C_(n-1) -> C_n
|
||||
right = shift(right, shift_amount); // D_(n-1) -> D_n
|
||||
let combined = concatenate_halves(left, right, 28);
|
||||
pc2(combined)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.expect("Exactly 16 subkeys expected")
|
||||
}
|
||||
|
||||
/// Generic bit permutation 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` - 1-based positions (1 to `input_bits`) where each output bit comes from
|
||||
#[must_use]
|
||||
fn permutate(input: u64, input_bits: u32, output_bits: u32, position_table: &[u8]) -> u64 {
|
||||
position_table
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(0, |acc, (idx, &pos)| {
|
||||
// Convert 1-based DES position to 0-based input position (MSB first)
|
||||
let pos_0based = u64::from(pos.saturating_sub(1));
|
||||
let input_bit_pos = u64::from(input_bits)
|
||||
.saturating_sub(1)
|
||||
.saturating_sub(pos_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 = u64::from(output_bits)
|
||||
.saturating_sub(1)
|
||||
.saturating_sub(idx as u64);
|
||||
let shifted_bit = bit_value << output_bit_pos;
|
||||
|
||||
acc | shifted_bit
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn ip(message: u64) -> u64 {
|
||||
permutate(message, 64, 64, &IP)
|
||||
}
|
||||
|
||||
/// Expand the right side of the data from 32 bits to 48.
|
||||
#[must_use]
|
||||
fn expansion_permutation(right: u32) -> u64 {
|
||||
permutate(u64::from(right), 32, 48, &E_BOX)
|
||||
}
|
||||
|
||||
/// Implementation for testing S-boxes in isolation.
|
||||
/// Applies all 8 DES S-boxes to a 48-bit input, returning 32-bit result.
|
||||
#[must_use]
|
||||
fn s_box_substitution(block: u64) -> u32 {
|
||||
S_BOXES.iter().enumerate().fold(0, |acc, (idx, s_box)| {
|
||||
let start_bit = 42 - idx * 6; // 42 = 48 - 6
|
||||
let shift_amount = (7 - idx) * 4;
|
||||
|
||||
let mask = 63 << start_bit; // 63 == 0b11_111
|
||||
let sbox_bits = u8::try_from((block & mask) >> start_bit).expect("8-bit value");
|
||||
|
||||
let row = (sbox_bits >> 5) << 1 | (sbox_bits & 1);
|
||||
let col = (sbox_bits >> 1) & 15; // 15 == 0b1111
|
||||
let sbox_value = s_box[row as usize][col as usize];
|
||||
acc | (u32::from(sbox_value) << shift_amount)
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn p_box_permutation(input: u32) -> u32 {
|
||||
u32::try_from(permutate(u64::from(input), 32, 32, &P_BOX)).expect("32-bit value")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fp(input: u64) -> u64 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Process 16 Feistel rounds for ECB encryption/decryption.
|
||||
#[must_use]
|
||||
fn process_feistel_rounds(initial_block: u64, subkeys: &[u64]) -> (u32, u32) {
|
||||
let (mut left, mut right) = split_block(initial_block);
|
||||
for &subkey in subkeys {
|
||||
(left, right) = feistel(left, right, subkey);
|
||||
}
|
||||
|
||||
(right, left) // left and right should be swapped
|
||||
}
|
||||
/// Feistel function: Expand, XOR with subkey, S-box, permute.
|
||||
/// `R_i` = `L_(i-1)` XOR f(`R_(i-1)`, `K_1`)
|
||||
#[must_use]
|
||||
fn feistel(left: u32, right: u32, subkey: u64) -> (u32, u32) {
|
||||
let function_output = f_function(right, subkey);
|
||||
let new_right = left ^ function_output;
|
||||
// L_i = R_(i-1)
|
||||
let new_left = right;
|
||||
(new_right, new_left)
|
||||
}
|
||||
|
||||
fn f_function(right: u32, subkey: u64) -> u32 {
|
||||
let expanded = expansion_permutation(right);
|
||||
let xored = expanded ^ subkey;
|
||||
let sboxed = s_box_substitution(xored);
|
||||
p_box_permutation(sboxed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::constants::S_BOXES;
|
||||
use claims::{assert_ge, assert_le};
|
||||
use rstest::rstest;
|
||||
|
||||
const TEST_KEY: u64 = 0x1334_5779_9BBC_DFF1;
|
||||
|
||||
const TEST_PLAINTEXT: u64 = 0x0123_4567_89AB_CDEF;
|
||||
const TEST_CIPHERTEXT: u64 = 0x85E8_1354_0F0A_B405;
|
||||
|
||||
const TEST_PC1_RESULT: u64 = 0x00F0_CCAA_F556_678F; // From calculator after PC-1
|
||||
const TEST_COMBINED_KEY: u64 = 0x00F0_CCAA_F556_678F; // From calculator after re-combination
|
||||
const TEST_PC2_RESULT: u64 = 0x0000_CB3D_8B0E_17F5; // From calculator after PC-2
|
||||
|
||||
#[test]
|
||||
fn initial_permutation() {
|
||||
let expected_ip = 0xCC00_CCFF_F0AA_F0AA;
|
||||
let result = ip(TEST_PLAINTEXT);
|
||||
assert_eq!(
|
||||
result, expected_ip,
|
||||
"Initial permulation failed expected 0x{expected_ip:016X}, got 0x{result:016X}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc1_permutaion_correct() {
|
||||
let result = pc1(TEST_KEY);
|
||||
|
||||
assert_eq!(result, TEST_PC1_RESULT, "PC1 permutation failed");
|
||||
assert_ge!(
|
||||
result.leading_zeros(),
|
||||
0,
|
||||
"PC1 result should have leading 8 bits as 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[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_correct(#[case] before: u64, #[case] after: u64) {
|
||||
let result = pc2(before);
|
||||
|
||||
assert_eq!(result, after, "PC2 permutation failed");
|
||||
assert_ge!(
|
||||
result.leading_zeros(),
|
||||
16,
|
||||
"PC2 result should have leading 16 bits as 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_key_56_bits() {
|
||||
let (left, right) = split_block(TEST_PC1_RESULT);
|
||||
|
||||
assert_eq!(left, 0x0F0C_CAAF, "split_key left half mismatch",);
|
||||
assert_eq!(right, 0x0556_678F, "split_key right half mismatch",);
|
||||
|
||||
// Verify 28-bit values have 4 leading zeros in u32
|
||||
assert_ge!(
|
||||
left.leading_zeros(),
|
||||
4,
|
||||
"Left should be 28-bit value in u32"
|
||||
);
|
||||
assert_ge!(
|
||||
right.leading_zeros(),
|
||||
4,
|
||||
"Right should be 28-bit value in u32"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_key_64_bits() {
|
||||
let text = ip(TEST_PLAINTEXT);
|
||||
let (left, right) = split_block(text);
|
||||
|
||||
assert_eq!(left, 0x0CC0_0CCFF, "split_key left half mismatch",);
|
||||
assert_eq!(right, 0x0F0A_AF0AA, "split_key right half mismatch",);
|
||||
}
|
||||
|
||||
#[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 shift_rotation(#[case] key: u32, #[case] expected_output: u32, #[case] shift_amount: u8) {
|
||||
let result = shift(key, shift_amount);
|
||||
assert_eq!(
|
||||
result, expected_output,
|
||||
"shift(0x{key:08X}, {shift_amount}) should equal 0x{expected_output:08X}"
|
||||
);
|
||||
|
||||
// Verify result is still 28 bits
|
||||
assert_eq!(
|
||||
result & 0x0FFF_FFFF,
|
||||
expected_output,
|
||||
"Shift result should preserve 28 bits"
|
||||
);
|
||||
assert_ge!(
|
||||
result.leading_zeros(),
|
||||
4,
|
||||
"Shift result should be 28-bit value in u32"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0x0F0C_CAAF, 0x0556_678F, 0x00F0_CCAA_F556_678F)] // CD_0
|
||||
#[case(0x0E19_955F, 0x0AAC_CF1E, 0x00E1_9955_FAAC_CF1E)] // CD_1
|
||||
#[case(0x0C33_2ABF, 0x0559_9E3D, 0x00C3_32AB_F559_9E3D)] // CD_2
|
||||
#[case(0x00CC_AAFF, 0x0566_78F5, 0x000C_CAAF_F566_78F5)] // CD_3
|
||||
#[case(0x0332_ABFC, 0x0599_E3D5, 0x0033_2ABF_C599_E3D5)] // CD_4
|
||||
#[case(0x0CCA_AFF0, 0x0667_8F55, 0x00CC_AAFF_0667_8F55)] // CD_5
|
||||
#[case(0x032A_BFC3, 0x099E_3D55, 0x0032_ABFC_399E_3D55)] // CD_6
|
||||
#[case(0x0CAA_FF0C, 0x0678_F556, 0x00CA_AFF0_C678_F556)] // CD_7
|
||||
#[case(0x02AB_FC33, 0x09E3_D559, 0x002A_BFC3_39E3_D559)] // CD_8
|
||||
#[case(0x0557_F866, 0x03C7_AAB3, 0x0055_7F86_63C7_AAB3)] // CD_9
|
||||
#[case(0x055F_E199, 0x0F1E_AACC, 0x0055_FE19_9F1E_AACC)] // CD_10
|
||||
#[case(0x057F_8665, 0x0C7A_AB33, 0x0057_F866_5C7A_AB33)] // CD_11
|
||||
#[case(0x05FE_1995, 0x01EA_ACCF, 0x005F_E199_51EA_ACCF)] // CD_12
|
||||
#[case(0x07F8_6655, 0x07AA_B33C, 0x007F_8665_57AA_B33C)] // CD_13
|
||||
#[case(0x0FE1_9955, 0x0EAA_CCF1, 0x00FE_1995_5EAA_CCF1)] // CD_14
|
||||
#[case(0x0F86_6557, 0x0AAB_33C7, 0x00F8_6655_7AAB_33C7)] // CD_15
|
||||
#[case(0x0F0C_CAAF, 0x0556_678F, 0x00F0_CCAA_F556_678F)] // CD_16
|
||||
fn concatenation(#[case] left: u32, #[case] right: u32, #[case] expected: u64) {
|
||||
let result = concatenate_halves(left, right, 28);
|
||||
|
||||
assert_eq!(
|
||||
result, expected,
|
||||
"0x{left:08X} and 0x{right:08X} concatination failed, expected {expected:016X}, got {result:016X}"
|
||||
);
|
||||
|
||||
// Verify correct bit layout
|
||||
assert_eq!(
|
||||
(result >> 28) & 0x0FFF_FFFF_FFFF,
|
||||
left as u64,
|
||||
"High 28 bits should be left"
|
||||
);
|
||||
assert_eq!(
|
||||
result & 0x0FFF_FFFF,
|
||||
right as u64,
|
||||
"Low 28 bits should be right"
|
||||
);
|
||||
assert_eq!(result >> 56, 0, "Combined should fit in 56 bits");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0xF0AA_F0AA, 0x7A15_557A_1555)] // Round 1
|
||||
#[case(0xEF4A_6544, 0x75EA_5430_AA09)] // Round 2
|
||||
#[case(0xCC01_7709, 0xE580_02BA_E853)] // Round 3
|
||||
#[case(0xA25C_0BF4, 0x5042_F805_7FA9)] // Round 4
|
||||
#[case(0x7722_0045, 0xBAE9_0400_020A)] // Round 5
|
||||
#[case(0x8A4F_A637, 0xC542_5FD0_C1AF)] // Round 6
|
||||
#[case(0xE967_CD69, 0xF52B_0FE5_AB53)] // Round 7
|
||||
#[case(0x064A_BA10, 0x00C2_555F_40A0)] // Round 8
|
||||
#[case(0xD569_4B90, 0x6AAB_52A5_7CA1)] // Round 9
|
||||
#[case(0x247C_C67A, 0x1083_F960_C3F4)] // Round 10
|
||||
#[case(0xB7D5_D7B2, 0x5AFE_ABEA_FDA5)] // Round 11
|
||||
#[case(0xC578_3C78, 0x60AB_F01F_83F1)] // Round 12
|
||||
#[case(0x75BD_1858, 0x3ABD_FA8F_02F0)] // Round 13
|
||||
#[case(0x18C3_155A, 0x0F16_068A_AAF4)] // Round 14
|
||||
#[case(0xC28C_960D, 0xE054_594A_C05B)] // Round 15
|
||||
#[case(0x4342_3234, 0x206A_041A_41A8)] // Round 16
|
||||
fn permutation_expansion(#[case] block: u32, #[case] expected: u64) {
|
||||
let expanded = expansion_permutation(block);
|
||||
|
||||
assert_eq!(expanded, expected);
|
||||
assert_eq!(expanded >> 48, 0, "Expansion exceeds 48 bits");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0x6117_BA86_6527, 0x5C82_B597)] // Round 1
|
||||
#[case(0x0C44_8DEB_63EC, 0xF8D0_3AAE)] // Round 2
|
||||
#[case(0xB07C_88F8_27CA, 0x2710_E16F)] // Round 3
|
||||
#[case(0x22EF_2EDE_4AB4, 0x21ED_9F3A)] // Round 4
|
||||
#[case(0xC605_03EB_51A2, 0x50C8_31EB)] // Round 5
|
||||
#[case(0xA6E7_6180_BA80, 0x41F3_4C3D)] // Round 6
|
||||
#[case(0x19AF_B813_B3EF, 0x1075_40AD)] // Round 7
|
||||
#[case(0xF748_6F9E_7B5B, 0x6C18_7CAE)] // Round 8
|
||||
#[case(0x8A70_B948_9B20, 0x110C_5777)] // Round 9
|
||||
#[case(0xA170_BEDA_85BB, 0xDA04_5275)] // Round 10
|
||||
#[case(0x7BA1_7834_2E23, 0x7305_D101)] // Round 11
|
||||
#[case(0x15DA_058B_E418, 0x7B8B_2635)] // Round 12
|
||||
#[case(0xAD78_2B75_B8B1, 0x9AD1_8B4F)] // Round 13
|
||||
#[case(0x5055_B178_4DCE, 0x6479_9AF1)] // Round 14
|
||||
#[case(0x5FC5_D477_FF51, 0xB2E8_8D3C)] // Round 15
|
||||
#[case(0xEB57_8F14_565D, 0xA783_2429)] // Round 16
|
||||
fn sbox_subsitution(#[case] block: u64, #[case] output: u32) {
|
||||
let result = s_box_substitution(block);
|
||||
assert_eq!(result, output, "Expected {output:08X}, got {result:08X}");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(0x5C82_B597, 0x234A_A9BB)] // Round 1
|
||||
#[case(0xF8D0_3AAE, 0x3CAB_87A3)] // Round 2
|
||||
#[case(0x2710_E16F, 0x4D16_6EB0)] // Round 3
|
||||
#[case(0x21ED_9F3A, 0xBB23_774C)] // Round 4
|
||||
#[case(0x50C8_31EB, 0x2813_ADC3)] // Round 5
|
||||
#[case(0x41F3_4C3D, 0x9E45_CD2C)] // Round 6
|
||||
#[case(0x1075_40AD, 0x8C05_1C27)] // Round 7
|
||||
#[case(0x6C18_7CAE, 0x3C0E_86F9)] // Round 8
|
||||
#[case(0x110C_5777, 0x2236_7C6A)] // Round 9
|
||||
#[case(0xDA04_5275, 0x62BC_9C22)] // Round 10
|
||||
#[case(0x7305_D101, 0xE104_FA02)] // Round 11
|
||||
#[case(0x7B8B_2635, 0xC268_CFEA)] // Round 12
|
||||
#[case(0x9AD1_8B4F, 0xDDBB_2922)] // Round 13
|
||||
#[case(0x6479_9AF1, 0xB731_8E55)] // Round 14
|
||||
#[case(0xB2E8_8D3C, 0x5B81_276E)] // Round 15
|
||||
#[case(0xA783_2429, 0xC8C0_4F98)] // Round 16
|
||||
fn permuation_pbox(#[case] block: u32, #[case] output: u32) {
|
||||
let result = p_box_permutation(block);
|
||||
assert_eq!(result, output, "Expected {output:08X}, got {result:08X}");
|
||||
}
|
||||
}
|
||||
167
des-lib/tests/des.rs
Normal file
167
des-lib/tests/des.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use rand::random;
|
||||
use std::time::Instant;
|
||||
|
||||
use des::Des;
|
||||
|
||||
const TEST_KEY: u64 = 0x1334_5779_9BBC_DFF1;
|
||||
const TEST_PLAINTEXT: u64 = 0x0123_4567_89AB_CDEF;
|
||||
const TEST_CIPHERTEXT: u64 = 0x85E8_1354_0F0A_B405;
|
||||
|
||||
/// Helper to create a test Des instance (use your actual key schedule)
|
||||
fn des_instance() -> Des {
|
||||
Des::new(TEST_KEY)
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn test_ecb_mode_equivalence() {
|
||||
// If you implement ECB mode, test it matches single block
|
||||
let key = 0x1334_5779_9BBC_DFF1;
|
||||
let des = Des::new(key);
|
||||
let plain = 0x0123_4567_89AB_CDEF;
|
||||
|
||||
let _single_block = des.encrypt(plain);
|
||||
// let ecb_result = encrypt_ecb(&[plain]);
|
||||
// assert_eq!(single_block, ecb_result[0]);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn test_with_real_data() {
|
||||
// Test with actual 8-byte data
|
||||
let key_bytes = b"KGenius\x01";
|
||||
let key = u64::from_le_bytes(*key_bytes);
|
||||
|
||||
let data_bytes = b"HelloDES!";
|
||||
let mut padded = [0u8; 8];
|
||||
padded[..data_bytes.len()].copy_from_slice(data_bytes);
|
||||
let plaintext = u64::from_le_bytes(padded);
|
||||
|
||||
let des = Des::new(key);
|
||||
let encrypted = des.encrypt(plaintext);
|
||||
|
||||
// Verify we can roundtrip
|
||||
let decrypted = des.decrypt(encrypted);
|
||||
let decrypted_bytes = decrypted.to_le_bytes();
|
||||
assert_eq!(decrypted_bytes[..data_bytes.len()], *data_bytes);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn all_zero_paintext() {
|
||||
let des = des_instance();
|
||||
|
||||
let plain = 0;
|
||||
let encrypted = des.encrypt(plain);
|
||||
let decrypted = des.decrypt(encrypted);
|
||||
assert_eq!(decrypted, plain, "All-zero plaintext failed");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
#[should_panic(expected = "Invalid key size")]
|
||||
fn invalid_key_size() {
|
||||
let _ = Des::new(0);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn encrypt_decrypt_roundtrip() {
|
||||
let des = des_instance();
|
||||
let plaintext = TEST_PLAINTEXT;
|
||||
let ciphertext = des.encrypt(plaintext);
|
||||
let dectrypted = des.decrypt(plaintext);
|
||||
let re_ciphertext = des.encrypt(dectrypted);
|
||||
|
||||
assert_eq!(ciphertext, TEST_CIPHERTEXT, "Encyption failed");
|
||||
assert_eq!(re_ciphertext, TEST_CIPHERTEXT, "Re-Encyption failed");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn weak_keys_rejected() {
|
||||
let weak_keys = [0x0101010101010101, 0xFEFEFEFEFEFEFEFE, 0xE001E001E001E001];
|
||||
|
||||
for key in weak_keys {
|
||||
let des = Des::new(key);
|
||||
let plaintext = TEST_PLAINTEXT;
|
||||
let encrypted = des.encrypt(plaintext);
|
||||
let dectrypted = des.decrypt(encrypted);
|
||||
assert_eq!(dectrypted, plaintext, "Weak key {key} failed roundtrip");
|
||||
}
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn multiple_blocks() {
|
||||
let des = des_instance();
|
||||
let blocks = [
|
||||
(0x0123456789ABCDEFu64, 0x85E813540F0AB405u64),
|
||||
(0xFEDCBA9876543210u64, 0xC08BF0FF627D3E6Fu64), // Another test vector
|
||||
(0x0000000000000000u64, 0x474D5E3B6F8A07F8u64), // Zero block
|
||||
];
|
||||
for (plaintext, expected) in blocks {
|
||||
let encrypted = des.encrypt(plaintext);
|
||||
assert_eq!(encrypted, expected, "Failed on plaintext: {plaintext:016X}");
|
||||
|
||||
let dectrypted = des.decrypt(encrypted);
|
||||
assert_eq!(dectrypted, plaintext, "Roundtrip failed on block");
|
||||
}
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn all_one_paintext() {
|
||||
let des = des_instance();
|
||||
|
||||
let plain = 1;
|
||||
let encrypted = des.encrypt(plain);
|
||||
let decrypted = des.decrypt(encrypted);
|
||||
assert_eq!(decrypted, plain, "All-one plaintext failed");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn different_inputs() {
|
||||
let des = des_instance();
|
||||
|
||||
let plain1 = 0x0000000000000001;
|
||||
let plain2 = 0x0000000000000002;
|
||||
let enc1 = des.encrypt(plain1);
|
||||
let enc2 = des.encrypt(plain2);
|
||||
assert_ne!(
|
||||
enc1, enc2,
|
||||
"Encryption not deterministic for different inputs"
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn performance() {
|
||||
let des = des_instance();
|
||||
let plaintext = TEST_PLAINTEXT;
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..10000 {
|
||||
let _ = des.encrypt(plaintext);
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!("10k encryption took: {duration:?}");
|
||||
// Reasonable benchmark: should be under 1ms on modern hardware
|
||||
assert!(duration.as_millis() < 100, "Performance degraded");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn fuzz_properties() {
|
||||
let des = des_instance();
|
||||
|
||||
for _ in 0..100 {
|
||||
let plaintext = random();
|
||||
let encrypted = des.encrypt(plaintext);
|
||||
let decrypted = des.decrypt(plaintext);
|
||||
|
||||
assert_eq!(decrypted, encrypted, "Fuzz roundtrip failed");
|
||||
assert_ne!(encrypted, plaintext, "Encryption is identity function");
|
||||
|
||||
let key2 = random();
|
||||
if key2 != TEST_KEY {
|
||||
let des2 = Des::new(key2);
|
||||
let encrypted2 = des2.encrypt(plaintext);
|
||||
assert_ne!(
|
||||
encrypted, encrypted2,
|
||||
"Different keys produced same encryption"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
des-lib/tests/key_schedule.rs
Normal file
91
des-lib/tests/key_schedule.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use des::Des;
|
||||
|
||||
// Full expected subkeys for TEST_KEY (48 bits each, from FIPS spec)
|
||||
const EXPECTED_SUBKEYS: [u64; 16] = [
|
||||
0x1B02_EFFC_7072,
|
||||
0x79AE_D9DB_C9E5,
|
||||
0x55FC_8A42_CF99,
|
||||
0x72AD_D6DB_351D,
|
||||
0x7CEC_07EB_53A8,
|
||||
0x63A5_3E50_7B2F,
|
||||
0xEC84_B7F6_18BC,
|
||||
0xF78A_3AC1_3BFB,
|
||||
0xE0DB_EBED_E781,
|
||||
0xB1F3_47BA_464F,
|
||||
0x215F_D3DE_D386,
|
||||
0x7571_F594_67E9,
|
||||
0x97C5_D1FA_BA41,
|
||||
0x5F43_B7F2_E73A,
|
||||
0xBF91_8D3D_3F0A,
|
||||
0xCB3D_8B0E_17F5,
|
||||
];
|
||||
|
||||
const TEST_KEY: u64 = 0x1334_5779_9BBC_DFF1;
|
||||
|
||||
#[test]
|
||||
fn key_schedule_generates_correct_subkeys() {
|
||||
const EXPECTED_SUBKEYS: [u64; 16] = [
|
||||
0x1B02_EFFC_7072,
|
||||
0x79AE_D9DB_C9E5,
|
||||
0x55FC_8A42_CF99,
|
||||
0x72AD_D6DB_351D,
|
||||
0x7CEC_07EB_53A8,
|
||||
0x63A5_3E50_7B2F,
|
||||
0xEC84_B7F6_18BC,
|
||||
0xF78A_3AC1_3BFB,
|
||||
0xE0DB_EBED_E781,
|
||||
0xB1F3_47BA_464F,
|
||||
0x215F_D3DE_D386,
|
||||
0x7571_F594_67E9,
|
||||
0x97C5_D1FA_BA41,
|
||||
0x5F43_B7F2_E73A,
|
||||
0xBF91_8D3D_3F0A,
|
||||
0xCB3D_8B0E_17F5,
|
||||
];
|
||||
|
||||
let des = Des::new(TEST_KEY);
|
||||
|
||||
assert_eq!(
|
||||
des.subkeys, EXPECTED_SUBKEYS,
|
||||
"Subkey generation failed. Expected: {EXPECTED_SUBKEYS:?}, Got: {:?}",
|
||||
des.subkeys
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn test_rotation_shifts() {
|
||||
// Test the left rotation logic in key schedule
|
||||
let mut c: u32 = 0x0FFFFFFF; // 28 bits all 1s
|
||||
c = c.rotate_left(1);
|
||||
assert_eq!(c, 0x1FFFFFFF >> 4, "Single bit rotation failed");
|
||||
|
||||
// Test double shift
|
||||
let mut d: u32 = 0xAAAAAAA; // 101010... pattern
|
||||
d = d.rotate_left(2);
|
||||
assert_eq!(d, 0x2AAAAAA, "Double rotation failed"); // Check pattern shift
|
||||
}
|
||||
|
||||
// #[test]
|
||||
fn test_weak_key_detection() {
|
||||
let weak_keys = [
|
||||
0x0101010101010101u64, // All odd parity
|
||||
0xFEFEFEFEFEFEFEFEu64, // All even parity
|
||||
0x1F1F1F1F0E0E0E0Eu64, // Semi-weak
|
||||
];
|
||||
|
||||
for key in weak_keys {
|
||||
let des = Des::new(key);
|
||||
// Weak keys often produce subkeys that don't vary much
|
||||
let subkeys = &des.subkeys;
|
||||
let first = subkeys[0];
|
||||
let last = subkeys[15];
|
||||
// For true weak keys, many subkeys may be identical
|
||||
// This is just a basic check - implement full weak key analysis if desired
|
||||
println!(
|
||||
"Weak key {} subkeys: first={:012X}, last={:012X}",
|
||||
key,
|
||||
first & 0xFFFFFFFFFFFF,
|
||||
last & 0xFFFFFFFFFFFF
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user