Skip to main content

arcana/cipher/
des.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! DES (FIPS 46-3) and Triple-DES (3DES / TDEA) block cipher
5//! implementations.
6//!
7//! DES operates on 64-bit blocks with a 56-bit effective key
8//! (stored as 64 bits with parity). Triple-DES uses three DES keys
9//! in EDE (Encrypt-Decrypt-Encrypt) mode.
10//!
11//! # ⚠ Side-channel posture (legacy, no hardening planned)
12//!
13//! Both DES and Triple-DES use **table-based S-boxes** with the
14//! same cache-timing leakage surface as the AES table-based
15//! implementation (cf. [`super::aes`]). There is **no hardening
16//! planned** — these primitives ship for legacy interop only and
17//! should be avoided on SCA-sensitive targets. Prefer AES (and
18//! once roadmap item `T1-A` lands, a fixsliced AES) instead.
19//!
20//! # Security note
21//!
22//! DES is considered insecure due to its 56-bit key length and
23//! should not be used for new applications. 3DES is deprecated by
24//! NIST as of 2023. Use AES instead.
25
26use crate::BlockCipher;
27
28// ============================================================
29// DES permutation and S-box tables
30// ============================================================
31
32/// Initial Permutation (IP), 1-indexed.
33const IP: [u8; 64] = [
34    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,
35    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,
36    39, 31, 23, 15, 7,
37];
38
39/// Final Permutation (IP^-1), 1-indexed.
40const FP: [u8; 64] = [
41    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,
42    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,
43    49, 17, 57, 25,
44];
45
46/// Expansion permutation E (32 -> 48 bits), 1-indexed.
47const E_PERM: [u8; 48] = [
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,
49    22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1,
50];
51
52/// Permutation P (after S-box output), 1-indexed.
53const P_PERM: [u8; 32] = [
54    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,
55    25,
56];
57
58/// Permuted Choice 1 (PC-1): select 56 bits from 64-bit key, 1-indexed.
59const PC1: [u8; 56] = [
60    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,
61    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,
62];
63
64/// Permuted Choice 2 (PC-2): select 48 bits from 56-bit key state, 1-indexed.
65const PC2: [u8; 48] = [
66    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,
67    40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32,
68];
69
70/// Number of left shifts per round in key schedule.
71const KEY_SHIFTS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
72
73/// DES S-boxes (8 boxes, each 4x16 = 64 entries, 6 bits in -> 4 bits out).
74const SBOXES: [[u8; 64]; 8] = [
75    // S1
76    [
77        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,
78        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,
79    ],
80    // S2
81    [
82        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,
83        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,
84    ],
85    // S3
86    [
87        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,
88        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,
89    ],
90    // S4
91    [
92        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,
93        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,
94    ],
95    // S5
96    [
97        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,
98        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,
99    ],
100    // S6
101    [
102        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,
103        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,
104    ],
105    // S7
106    [
107        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,
108        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,
109    ],
110    // S8
111    [
112        13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7,
113        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,
114    ],
115];
116
117// ============================================================
118// u64-based helpers for DES
119// ============================================================
120
121/// Convert 8 bytes (big-endian) to u64.
122#[inline]
123fn bytes_to_u64(b: &[u8]) -> u64 {
124    ((b[0] as u64) << 56)
125        | ((b[1] as u64) << 48)
126        | ((b[2] as u64) << 40)
127        | ((b[3] as u64) << 32)
128        | ((b[4] as u64) << 24)
129        | ((b[5] as u64) << 16)
130        | ((b[6] as u64) << 8)
131        | (b[7] as u64)
132}
133
134/// Convert u64 to 8 bytes (big-endian).
135#[inline]
136fn u64_to_bytes(v: u64) -> [u8; 8] {
137    v.to_be_bytes()
138}
139
140/// Get bit `pos` (1-indexed, MSB=1) from a u64.
141#[inline]
142fn get_bit64(val: u64, pos: u8) -> u64 {
143    (val >> (64 - pos as u32)) & 1
144}
145
146/// Apply a permutation table on a u64.
147/// `in_width` is the number of significant bits in the input (counted from MSB).
148/// The table entries are 1-indexed positions in the input.
149/// Output bits are placed starting from the MSB of the returned u64.
150fn permute64(input: u64, table: &[u8]) -> u64 {
151    let mut output: u64 = 0;
152    for (i, &pos) in table.iter().enumerate() {
153        let bit = get_bit64(input, pos);
154        output |= bit << (63 - i as u32);
155    }
156    output
157}
158
159// ============================================================
160// DES core
161// ============================================================
162
163/// DES block cipher (64-bit block, 56-bit effective key).
164pub struct Des {
165    /// 16 round subkeys, each 48 bits (stored right-aligned in u64, but
166    /// we keep them in the high 48 bits for consistency).
167    subkeys: [u64; 16],
168}
169
170impl Des {
171    /// Generate the 16 round subkeys from a 64-bit key.
172    fn generate_subkeys(key: u64) -> [u64; 16] {
173        // Apply PC-1 to get 56 bits in the high 56 bits of a u64
174        let pc1 = permute64(key, &PC1);
175
176        // Split into C (high 28 bits) and D (next 28 bits)
177        let mut c: u32 = (pc1 >> 36) as u32; // top 28 bits
178        let mut d: u32 = ((pc1 >> 8) as u32) & 0x0FFFFFFF; // next 28 bits
179
180        let mut subkeys = [0u64; 16];
181
182        for round in 0..16 {
183            let shift = KEY_SHIFTS[round] as u32;
184            // Left-rotate C and D by `shift` positions within 28 bits
185            c = ((c << shift) | (c >> (28 - shift))) & 0x0FFFFFFF;
186            d = ((d << shift) | (d >> (28 - shift))) & 0x0FFFFFFF;
187
188            // Concatenate C and D into 56 bits in the high part of a u64
189            let cd: u64 = ((c as u64) << 36) | ((d as u64) << 8);
190
191            // Apply PC-2 to get 48-bit subkey in high 48 bits
192            subkeys[round] = permute64(cd, &PC2);
193        }
194
195        subkeys
196    }
197
198    /// The DES Feistel function f(R, K).
199    /// R is 32 bits in the high 32 bits of a u64.
200    /// K is 48 bits in the high 48 bits of a u64.
201    /// Returns 32 bits in the high 32 bits of a u64.
202    fn feistel(r: u32, subkey: u64) -> u32 {
203        // Expand R from 32 to 48 bits using E permutation
204        // R is in the high 32 bits... actually let's put R into a u64 in high bits
205        let r64 = (r as u64) << 32;
206        let expanded = permute64(r64, &E_PERM); // 48 bits in high 48 bits
207
208        // XOR with subkey
209        let xored = expanded ^ subkey;
210
211        // Apply 8 S-boxes: extract 6 bits at a time from the high 48 bits
212        let mut sbox_result: u32 = 0;
213        for i in 0..8 {
214            // Extract 6 bits starting at bit position (16 + i*6) from the right
215            // or equivalently, bits [63 - i*6 .. 63 - i*6 - 5] from MSB numbering
216            let shift = 58 - (i * 6); // 58, 52, 46, 40, 34, 28, 22, 16
217            let six_bits = ((xored >> shift) & 0x3F) as u8;
218
219            // Row = outer two bits (bit5, bit0), column = inner four bits (bit4..bit1)
220            let row = ((six_bits & 0x20) >> 4) | (six_bits & 0x01); // bit5 << 1 | bit0
221            let col = (six_bits >> 1) & 0x0F;
222            let val = SBOXES[i as usize][(row as usize) * 16 + (col as usize)];
223
224            // Pack into sbox_result (high 32 bits pattern: 4 bits per S-box)
225            sbox_result |= (val as u32) << (28 - i * 4);
226        }
227
228        // Apply P permutation on the 32-bit S-box output
229        let sbox64 = (sbox_result as u64) << 32;
230        let p_out = permute64(sbox64, &P_PERM);
231        (p_out >> 32) as u32
232    }
233
234    /// Core DES cipher (encrypt or decrypt based on subkey order).
235    fn des_cipher(&self, input: u64, decrypt: bool) -> u64 {
236        // Initial permutation
237        let permuted = permute64(input, &IP);
238
239        let mut l: u32 = (permuted >> 32) as u32;
240        let mut r: u32 = permuted as u32;
241
242        // 16 Feistel rounds
243        for round in 0..16 {
244            let subkey_idx = if decrypt { 15 - round } else { round };
245            let f_out = Self::feistel(r, self.subkeys[subkey_idx]);
246
247            let new_r = l ^ f_out;
248            l = r;
249            r = new_r;
250        }
251
252        // Combine R || L (note the 32-bit swap) and apply final permutation
253        let pre_fp: u64 = ((r as u64) << 32) | (l as u64);
254        permute64(pre_fp, &FP)
255    }
256}
257
258impl BlockCipher for Des {
259    const BLOCK_LEN: usize = 8;
260    const KEY_LENS: &'static [usize] = &[8]; // 64 bits (56 effective + 8 parity)
261
262    fn new(key: &[u8]) -> Self {
263        assert_eq!(key.len(), 8, "DES requires an 8-byte key");
264        Des {
265            subkeys: Self::generate_subkeys(bytes_to_u64(key)),
266        }
267    }
268
269    fn encrypt_block(&self, block: &mut [u8]) {
270        assert!(block.len() >= 8, "DES: block must be at least 8 bytes");
271        let input = bytes_to_u64(&block[..8]);
272        let output = self.des_cipher(input, false);
273        block[..8].copy_from_slice(&u64_to_bytes(output));
274    }
275
276    fn decrypt_block(&self, block: &mut [u8]) {
277        assert!(block.len() >= 8, "DES: block must be at least 8 bytes");
278        let input = bytes_to_u64(&block[..8]);
279        let output = self.des_cipher(input, true);
280        block[..8].copy_from_slice(&u64_to_bytes(output));
281    }
282}
283
284// ============================================================
285// Triple DES (3DES / TDEA) -- EDE mode
286// ============================================================
287
288/// Triple DES in EDE (Encrypt-Decrypt-Encrypt) mode.
289///
290/// Uses three independent DES keys (K1, K2, K3). The 24-byte key is split
291/// into three 8-byte DES keys.
292pub struct TripleDes {
293    k1: Des,
294    k2: Des,
295    k3: Des,
296}
297
298impl BlockCipher for TripleDes {
299    const BLOCK_LEN: usize = 8;
300    const KEY_LENS: &'static [usize] = &[24]; // 3 x 8 bytes
301
302    fn new(key: &[u8]) -> Self {
303        assert_eq!(key.len(), 24, "3DES requires a 24-byte key (3 x 8)");
304        TripleDes {
305            k1: Des::new(&key[0..8]),
306            k2: Des::new(&key[8..16]),
307            k3: Des::new(&key[16..24]),
308        }
309    }
310
311    fn encrypt_block(&self, block: &mut [u8]) {
312        self.k1.encrypt_block(block);
313        self.k2.decrypt_block(block);
314        self.k3.encrypt_block(block);
315    }
316
317    fn decrypt_block(&self, block: &mut [u8]) {
318        self.k3.decrypt_block(block);
319        self.k2.encrypt_block(block);
320        self.k1.decrypt_block(block);
321    }
322}
323
324// ============================================================
325// Tests
326// ============================================================
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    fn hex_to_bytes(s: &str) -> Vec<u8> {
333        (0..s.len())
334            .step_by(2)
335            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
336            .collect()
337    }
338
339    /// DES ECB known-answer tests from authoritative sources.
340    #[test]
341    fn des_known_answer_tests() {
342        // J. Orlin Grabbe DES tutorial vector
343        let k1 = Des::new(&hex_to_bytes("133457799BBCDFF1"));
344        let mut b1 = hex_to_bytes("0123456789ABCDEF");
345        k1.encrypt_block(&mut b1);
346        assert_eq!(b1, hex_to_bytes("85E813540F0AB405").as_slice());
347        k1.decrypt_block(&mut b1);
348        assert_eq!(b1, hex_to_bytes("0123456789ABCDEF").as_slice());
349
350        // Key=FEDCBA9876543210, PT=0123456789ABCDEF -> ED39D950FA74BCC4
351        let k2 = Des::new(&hex_to_bytes("FEDCBA9876543210"));
352        let mut b2 = hex_to_bytes("0123456789ABCDEF");
353        k2.encrypt_block(&mut b2);
354        assert_eq!(b2, hex_to_bytes("ED39D950FA74BCC4").as_slice());
355        k2.decrypt_block(&mut b2);
356        assert_eq!(b2, hex_to_bytes("0123456789ABCDEF").as_slice());
357
358        // Key=0123456789ABCDEF, PT=0000000000000000 -> D5D44FF720683D0D
359        let k3 = Des::new(&hex_to_bytes("0123456789ABCDEF"));
360        let mut b3 = [0u8; 8];
361        k3.encrypt_block(&mut b3);
362        assert_eq!(b3.to_vec(), hex_to_bytes("D5D44FF720683D0D"));
363
364        // Key=0123456789ABCDEF, PT=0123456789ABCDEF -> 56CC09E7CFDC4CEF
365        let mut b4 = hex_to_bytes("0123456789ABCDEF");
366        k3.encrypt_block(&mut b4);
367        assert_eq!(b4, hex_to_bytes("56CC09E7CFDC4CEF").as_slice());
368    }
369
370    /// DES round-trip with various inputs.
371    #[test]
372    fn des_round_trip() {
373        let cipher = Des::new(&hex_to_bytes("0123456789ABCDEF"));
374        for pt_hex in &[
375            "0000000000000000",
376            "FFFFFFFFFFFFFFFF",
377            "4E6F772069732074",
378            "0123456789ABCDEF",
379        ] {
380            let pt = hex_to_bytes(pt_hex);
381            let mut block = pt.clone();
382            cipher.encrypt_block(&mut block);
383            assert_ne!(block, pt.as_slice(), "CT should differ from PT for {}", pt_hex);
384            cipher.decrypt_block(&mut block);
385            assert_eq!(block, pt.as_slice(), "round-trip failed for PT={}", pt_hex);
386        }
387    }
388
389    /// 3DES round-trip test.
390    #[test]
391    fn triple_des_round_trip() {
392        let key = hex_to_bytes("0123456789ABCDEF23456789ABCDEF01456789ABCDEF0123");
393        let plaintext = hex_to_bytes("4E6F772069732074");
394
395        let cipher = TripleDes::new(&key);
396
397        let mut block = plaintext.clone();
398        cipher.encrypt_block(&mut block);
399        assert_ne!(block, plaintext.as_slice());
400
401        cipher.decrypt_block(&mut block);
402        assert_eq!(block, plaintext.as_slice());
403    }
404
405    /// 3DES EDE consistency: TripleDes matches manual E-D-E.
406    #[test]
407    fn triple_des_ede_consistency() {
408        let key = hex_to_bytes("0123456789ABCDEF23456789ABCDEF01456789ABCDEF0123");
409        let plaintext = hex_to_bytes("4E6F772069732074");
410        let cipher = TripleDes::new(&key);
411
412        let mut block = plaintext.clone();
413        cipher.encrypt_block(&mut block);
414
415        // Verify by manual EDE
416        let k1 = Des::new(&hex_to_bytes("0123456789ABCDEF"));
417        let k2 = Des::new(&hex_to_bytes("23456789ABCDEF01"));
418        let k3 = Des::new(&hex_to_bytes("456789ABCDEF0123"));
419
420        let mut manual = plaintext.clone();
421        k1.encrypt_block(&mut manual);
422        k2.decrypt_block(&mut manual);
423        k3.encrypt_block(&mut manual);
424
425        assert_eq!(block, manual.as_slice(), "3DES EDE mismatch");
426    }
427}