old_crypto_rs/
vic.rs

1//! VIC cipher implementation.
2//!
3//! The VIC cipher is a sophisticated pencil-and-paper cipher used by Soviet spy Reino Häyhänen
4//! in the 1950s. It combines a straddling checkerboard with two transposition steps.
5//!
6//! Full description & test vectors: <http://www.quadibloc.com/crypto/pp1324.htm>
7//! Additional information in [Kahn on Codes, 1984](https://www.goodreads.com/book/show/457215.Kahn_on_Codes)
8//! ISBN: 978-0-02-560640-1
9//!
10use crate::Block;
11use crate::transposition::{Transposition, IrregularTransposition};
12use crate::straddling::StraddlingCheckerboard;
13use crate::helpers::{to_numeric, SC_ALPHABET};
14
15use eyre::Result;
16
17/// VIC cipher implementation combining straddling checkerboard and transposition ciphers.
18///
19/// The VIC cipher uses a complex key derivation system and three main components:
20/// - A straddling checkerboard for initial encoding
21/// - A first regular transposition
22/// - A second irregular transposition
23/// 
24#[derive(Debug)]
25pub struct VicCipher {
26    // First transposition
27    firsttp: Transposition,
28    // Second transposition
29    secondtp: IrregularTransposition,
30    // Straddling Checkerboard
31    pub sc: StraddlingCheckerboard,
32}
33
34impl VicCipher {
35    /// Creates a new VIC cipher instance with the specified key material.
36    ///
37    /// This constructor performs the complex key derivation process used in the VIC cipher,
38    /// which involves expanding the key material into three separate keys:
39    /// - A key for the first regular transposition
40    /// - A key for the second irregular transposition
41    /// - A key for the straddling checkerboard
42    ///
43    /// # Arguments
44    ///
45    /// * `persn` - Personal number used for the straddling checkerboard (typically 2 digits)
46    /// * `ind` - Indicator string containing at least 5 digits used in key derivation
47    /// * `phrase` - Key phrase that must be at least 20 characters long, used for key expansion
48    /// * `imsg` - Initial message number as a string of digits
49    ///
50    /// # Returns
51    ///
52    /// Returns `Ok(VicCipher)` if the cipher was successfully constructed, or `Err(String)`
53    /// if any of the key material is invalid or if the transposition or straddling checkerboard
54    /// construction fails.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// # use old_crypto_rs::VicCipher;
60    /// let cipher = VicCipher::new(
61    ///     "89",
62    ///     "741776",
63    ///     "IDREAMOFJEANNIEWITHT",
64    ///     "77651"
65    /// ).unwrap();
66    /// ```
67    ///
68    pub fn new(persn: &str, ind: &str, phrase: &str, imsg: &str) -> Result<Self> {
69        let imsg_int = str2int(imsg);
70        let ikey5 = str2int(&ind[..5]);
71
72        let expanded = expand_key(phrase, &imsg_int, &ikey5);
73
74        // First transposition is regular, using 'second' as key
75        let firsttp = Transposition::new(&String::from_utf8_lossy(&expanded.second))?;
76
77        // Second transposition is irregular, using 'third' as key
78        let secondtp = IrregularTransposition::new(&String::from_utf8_lossy(&expanded.third))?;
79
80        // Straddling Checkerboard using 'sckey' (converted to letters) and 'persn'
81        let sc_key_str: String = expanded.sckey.iter().map(|&v| (b'0' + v) as char).collect();
82        let sc = StraddlingCheckerboard::new_with_freq(&sc_key_str, persn, "ATONESIR", SC_ALPHABET)?;
83
84        Ok(VicCipher {
85            firsttp,
86            secondtp,
87            sc,
88        })
89    }
90}
91
92/// Intermediate structure holding expanded key material.
93///
94/// This structure contains the derived keys used for the two transpositions
95/// and the straddling checkerboard.
96///
97#[derive(Debug)]
98struct ExpandedKey {
99    /// Key for the first (regular) transposition
100    second: Vec<u8>,
101    /// Key for the second (irregular) transposition
102    third: Vec<u8>,
103    /// Key for the straddling checkerboard
104    sckey: Vec<u8>,
105}
106
107/// Expands the key material into the three keys needed for the VIC cipher.
108///
109/// This function performs the complex key derivation process using chain addition
110/// and modular arithmetic to generate the transposition and checkerboard keys.
111///
112/// # Arguments
113///
114/// * `phrase` - Key phrase (at least 20 characters) split into two parts
115/// * `imsg` - Initial message number as byte array
116/// * `ikey5` - First 5 digits of indicator as byte array
117///
118/// # Returns
119///
120/// Returns an `ExpandedKey` structure containing all derived key material.
121///
122fn expand_key(phrase: &str, imsg: &[u8], ikey5: &[u8]) -> ExpandedKey {
123    let ph1: Vec<u8> = to_numeric(&phrase[..10]).into_iter().map(|x| (x as u8 + 1) % 10).collect();
124    let ph2: Vec<u8> = to_numeric(&phrase[10..20]).into_iter().map(|x| (x as u8 + 1) % 10).collect();
125
126    let mut first = submod10(imsg, ikey5);
127    first = chainadd_extend(&first, 5);
128
129    addmod10_inplace(&mut first, &ph1);
130    let second = first_encode(&first, &ph2);
131
132    let mut r = second.clone();
133    for _ in 0..5 {
134        chainadd_inplace(&mut r);
135    }
136
137    // In VIC, the key for the second transposition and the SC is derived 
138    // from the 5th iteration of chain addition.
139    let third = r.clone();
140    let r_str: String = r.iter().map(|&b| (b + b'0') as char).collect();
141    let sckey = to_numeric(&r_str);
142
143    ExpandedKey {
144        second,
145        third,
146        sckey,
147    }
148}
149
150
151/// Converts a string of digits to a vector of integers.
152///
153/// # Arguments
154///
155/// * `str` - String containing ASCII digits ('0'-'9')
156///
157/// # Returns
158///
159/// Returns a vector of bytes where each byte is the numeric value (0-9) of the digit.
160///
161#[inline]
162fn str2int(str: &str) -> Vec<u8> {
163    str.bytes().map(|b| b - b'0').collect()
164}
165
166/// Adds two vectors element-wise modulo 10 in-place.
167///
168/// Each element in `a` is replaced with `(a[i] + b[i]) % 10`. The operation
169/// stops when either vector is exhausted.
170///
171/// # Arguments
172///
173/// * `a` - Mutable slice that will be modified with the result
174/// * `b` - Slice to add to `a`
175///
176#[inline]
177fn addmod10_inplace(a: &mut [u8], b: &[u8]) {
178    for (x, y) in a.iter_mut().zip(b) {
179        *x = (*x + *y) % 10;
180    }
181}
182
183#[inline]
184fn submod10(a: &[u8], b: &[u8]) -> Vec<u8> {
185    a.iter().zip(b).map(|(x, y)| (x + 10 - y) % 10).collect()
186}
187
188/// Performs chain addition in-place on a vector.
189///
190/// Chain addition adds each element to its right neighbor (wrapping around at the end)
191/// and stores the result modulo 10 in the original position.
192///
193/// # Arguments
194///
195/// * `a` - Mutable slice to perform chain addition on
196///
197fn chainadd_inplace(a: &mut [u8]) {
198    let l = a.len();
199    if l < 2 { return; }
200    let first = a[0];
201    for i in 0..l - 1 {
202        a[i] = (a[i] + a[i + 1]) % 10;
203    }
204    a[l - 1] = (a[l - 1] + first) % 10;
205}
206
207/// Extends a vector using chain addition.
208///
209/// Each new element is the sum of the element at current index and its successor.
210///
211/// # Arguments
212///
213/// * `a` - Initial slice
214/// * `n` - Number of elements to add
215///
216fn chainadd_extend(a: &[u8], n: usize) -> Vec<u8> {
217    let mut res = Vec::with_capacity(a.len() + n);
218    res.extend_from_slice(a);
219    for i in 0..n {
220        let sum = (res[i] + res[i+1]) % 10;
221        res.push(sum);
222    }
223    res
224}
225
226/// In the original VIC cipher, in order to confuse the adversary even more, plaintext is cut
227/// around the middle, and the two parts are swapped with a marker in between.
228///
229/// (cf. Kahn on Codes)
230///
231//     let ml = pt.len() / 2;
232//     let intv = rand::rng().random_range(1..=(ml / 2));
233//     let ml = ml - intv;
234/// Example:
235/// "ABCDEFGH" is split around the middle and becomes "EFGH-ABCD".
236///
237fn split_plaintext(pt: &[u8], ml: usize) -> Vec<u8> {
238    let mut beg = pt[0..ml].to_vec();
239    let mut res = pt[ml..].to_vec();
240    res.append(&mut vec![b'-' as u8]);
241    res.append(&mut beg);
242    res
243}
244
245/// Expands a 5-element vector to 10 elements using chain addition.
246///
247/// The result contains the original 5 elements followed by 5 elements
248/// generated by chain addition.
249///
250/// # Arguments
251///
252/// * `a` - Input slice (typically 5 elements)
253///
254/// # Returns
255///
256/// Returns a 10-element vector.
257///
258#[inline]
259#[cfg(test)]
260fn expand5to10(a: &[u8]) -> Vec<u8> {
261    chainadd_extend(a, 5)
262}
263
264/// Encodes vector `a` using vector `b` as a lookup table.
265///
266/// Each element in `a` is used as an index (after adjustment) into vector `b`.
267///
268/// # Arguments
269///
270/// * `a` - Vector of indices
271/// * `b` - Lookup table vector
272///
273/// # Returns
274///
275/// Returns a vector where each element is `b[((a[i] + 10) % 10) - 1]`.
276///
277#[inline]
278fn first_encode(a: &[u8], b: &[u8]) -> Vec<u8> {
279    a.iter().map(|&v| b[((v as i32 + 9) % 10) as usize]).collect()
280}
281
282impl Block for VicCipher {
283    fn block_size(&self) -> usize {
284        1
285    }
286
287    /// Encrypts plaintext using the VIC cipher.
288    ///
289    /// The encryption process consists of three steps:
290    /// 1. Encode using the straddling checkerboard
291    /// 2. Apply the first (regular) transposition
292    /// 3. Apply the second (irregular) transposition
293    ///
294    /// # Arguments
295    ///
296    /// * `dst` - Destination buffer for ciphertext (must be large enough)
297    /// * `src` - Source plaintext as bytes
298    ///
299    /// # Returns
300    ///
301    /// Returns the number of bytes written to the destination buffer.
302    ///
303    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
304        // VIC Encipherment:
305        // 0. Split plaintext around the middle and swap halves (with marker).
306        // 1. Straddling Checkerboard
307        // 2. First Transposition (regular)
308        // 3. Second Transposition (irregular)
309        //
310        let split = if src.len() < 2 {
311            src.to_vec()
312        } else {
313            use rand;
314            use rand::RngExt;
315            let mid = src.len() / 2;
316            let delta = src.len() / 3;
317            let min_ml = mid.saturating_sub(delta);
318            let max_ml = (mid + delta).min(src.len() - 1);
319            let ml = if min_ml == max_ml {
320                min_ml
321            } else {
322                rand::rng().random_range(min_ml..=max_ml)
323            };
324            split_plaintext(src, ml)
325        };
326
327        let mut buf_sc = vec![0u8; split.len() * 3]; // Straddling can expand
328        let sc_len = self.sc.encrypt(&mut buf_sc, &split);
329
330        let mut buf_tp1 = vec![0u8; sc_len];
331        let tp1_len = self.firsttp.encrypt(&mut buf_tp1, &buf_sc[..sc_len]);
332
333        self.secondtp.encrypt(dst, &buf_tp1[..tp1_len])
334    }
335
336    /// Decrypts ciphertext using the VIC cipher.
337    ///
338    /// The decryption process reverses the encryption steps:
339    /// 1. Reverse the second (irregular) transposition
340    /// 2. Reverse the first (regular) transposition
341    /// 3. Decode using the straddling checkerboard
342    ///
343    /// # Arguments
344    ///
345    /// * `dst` - Destination buffer for plaintext (must be large enough)
346    /// * `src` - Source ciphertext as bytes
347    ///
348    /// # Returns
349    ///
350    /// Returns the number of bytes written to the destination buffer.
351    ///
352    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
353        // VIC Decipherment (Reverse of Encipherment):
354        // 1. Second Transposition (irregular)
355        // 2. First Transposition (regular)
356        // 3. Straddling Checkerboard
357        // 4. Unsplit plaintext by removing marker and swapping halves back.
358        //
359        let mut buf_tp2 = vec![0u8; src.len()];
360        let tp2_len = self.secondtp.decrypt(&mut buf_tp2, src);
361
362        let mut buf_tp1 = vec![0u8; tp2_len];
363        let tp1_len = self.firsttp.decrypt(&mut buf_tp1, &buf_tp2[..tp2_len]);
364
365        let mut buf_sc = vec![0u8; tp1_len];
366        let sc_len = self.sc.decrypt(&mut buf_sc, &buf_tp1[..tp1_len]);
367        let sc_plain = &buf_sc[..sc_len];
368
369        if let Some(dash_pos) = sc_plain.iter().position(|&b| b == b'-') {
370            let after = &sc_plain[dash_pos + 1..];
371            let before = &sc_plain[..dash_pos];
372            let mut out_len = 0;
373
374            if out_len + after.len() <= dst.len() {
375                dst[..after.len()].copy_from_slice(after);
376                out_len += after.len();
377            } else {
378                let n = dst.len().saturating_sub(out_len);
379                dst[..n].copy_from_slice(&after[..n]);
380                return out_len + n;
381            }
382
383            if out_len + before.len() <= dst.len() {
384                dst[out_len..out_len + before.len()].copy_from_slice(before);
385                out_len += before.len();
386            } else {
387                let n = dst.len().saturating_sub(out_len);
388                dst[out_len..out_len + n].copy_from_slice(&before[..n]);
389                out_len += n;
390            }
391
392            out_len
393        } else {
394            let n = sc_plain.len().min(dst.len());
395            dst[..n].copy_from_slice(&sc_plain[..n]);
396            n
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use rstest::rstest;
405    use crate::helpers::to_numeric;
406
407    #[test]
408    fn test_new_cipher() {
409        let _c = VicCipher::new("89", "741776", "IDREAMOFJEANNIEWITHT", "77651").unwrap();
410    }
411
412    #[rstest]
413    #[case("IDREAMOFJE", vec![6, 2, 0, 3, 1, 8, 9, 5, 7, 4])]
414    #[case("ANNIEWITHT", vec![1, 6, 7, 4, 2, 0, 5, 8, 3, 9])]
415    fn test_to_numeric_one(#[case] s: &str, #[case] r: Vec<u8>) {
416        let res: Vec<u8> = to_numeric(s).into_iter().map(|x| (x as u8 + 1) % 10).collect();
417        assert_eq!(res, r);
418    }
419
420    #[rstest]
421    #[case(vec![8, 6, 1, 5, 4], vec![2, 0, 9, 5, 2], vec![0, 6, 0, 0, 6])]
422    #[case(vec![7, 7, 6, 5, 1], vec![7, 4, 1, 7, 7], vec![4, 1, 7, 2, 8])]
423    fn test_addmod10(#[case] mut a: Vec<u8>, #[case] b: Vec<u8>, #[case] c: Vec<u8>) {
424        addmod10_inplace(&mut a, &b);
425        assert_eq!(a, c);
426    }
427
428    #[rstest]
429    #[case(vec![8, 6, 1, 5, 4], vec![2, 0, 9, 5, 2], vec![6, 6, 2, 0, 2])]
430    #[case(vec![7, 7, 6, 5, 1], vec![7, 4, 1, 7, 7], vec![0, 3, 5, 8, 4])]
431    fn test_submod10(#[case] a: Vec<u8>, #[case] b: Vec<u8>, #[case] c: Vec<u8>) {
432        assert_eq!(submod10(&a, &b), c);
433    }
434
435    #[rstest]
436    #[case(vec![8, 6, 1, 5, 4], vec![4, 7, 6, 9, 2])]
437    #[case(vec![7, 7, 6, 5, 1], vec![4, 3, 1, 6, 8])]
438    fn test_chainadd_inplace(#[case] mut a: Vec<u8>, #[case] b: Vec<u8>) {
439        chainadd_inplace(&mut a);
440        assert_eq!(a, b);
441    }
442
443    #[rstest]
444    #[case(vec![8, 6, 1, 5, 4], vec![8, 6, 1, 5, 4, 4, 7, 6, 9, 8])]
445    #[case(vec![7, 7, 6, 5, 1], vec![7, 7, 6, 5, 1, 4, 3, 1, 6, 5])]
446    #[case(vec![0, 3, 5, 8, 4], vec![0, 3, 5, 8, 4, 3, 8, 3, 2, 7])]
447    fn test_expand5to10(#[case] a: Vec<u8>, #[case] b: Vec<u8>) {
448        assert_eq!(expand5to10(&a), b);
449    }
450
451    #[test]
452    fn test_first_encode() {
453        let r1 = vec![6, 5, 5, 1, 5, 1, 7, 8, 9, 1];
454        let r2 = vec![1, 6, 7, 4, 2, 0, 5, 8, 3, 9];
455        let r = vec![0, 2, 2, 1, 2, 1, 5, 8, 3, 1];
456        assert_eq!(first_encode(&r1, &r2), r);
457    }
458
459    #[test]
460    fn test_vic_cipher_full() {
461        let c = VicCipher::new("89", "741776", "IDREAMOFJEANNIEWITHT", "77651").unwrap();
462        
463        let pt = "HELLOWORLD";
464        let mut ct = vec![0u8; 100];
465        let ct_actual_len = c.encrypt(&mut ct, pt.as_bytes());
466        
467        let ct_trimmed = &ct[..ct_actual_len];
468
469        // Last digit of ind "741776" is 6.
470        // imsg is "77651".
471        // ct_trimmed should have imsg inserted at index 6.
472        //
473        assert!(ct_trimmed.len() >= 11);
474
475        let mut decrypted = vec![0u8; 100];
476        let dec_len = c.decrypt(&mut decrypted, ct_trimmed);
477        
478        let res = String::from_utf8_lossy(&decrypted[..dec_len]).to_string();
479        assert_eq!(res, pt);
480    }
481
482    #[test]
483    fn test_vic_wikipedia_example() {
484        // From Wikipedia:
485        // Phrase: IDREAMOFJEANNIEWITHT
486        // Date: 13 Sept 1944 -> 1391944 (7 digits)
487        // Personal Number: 6
488        // Indicator: 74177 (first 5 digits)
489        
490        // Let's see how Wikipedia maps this to our New arguments.
491        // persn: "60" (Personal number 6, usually represented as 2 digits for SC)
492        // ind: "74177"
493        // phrase: "IDREAMOFJEANNIEWITHT"
494        // imsg: "1391944"
495
496        // Step 1: Subtraction modulo 10
497        // G = 1 3 9 1 9 (first 5 of imsg)
498        // H = 7 4 1 7 7 (ikey5)
499        // J = 4 9 8 4 2 (G - H mod 10)
500        //
501        let imsg = str2int("1391944");
502        let ikey5 = str2int("74177");
503        let j = submod10(&imsg[..5], &ikey5);
504        assert_eq!(j, vec![4, 9, 8, 4, 2]);
505
506        // Step 2: Chain addition to 10 digits
507        // 4 9 8 4 2
508        // 3 7 2 6 5 (4+9=13, 9+8=17, 8+4=12, 4+2=6, 2+3=5)
509        // K = 4 9 8 4 2 3 7 2 6 5
510        //
511        let k = expand5to10(&j);
512        assert_eq!(k, vec![4, 9, 8, 4, 2, 3, 7, 2, 6, 5]);
513
514        // Step 3: Add to PH1
515        // Phrase: I D R E A M O F J E | A N N I E W I T H T
516        // PH1:    5 1 9 2 0 6 7 3 8 4
517        // A D E E F I J M O R
518        // 0 1 2 3 4 5 6 7 8 9
519        // A:0 D:1 E:2 E:3 F:4 I:5 J:6 M:7 O:8 R:9
520        // +1 mod 10:
521        // A:1 D:2 E:3 E:4 F:5 I:6 J:7 M:8 O:9 R:0
522        // IDREAMOFJE:
523        // I:6 D:2 R:0 E:3 A:1 M:8 O:9 F:5 J:7 E:4 -> 6 2 0 3 1 8 9 5 7 4?
524        //
525        // A: 0
526        // D: 1
527        // E: 2
528        // E: 3
529        // F: 4
530        // I: 5
531        // J: 6
532        // M: 7
533        // O: 8
534        // R: 9
535        // IDREAMOFJE:
536        // I: 5 -> 6
537        // D: 1 -> 2
538        // R: 9 -> 0
539        // E: 2 -> 3
540        // A: 0 -> 1
541        // M: 7 -> 8
542        // O: 8 -> 9
543        // F: 4 -> 5
544        // J: 6 -> 7
545        // E: 3 -> 4
546        // So ph1 should be [6, 2, 0, 3, 1, 8, 9, 5, 7, 4].
547        //
548        let ph1 = vec![6, 2, 0, 3, 1, 8, 9, 5, 7, 4];
549
550        // L = K + PH1 mod 10
551        // 4 9 8 4 2 3 7 2 6 5
552        // 6 2 0 3 1 8 9 5 7 4
553        // -------------------
554        // 0 1 8 7 3 1 6 7 3 9
555        //
556        let mut l = k.clone();
557        addmod10_inplace(&mut l, &ph1);
558        assert_eq!(l, vec![0, 1, 8, 7, 3, 1, 6, 7, 3, 9]);
559
560        // Step 4: First Encoding
561        // PH2: 1 6 7 4 2 0 5 8 3 9 (ranks of ANNIEWITHT)
562        // M = encode L with PH2
563        // L: 0 1 8 7 3 1 6 7 3 9
564        // PH2: 1 2 3 4 5 6 7 8 9 0 (index 1..10)
565        //      1 6 7 4 2 0 5 8 3 9 (value)
566        //
567        // Note: Wikipedia says "replace each digit in L with the digit below it in the PH2 line"
568        // Digit 0 -> index 10 in PH2 (if 1-based)
569        // Digit 9 -> index 9 in PH2
570        //
571        let ph2 = vec![1, 6, 7, 4, 2, 0, 5, 8, 3, 9];
572        let m = first_encode(&l, &ph2);
573
574        // Wikipedia result for M: 9 1 8 5 7 1 0 5 7 3
575        // L[0]=0 -> PH2[9]=9.
576        // L[1]=1 -> PH2[0]=1.
577        // L[2]=8 -> PH2[7]=8.
578        // L[3]=7 -> PH2[6]=5.
579        // L[4]=3 -> PH2[2]=7.
580        // L[5]=1 -> PH2[0]=1.
581        // L[6]=6 -> PH2[5]=0.
582        // L[7]=7 -> PH2[6]=5.
583        // L[8]=3 -> PH2[2]=7.
584        // L[9]=9 -> PH2[8]=3.
585        //
586        assert_eq!(m, vec![9, 1, 8, 5, 7, 1, 0, 5, 7, 3]);
587
588        // Step 5: Chain addition 5 times
589        //
590        let mut r = m.clone();
591        for _ in 0..5 {
592            chainadd_inplace(&mut r);
593        }
594
595        // Result should be used for second transposition and SC
596        // Wikipedia: 
597        // 1st: 0 9 3 2 8 1 5 2 0 2
598        // 2nd: 9 2 5 0 9 6 7 2 2 2
599        // 3rd: 1 7 5 9 5 3 9 4 4 1
600        // 4th: 8 2 4 4 8 2 3 8 5 2
601        // 5th: 0 6 8 2 0 5 1 3 7 0
602        //
603        assert_eq!(r, vec![0, 6, 8, 2, 0, 5, 1, 3, 7, 0]);
604
605        // These digits are used for the second transposition key
606        // And their numerical order for SC key.
607        //
608        let r_str: String = r.iter().map(|&b| (b + b'0') as char).collect();
609        let sckey = to_numeric(&r_str);
610
611        // 0 6 8 2 0 5 1 3 7 0
612        // Ranks (0-based, stable sort):
613        // Pos 0: digit 0 -> rank 0
614        // Pos 1: digit 6 -> rank 7
615        // Pos 2: digit 8 -> rank 9
616        // Pos 3: digit 2 -> rank 4
617        // Pos 4: digit 0 -> rank 1
618        // Pos 5: digit 5 -> rank 6
619        // Pos 6: digit 1 -> rank 3
620        // Pos 7: digit 3 -> rank 5
621        // Pos 8: digit 7 -> rank 8
622        // Pos 9: digit 0 -> rank 2
623        // Ranks: 0 7 9 4 1 6 3 5 8 2
624        //
625        assert_eq!(sckey, vec![0, 7, 9, 4, 1, 6, 3, 5, 8, 2]);
626    }
627
628    #[rstest]
629    #[case(b"ABCDEFGH", 1, b"BCDEFGH-A")]
630    #[case(b"ABCDEFGH", 4, b"EFGH-ABCD")]
631    #[case(b"ABCDEFGH", 7, b"H-ABCDEFG")]
632    fn test_split_plaintext_cases(#[case] pt: &[u8], #[case] ml: usize, #[case] expected: &[u8]) {
633        let out = split_plaintext(pt, ml);
634        assert_eq!(out, expected);
635    }
636
637    #[test]
638    fn test_split_plaintext_invariants() {
639        let pt = b"ABCDEFGHIJKLMNOP";
640        let ml = 6;
641        let out = split_plaintext(pt, ml);
642        assert_eq!(out.len(), pt.len() + 1);
643
644        let dash_positions: Vec<usize> = out.iter().enumerate().filter_map(|(i, &b)| if b == b'-' { Some(i) } else { None }).collect();
645        assert_eq!(dash_positions.len(), 1);
646
647        let dash_pos = dash_positions[0];
648        assert_eq!(dash_pos, pt.len() - ml);
649
650        let mut without_dash = out.clone();
651        without_dash.retain(|&b| b != b'-');
652        let mut expected = pt[ml..].to_vec();
653        expected.extend_from_slice(&pt[..ml]);
654        assert_eq!(without_dash, expected);
655    }
656
657}