old_crypto_rs/secom/
mod.rs

1//! SECOM cipher implementation (VIC-derived).
2//!
3//! Based on the description and worked example at:
4//! <http://www.ciphermachinesandcryptology.com/en/secom.htm>
5//!
6//! NOTE: there are separate implementation of the Checkerboard and disrupted
7//! transposition due to the nature of both ciphers:
8//! 1. The alphabets are different, and we need two digits in the VIC cipher, whereas SECOM needs 3.
9//! 2. The disrupted transposition is not symmetric, so we need to implement it twice.
10//!
11//! SECOM is easier to implement, as the key is only one phrase, and the key derivation is also
12//! easier to run.  Also, you can use numbers directly in the plaintext as they will not be triplicated
13//! like for any cipher using only 25 letters.
14//!
15//! # Example
16//!
17//! ```rust
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! use old_crypto_rs::SecomCipher;
20//! use old_crypto_rs::Block;
21//!
22//! let key_phrase = "MAKE NEW FRIENDS BUT KEEP THE OLD";
23//! let freq = "ESTONIA";
24//!
25//! let cipher = SecomCipher::new(key_phrase, freq)?;
26//! let pt = b"HELLO WORLD";
27//!
28//! let mut ct = vec![0u8; 128];
29//! let ct_len = cipher.encrypt(&mut ct, pt);
30//! let mut dec = vec![0u8; 128];
31//! let dec_len = cipher.decrypt(&mut dec, &ct[..ct_len]);
32//! assert_eq!(&dec[..dec_len], pt);
33//!
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # Frequency
39//!
40//! The second parameter to `SecomCipher::new()` is a set of letters to be used as the most
41//! frequent ones.  It influences the creation of the checkerboard.  You can see the difference in
42//! cipher length when running the `demo` example:
43//!
44//! ```text
45//!==> SECOM (IDREAMOFJEANNIEWITHT, ATONESI)
46//! 22265 58532 79021 61206 54087 58289 27682 72352 18256 22566 72551 11552 86498 27099 253
47//! decrypt ok
48//!
49//! ==> SECOM (IDREAMOFJEANNIEWITHT, ESANTIR)
50//! 56763 72250 55524 26777 27282 92929 52712 88782 65016 42708 80532 76207 25573 72925 50700 67
51//! decrypt ok
52//! ```
53//! The first example use the English frequent letters, whereas the second one is using the French set.
54//! (plain text is in English)
55//!
56
57mod checkerboard;
58mod disrupted;
59mod subr;
60
61pub(crate) use checkerboard::SecomCheckerboard;
62pub(crate) use disrupted::SecomDisruptedTransposition;
63pub(crate) use subr::{
64    addmod10, chain_add_row, letters_to_digits_1to0, normalize_key_phrase, rank_digits_1to0,
65    read_out_columns, transposition_widths_from_last_row, vec_to_array_10,
66};
67
68use crate::{Block, Transposition};
69use crate::error::Error;
70
71use eyre::Result;
72
73/// SECOM cipher implementation.
74///
75/// SECOM is a VIC-derived cipher that uses a single key phrase and a frequency
76/// string to initialize its components: a straddling checkerboard and two
77/// columnar transpositions (one regular and one disrupted).
78///
79#[derive(Debug)]
80pub struct SecomCipher {
81    /// The internal straddling checkerboard.
82    checker: SecomCheckerboard,
83    /// First (regular) columnar transposition.
84    tp1: Transposition,
85    /// The second (disrupted) columnar transposition.
86    tp2: SecomDisruptedTransposition,
87}
88
89impl SecomCipher {
90    /// Creates a new `SecomCipher` instance.
91    ///
92    /// # Arguments
93    ///
94    /// * `key_phrase` - A string of at least 20 letters used for key derivation.
95    /// * `freq` - A 7-letter frequency string for the checkerboard (e.g., "ESTONIA").
96    ///
97    pub fn new(key_phrase: &str, freq: &str) -> Result<Self> {
98        let key = normalize_key_phrase(key_phrase);
99        if key.len() < 20 {
100            return Err(Error::KeyTooShort(20).into());
101        }
102        let key20 = &key[..20];
103        let a = &key20[..10];
104        let b = &key20[10..20];
105
106        // Derive key digits from the key phrase
107        //
108        let a_digits = letters_to_digits_1to0(a);
109        let b_digits = letters_to_digits_1to0(b);
110        let sum_row = addmod10(&a_digits, &b_digits);
111
112        // Now, do the chain addition
113        //
114        let mut rows = Vec::with_capacity(5);
115        let mut prev = sum_row.clone();
116        for _ in 0..5 {
117            let next = chain_add_row(&prev);
118            rows.push(next.clone());
119            prev = next;
120        }
121
122        // Derive the checkerboard key from the last row
123        //
124        let last_row = rows.last().ok_or(Error::RowGenerationFailed)?.clone();
125        let header_digits = rank_digits_1to0(&last_row);
126        let checker = SecomCheckerboard::new(vec_to_array_10(&header_digits)?, freq)?;
127
128        // Derive the lengths of both transpositions
129        //
130        let widths = transposition_widths_from_last_row(&last_row)?;
131        let tp1_width = widths.0;
132        let tp2_width = widths.1;
133
134        // The two transposition keys are generated by adding the checkerboard key & the 2nd part of the
135        // keyphrase, and this will be used as a transposition.
136        //
137        let key10 = addmod10(&b_digits, &header_digits);
138
139        // Generate the two keys by taking enough digits.
140        //
141        let key_stream = read_out_columns(&rows, &key10);
142        if key_stream.len() < tp1_width + tp2_width {
143            return Err(Error::BadKeyStream.into());
144        }
145
146        // First transposition
147        //
148        let tp1_key = &key_stream[..tp1_width];
149        let mut tp1_key_str = String::with_capacity(tp1_key.len());
150        for &d in tp1_key {
151            tp1_key_str.push((b'0' + d) as char);
152        }
153        let tp1 = Transposition::new(&tp1_key_str)?;
154
155        // Second transposition
156        //
157        let tp2_key = key_stream[tp1_width..tp1_width + tp2_width].to_vec();
158        let tp2 = SecomDisruptedTransposition::new(tp2_width, tp2_key);
159
160        Ok(SecomCipher { checker, tp1, tp2 })
161    }
162
163    /// Normalizes plaintext for encryption: converts spaces to '*' and
164    /// keeps only A-Z, 0-9, and '*'.
165    ///
166    fn preprocess_plaintext(pt: &[u8]) -> Vec<u8> {
167        let mut out = Vec::with_capacity(pt.len());
168        for &ch in pt {
169            let c = if ch == b' ' {
170                b'*'
171            } else {
172                ch.to_ascii_uppercase()
173            };
174            if (b'A'..=b'Z').contains(&c) || (b'0'..=b'9').contains(&c) || c == b'*' {
175                out.push(c);
176            }
177        }
178        out
179    }
180
181    /// Converts '*' back to spaces in the decrypted plaintext.
182    ///
183    fn postprocess_plaintext(pt: &[u8]) -> Vec<u8> {
184        let mut out = Vec::with_capacity(pt.len());
185        for &ch in pt {
186            out.push(if ch == b'*' { b' ' } else { ch });
187        }
188        out
189    }
190}
191
192impl Block for SecomCipher {
193    fn block_size(&self) -> usize {
194        1
195    }
196
197    /// Encrypts source data into destination buffer.
198    ///
199    /// This implementation performs:
200    /// 1. Preprocessing (uppercase, space conversion).
201    /// 2. Checkerboard encoding.
202    /// 3. Padding to a multiple of 5.
203    /// 4. First (regular) columnar transposition.
204    /// 5. Second (disrupted) columnar transposition.
205    ///
206    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
207        let pt = Self::preprocess_plaintext(src);
208        let mut digits = vec![0u8; pt.len() * 2];
209        let sc_len = self.checker.encrypt(&mut digits, &pt);
210        let digits = &digits[..sc_len];
211
212        let mut tp1_buf = vec![0u8; digits.len()];
213        self.tp1.encrypt(&mut tp1_buf, digits);
214
215        self.tp2.encrypt(dst, &tp1_buf)
216    }
217
218    /// Decrypts source data into destination buffer.
219    ///
220    /// This implementation performs:
221    /// 1. Second (disrupted) columnar transposition (reverse).
222    /// 2. First (regular) columnar transposition (reverse).
223    /// 3. Padding removal.
224    /// 4. Checkerboard decoding.
225    /// 5. Postprocessing.
226    ///
227    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
228        let mut tp1_encoded = vec![0u8; src.len()];
229        let tp1_len = self.tp2.decrypt(&mut tp1_encoded, src);
230        let tp1_encoded = &tp1_encoded[..tp1_len];
231
232        let mut digits = vec![0u8; tp1_encoded.len()];
233        self.tp1.decrypt(&mut digits, tp1_encoded);
234
235        let mut buf_pt = vec![0u8; digits.len()];
236        let pt_len = self.checker.decrypt(&mut buf_pt, &digits);
237        let post = Self::postprocess_plaintext(&buf_pt[..pt_len]);
238
239        let n = post.len().min(dst.len());
240        dst[..n].copy_from_slice(&post[..n]);
241        n
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::secom::subr::{
249        addmod10, chain_add_row, column_order_from_digits, letters_to_digits_1to0,
250        rank_digits_1to0, read_out_columns, transposition_widths_from_last_row, vec_to_array_10,
251    };
252
253    #[test]
254    fn test_key_derivation_example() {
255        let key_phrase = "MAKE NEW FRIENDS BUT KEEP THE OLD";
256        let key = normalize_key_phrase(key_phrase);
257        let key20 = &key[..20];
258        let a = &key20[..10];
259        let b = &key20[10..20];
260
261        let a_digits = letters_to_digits_1to0(a);
262        let b_digits = letters_to_digits_1to0(b);
263        assert_eq!(a_digits, vec![7, 1, 6, 2, 8, 3, 0, 4, 9, 5]);
264        assert_eq!(b_digits, vec![3, 7, 2, 8, 1, 0, 9, 6, 4, 5]);
265
266        let sum_row = addmod10(&a_digits, &b_digits);
267        assert_eq!(sum_row, vec![0, 8, 8, 0, 9, 3, 9, 0, 3, 0]);
268
269        let mut rows = Vec::new();
270        let mut prev = sum_row.clone();
271        for _ in 0..5 {
272            let next = chain_add_row(&prev);
273            rows.push(next.clone());
274            prev = next;
275        }
276        assert_eq!(rows[0], vec![8, 6, 8, 9, 2, 2, 9, 3, 3, 8]);
277        assert_eq!(rows[1], vec![4, 4, 7, 1, 4, 1, 2, 6, 1, 2]);
278        assert_eq!(rows[2], vec![8, 1, 8, 5, 5, 3, 8, 7, 3, 0]);
279        assert_eq!(rows[3], vec![9, 9, 3, 0, 8, 1, 5, 0, 3, 9]);
280        assert_eq!(rows[4], vec![8, 2, 3, 8, 9, 6, 5, 3, 2, 7]);
281
282        let header = rank_digits_1to0(&rows[4]);
283        assert_eq!(header, vec![8, 1, 3, 9, 0, 6, 5, 4, 2, 7]);
284
285        let (w1, w2) = transposition_widths_from_last_row(&rows[4]).unwrap();
286        assert_eq!(w1, 12);
287        assert_eq!(w2, 11);
288
289        let key10 = addmod10(&b_digits, &header);
290        assert_eq!(key10, vec![1, 8, 5, 7, 1, 6, 4, 0, 6, 2]);
291
292        let key_stream = read_out_columns(&rows, &key10);
293        let tp1_key: String = key_stream[..w1]
294            .iter()
295            .map(|d| (b'0' + d) as char)
296            .collect();
297        let tp2_key: String = key_stream[w1..w1 + w2]
298            .iter()
299            .map(|d| (b'0' + d) as char)
300            .collect();
301        assert_eq!(tp1_key, "848982458982");
302        assert_eq!(tp2_key, "09792855878");
303    }
304
305    #[test]
306    fn test_checkerboard_example() {
307        let header = vec![8, 1, 3, 9, 0, 6, 5, 4, 2, 7];
308        let checker = SecomCheckerboard::new(vec_to_array_10(&header).unwrap(), "ESTONIA").unwrap();
309        assert_eq!(checker.longc, [3, 6, 2]);
310
311        let mut out = vec![0u8; 256];
312        let pt = b"RV*TOMORROW*AT*1400PM*TO*COMPLETE*TRANSACTION*USE*DEADDROP*AS*USUAL";
313        let len = checker.encrypt(&mut out, pt);
314        let ct = String::from_utf8_lossy(&out[..len]).to_string();
315        assert_eq!(
316            ct,
317            "64676090310646406860796021202828663160906039031663889860964751739940560621860308730306406660716062162738"
318        );
319    }
320
321    #[test]
322    fn test_checkerboard_isolated() {
323        // From the example: header = [8, 1, 3, 9, 0, 6, 5, 4, 2, 7], freq = "ESTONIA"
324        //
325        let header = [8, 1, 3, 9, 0, 6, 5, 4, 2, 7];
326        let checker = SecomCheckerboard::new(header, "ESTONIA").unwrap();
327
328        let pt = b"HELLO";
329        let mut ct = vec![0u8; 10];
330        let ct_len = checker.encrypt(&mut ct, pt);
331
332        // H is not in ESTONIA. It's in the first extra row (long digit 3).
333        // E is in ESTONIA. Top row.
334        // L is not in ESTONIA. Extra row.
335        // O is in ESTONIA. Top row.
336
337        // Let's verify some mappings manually based on the logic:
338        // header: 8 1 3 9 0 6 5 4 2 7
339        // col:    0 1 2 3 4 5 6 7 8 9
340        // FREQ_BLANK_POS: 2, 5, 8 (header[2]=3, header[5]=6, header[8]=2)
341        // Top row (freq="ESTONIA"):
342        // col 0: header[0]=8 -> E
343        // col 1: header[1]=1 -> S
344        // col 3: header[3]=9 -> T
345        // col 4: header[4]=0 -> O
346        // col 6: header[6]=5 -> N
347        // col 7: header[7]=4 -> I
348        // col 9: header[9]=7 -> A
349        //
350        let mut dec = vec![0u8; 5];
351        let dec_len = checker.decrypt(&mut dec, &ct[..ct_len]);
352        assert_eq!(&dec[..dec_len], pt);
353    }
354
355    #[test]
356    fn test_disrupted_transposition_isolated() {
357        let key = vec![0, 9, 7, 9, 2, 8, 5, 5, 8, 7, 8]; // tp2_key from example
358        let width = 11;
359        let tp = SecomDisruptedTransposition::new(width, key);
360
361        let pt = b"1234567890123456789012"; // 2 full rows
362        let mut ct = vec![0u8; 22];
363        tp.encrypt(&mut ct, pt);
364
365        let mut dec = vec![0u8; 22];
366        tp.decrypt(&mut dec, &ct);
367        assert_eq!(dec, pt);
368    }
369
370    #[test]
371    fn test_column_order() {
372        let key = vec![1, 8, 5, 7, 1, 6, 4, 0, 6, 2];
373        let order = column_order_from_digits(&key);
374
375        // Let's use the actual observed output to match the logic
376        // key: [1, 8, 5, 7, 1, 6, 4, 0, 6, 2]
377        // sorted by (if d==0 {10} else {d}, index):
378        // (1, 0) -> 0
379        // (1, 4) -> 4
380        // (2, 9) -> 9
381        // (4, 6) -> 6
382        // (5, 2) -> 2
383        // (6, 5) -> 5
384        // (6, 8) -> 8
385        // (7, 3) -> 3
386        // (8, 1) -> 1
387        // (10, 7) -> 7 (because d=0)
388        //
389        assert_eq!(order, vec![0, 4, 9, 6, 2, 5, 8, 3, 1, 7]);
390    }
391
392    #[test]
393    fn test_full_cipher_example() {
394        let key_phrase = "MAKE NEW FRIENDS BUT KEEP THE OLD";
395        let cipher = SecomCipher::new(key_phrase, "ESTONIA").unwrap();
396
397        let pt = b"RV TOMORROW AT 1400PM TO COMPLETE TRANSACTION USE DEADDROP AS USUAL";
398        let mut ct = vec![0u8; 256];
399        let ct_len = cipher.encrypt(&mut ct, pt);
400        let ct_str = String::from_utf8_lossy(&ct[..ct_len]).to_string();
401        assert_eq!(
402            ct_str,
403            "37719386226003204230600382968314608060517801673776060646936069686740369681890014021906662606660863160549"
404        );
405
406        let mut dec = vec![0u8; 256];
407        let dec_len = cipher.decrypt(&mut dec, ct_str.as_bytes());
408        let dec_str = String::from_utf8_lossy(&dec[..dec_len]).to_string();
409        assert_eq!(
410            dec_str,
411            "RV TOMORROW AT 1400PM TO COMPLETE TRANSACTION USE DEADDROP AS USUAL"
412        );
413    }
414
415    #[test]
416    fn test_custom_freq() {
417        let key_phrase = "MAKE NEW FRIENDS BUT KEEP THE OLD";
418        let freq = "BCDFGHJ";
419        let cipher = SecomCipher::new(key_phrase, freq).unwrap();
420        let pt = b"HELLO WORLD";
421        let mut ct = vec![0u8; 128];
422        let ct_len = cipher.encrypt(&mut ct, pt);
423        let mut dec = vec![0u8; 128];
424        let dec_len = cipher.decrypt(&mut dec, &ct[..ct_len]);
425        assert_eq!(&dec[..dec_len], pt);
426    }
427}