1mod 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#[derive(Debug)]
80pub struct SecomCipher {
81 checker: SecomCheckerboard,
83 tp1: Transposition,
85 tp2: SecomDisruptedTransposition,
87}
88
89impl SecomCipher {
90 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 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 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 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 let widths = transposition_widths_from_last_row(&last_row)?;
131 let tp1_width = widths.0;
132 let tp2_width = widths.1;
133
134 let key10 = addmod10(&b_digits, &header_digits);
138
139 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 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 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 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 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 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 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 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 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]; let width = 11;
359 let tp = SecomDisruptedTransposition::new(width, key);
360
361 let pt = b"1234567890123456789012"; 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 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}