From ad4a888af83987b79dd5dfa06232b257d2937292 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 29 Sep 2025 15:36:44 +0300 Subject: [PATCH] Initial commit --- .gitignore | 19 +++ Cargo.lock | 164 +++++++++++++++++++ Cargo.toml | 16 ++ src/lib.rs | 371 ++++++++++++++++++++++++++++++++++++++++++ tests/des.rs | 33 ++++ tests/key_schedule.rs | 76 +++++++++ 6 files changed, 679 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 tests/des.rs create mode 100644 tests/key_schedule.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1efa08b --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +#--------------------------------------------------# +# The following was generated with gitignore.nvim: # +#--------------------------------------------------# +# Gitignore for the following technologies: Rust + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..832fbd2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "claims" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" + +[[package]] +name = "des" +version = "0.1.0" +dependencies = [ + "claims", + "rand", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bf17d07 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "des" +version = "0.1.0" +authors = ["Kristofers Solo "] +edition = "2024" + +[dependencies] + +[dev-dependencies] +claims = "0.8" +rand = "0.9" + +[lints.clippy] +pedantic = "warn" +nursery = "warn" +unwrap_used = "warn" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..97b192e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,371 @@ +#[derive(Debug)] +pub struct DES { + pub subkeys: [u64; 16], + _s_boxes: Vec>>, +} + +impl DES { + #[must_use] + pub fn new(key: u64) -> Self { + let mut des = Self { + subkeys: [0; 16], + _s_boxes: Vec::new(), + }; + des.generate_subkeys(key); + des + } + + #[must_use] + pub fn encrypt(&self, _plaintext: u64) -> u64 { + todo!() + } + + #[must_use] + pub fn decrypt(&self, _plaintext: u64) -> u64 { + todo!() + } + + #[must_use] + pub fn ip(&self, _input: u64) -> u64 { + todo!() + } + + #[must_use] + pub fn pc1(&self, _key: u64) -> u64 { + todo!() + } + + #[must_use] + pub fn expand(&self, _right_half: u32) -> u64 { + todo!() + } + + #[must_use] + pub fn permutate_output(&self, _input: u32) -> u32 { + todo!() + } + + #[must_use] + pub fn feistel(&self, _right: u32, _subkey: u64) -> u32 { + todo!() + } + + fn generate_subkeys(&mut self, _key: u64) { + todo!() + } +} + +/// Encrypts data using ECB mode. +/// +/// # Arguments +/// - `data` - Plaintext bytes (must be multiple of 8 for ECB) +/// - `key` - 8-byte DES key +/// +/// # Returns +/// +/// Ciphertext as Vec, same length as input +/// +/// # Panics +/// +/// If data length is not multiple of 8 bytes +#[must_use] +pub fn encrypt_ecb(_data: &[u8], _key: &[u8; 8]) -> Vec { + todo!() +} + +/// Decrypts ECB-encrypted data. +/// +/// # Arguments +/// - `data` - Plaintext bytes (must be multiple of 8 for ECB) +/// - `key` - 8-byte DES key +/// +/// # Returns +/// +/// Ciphertext as Vec, same length as input +/// +/// # Panics +/// +/// If data length is not multiple of 8 bytes +#[must_use] +pub fn decrypt_ecb(_data: &[u8], _key: &[u8; 8]) -> Vec { + todo!() +} + +#[cfg(test)] +mod tests { + use super::*; + use claims::assert_le; + use rand::random; + use std::time::Instant; + + const TEST_KEY: u64 = 0x133457799BBCDFF1; + const RIGHT_KEY: u32 = 0x12345678; + const TEST_PLAINTEXT: u64 = 0x0123456789ABCDEF; + const TEST_CIPHERTEXT: u64 = 0x85E813540F0AB405; + + impl DES { + fn apply_sboxes(&self, _input: u64) -> u32 { + // Implementation for testing S-boxes in isolation + // Return 32-bit result after 8 S-boxes + todo!() + } + } + + /// Helper to create a test Des instance (use your actual key schedule) + fn des_instance() -> DES { + DES::new(TEST_KEY) + } + + #[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 key_schedule_generates_correct_subkeys() { + let expected_subkeys = [ + 0xF3FDFBF373848CF5u64, + 0xF3738CF548C4F3F5u64, + 0x848C4F3F5F373848u64, + ]; + + let des = des_instance(); + let generated = des.subkeys; + + for (idx, &expected) in expected_subkeys.iter().enumerate() { + let masked_gen = generated[idx]; + let masked_exp = expected; + assert_eq!( + masked_gen, masked_exp, + "Subkey {idx} mismatch: expected {masked_exp:012X}, got {masked_gen:012X}" + ); + } + } + + #[test] + fn initial_permutation() { + let input = TEST_KEY; + let expected_ip = 0xC2B093C7A3A7C24A; + let result = des_instance().ip(input); + assert_eq!(result, expected_ip, "Initial permulation failed"); + } + + #[test] + fn pc1_permutaion_correct() { + let des = des_instance(); + let key = TEST_KEY; + let expected_pc1 = 0x0A2B3C4D5E6F789A; // Truncated 56 bits from spec + let result = des.pc1(key); + let masked_result = result & 0x00FF_FFFF_FFFF_FFFF; // 56 bits + let masked_expected = expected_pc1 & 0x00FF_FFFF_FFFF_FFFF; + assert_eq!(masked_result, masked_expected, "PC1 permutation failed"); + } + + #[test] + fn expansion_permutation() { + let des = des_instance(); + let right_half = RIGHT_KEY; + let expanded = des.expand(right_half); + + // Expansion should produce 48 bits from 32 + assert_eq!(expanded >> 48, 0, "Expandsion exceeds 48 bits"); + + // Test that expansion duplicates bits correctly + // Bit 0 of expanded should match bit 31 of input (EXPANSION[0]=32) + assert_eq!( + (expanded >> 47) & 1, + ((right_half as u64) >> 31) & 1, + "Expansion bit 0 failed" + ); + // Bit 1 should match bit 0 (EXPANSION[1]=1) + assert_eq!( + (expanded >> 46) & 1, + (right_half as u64) & 1, + "Expansion bit 1 failed" + ); + // Test wraparound: bit 47 should match bit 0 again (EXPANSION[47]=1) + assert_eq!( + expanded & 1, + (right_half as u64) & 1, + "Expansion wraparound failed" + ); + } + + #[test] + fn sbox_subsitution() { + let des = des_instance(); + let sbox_tests = [ + // (box_idx, 6-bit input, expected 4-bit output) + (0, 0b000000, 14), // S1: 00 0000 -> row 0, col 0 -> 14 + (0, 0b011111, 9), // S1: 01 1111 -> row 1, col 15 -> 9 + (1, 0b100000, 0), // S2: 10 0000 -> row 2, col 0 -> 0 + (2, 0b001010, 2), // S3: 00 1010 -> row 0, col 10 -> 2 + ]; + + for (box_idx, input, expected) in sbox_tests { + let row = (input & 1) | ((input >> 4) & 0x2); + let col = (input >> 1) & 0xF; + let val = des._s_boxes[box_idx][row as usize][col as usize]; + + assert_eq!( + val, + expected as u8, + "S{} failed: input {input:06b} (row {row}, col {col}) expected {expected}, got {val}", + box_idx + 1 + ); + } + } + + #[test] + fn permuation_pbox() { + let des = des_instance(); + let input = RIGHT_KEY; + let result = des.permutate_output(input); + + // P-box should preserve all bits (32 in, 32 out), just reorder + let bit_count = input.count_ones(); + let result_bit_count = result.count_ones(); + assert_eq!(bit_count, result_bit_count, "P-box changes bit count"); + + // Test specific bit mapping: PERMUTATION[0]=16 means bit 15 (0-based) of output = bit 15 of input + let input_bit_15 = (input >> 15) & 1; + let output_bit_0 = (result >> 31) & 1; // MSB first + assert_eq!(input_bit_15, output_bit_0, "P-box bit mapping failed"); + } + + #[test] + fn feistel_function_properties() { + let des = des_instance(); + let right = RIGHT_KEY; + let subkey = 0xFEDCBA9876543210 & 0xFFFF_FFFF_FFFF; + + let feistel_result = des.feistel(right, subkey); + + // Feistel output should always be 32 bits + assert_le!(feistel_result, u32::MAX, "Feistel output exceeds 32 bits"); + + // Test that zero subkey produces deterministic result + let zero_subkey_result = des.feistel(right, 0); + let zero_expanded = des.expand(right); + let sbox_result = des.apply_sboxes(zero_expanded); + let expected = des.permutate_output(sbox_result as u32); + assert_eq!(zero_subkey_result, expected, "Feistel with zero key failed"); + } + + #[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] + 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] + #[should_panic(expected = "Invalid key size")] + fn invalid_key_size() { + DES::new(0); + } + + #[test] + fn performance() { + let des = des_instance(); + let plaintext = TEST_PLAINTEXT; + + let start = Instant::now(); + for _ in 0..10000 { + 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" + ); + } + } + } +} diff --git a/tests/des.rs b/tests/des.rs new file mode 100644 index 0000000..0be8699 --- /dev/null +++ b/tests/des.rs @@ -0,0 +1,33 @@ +use des::DES; + +#[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); +} diff --git a/tests/key_schedule.rs b/tests/key_schedule.rs new file mode 100644 index 0000000..00c11ea --- /dev/null +++ b/tests/key_schedule.rs @@ -0,0 +1,76 @@ +use des::DES; + +// Full expected subkeys for TEST_KEY (48 bits each, from FIPS spec) +const EXPECTED_SUBKEYS: [u64; 16] = [ + 0xF3FDFBF373848CF5u64, + 0xF3738CF548C4F3F5u64, + 0x848C4F3F5F373848u64, + 0xC4F3F5F373848CCFu64, + 0xF3F5F373848CCF39u64, + 0x5F373848CCF39A7Au64, + 0x373848CCF39A7A29u64, + 0x848CCF39A7A29D6Bu64, + 0xCCF39A7A29D6B3E6u64, + 0xF39A7A29D6B3E674u64, + 0x9A7A29D6B3E674F1u64, + 0x7A29D6B3E674F1D3u64, + 0x29D6B3E674F1D39Bu64, + 0xD6B3E674F1D39BFAu64, + 0xB3E674F1D39BFACFu64, + 0xE674F1D39BFACF3Fu64, +]; + +const TEST_KEY: u64 = 0x133457799BBCDFF1; + +#[test] +fn test_full_key_schedule() { + let des = DES::new(TEST_KEY); + + for (i, &expected) in EXPECTED_SUBKEYS.iter().enumerate() { + let masked_gen = des.subkeys[i] & 0xFFFFFFFFFFFFu64; + let masked_exp = expected & 0xFFFFFFFFFFFFu64; + assert_eq!( + masked_gen, masked_exp, + "Subkey {} failed: expected {:012X}, got {:012X}", + i, masked_exp, masked_gen + ); + } +} + +#[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 + ); + } +}