old_crypto_rs/
solitaire.rs

1//! Implements the Solitaire cipher algorithm for encryption and decryption.
2//!
3//! The Solitaire cipher (also known as Pontifex) is a manual cryptographic algorithm
4//! designed by Bruce Schneier for the novel "Cryptonomicon" by Neal Stephenson.
5//! It uses a standard 54-card deck (52 playing cards plus 2 jokers) to generate
6//! a keystream for encryption and decryption.
7//!
8//! # Algorithm Overview
9//!
10//! The cipher operates by repeatedly transforming the deck through a series of steps:
11//! 1. Move Joker A (card 53) one position down
12//! 2. Move Joker B (card 54) two positions down
13//! 3. Perform a triple cut around the two jokers
14//! 4. Perform a count cut based on the bottom card's value
15//! 5. Output the card at the position indicated by the top card's value
16//!
17//! # Security Note
18//!
19//! This cipher is designed for educational purposes and manual computation.
20//! It is **not recommended** for securing sensitive information in practice,
21//! as it is vulnerable to various cryptanalytic attacks when used incorrectly.
22//!
23//! # Usage
24//!
25//! ```
26//! use old_crypto_rs::Solitaire;
27//! use old_crypto_rs::Block;
28//!
29//! // Create cipher with unkeyed (sorted) deck
30//! let cipher = Solitaire::new_unkeyed();
31//!
32//! // Or create with a passphrase
33//! let cipher = Solitaire::new_with_passphrase("MYSECRET");
34//!
35//! // Encrypt plaintext
36//! let plaintext = b"HELLO";
37//! let mut ciphertext = vec![0u8; plaintext.len()];
38//! cipher.encrypt(&mut ciphertext, plaintext);
39//!
40//! // Decrypt ciphertext
41//! let mut decrypted = vec![0u8; ciphertext.len()];
42//! cipher.decrypt(&mut decrypted, &ciphertext);
43//! ```
44//!
45//! # References
46//!
47//! - Bruce Schneier's original specification: <https://www.schneier.com/academic/solitaire/>
48//! - "Cryptonomicon" by Neal Stephenson
49//! - Wikipedia article on Solitaire cipher
50//! 
51use crate::Block;
52use std::cell::RefCell;
53
54/// A Solitaire cipher implementation using a 54-card deck.
55///
56/// The `Solitaire` struct maintains the state of a deck of cards used for encryption
57/// and decryption operations. It stores both the initial deck configuration (for
58/// resetting between operations) and the current deck state (which is modified during
59/// keystream generation).
60///
61/// # Structure
62///
63/// - `initial_deck`: The original deck configuration, preserved for resetting
64/// - `deck`: The working deck state, wrapped in `RefCell` for interior mutability
65///
66/// # Thread Safety
67///
68/// This type is not thread-safe due to the use of `RefCell`. Each thread should
69/// create its own instance if parallel encryption/decryption is needed.
70///
71/// # Examples
72///
73/// ```
74/// use old_crypto_rs::Solitaire;
75/// use old_crypto_rs::Block;
76///
77/// // Create with default unkeyed deck
78/// let cipher = Solitaire::new_unkeyed();
79///
80/// // Create with a passphrase
81/// let cipher = Solitaire::new_with_passphrase("SECRET");
82///
83/// // Create with custom deck
84/// let custom_deck: Vec<u8> = (1..=54).collect();
85/// let cipher = Solitaire::new(custom_deck);
86/// ```
87/// 
88#[derive(Clone)]
89pub struct Solitaire {
90    initial_deck: Vec<u8>,
91    deck: RefCell<Vec<u8>>,
92}
93
94impl Solitaire {
95    /// Creates a new Solitaire cipher with a custom deck configuration.
96    ///
97    /// The deck must be a permutation of 54 cards:
98    /// - Cards 1-52: Standard playing cards (1-13 = Clubs, 14-26 = Diamonds,
99    ///   27-39 = Hearts, 40-52 = Spades)
100    /// - Card 53: Joker A
101    /// - Card 54: Joker B
102    ///
103    /// # Arguments
104    ///
105    /// * `deck` - A vector of exactly 54 unique bytes representing the deck order
106    ///
107    /// # Panics
108    ///
109    /// Panics if the deck length is not exactly 54 cards.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use old_crypto_rs::Solitaire;
115    ///
116    /// // Create with a custom deck order
117    /// let mut custom_deck: Vec<u8> = (1..=54).collect();
118    /// // Shuffle or reorder as needed
119    /// let cipher = Solitaire::new(custom_deck);
120    /// ```
121    /// 
122    pub fn new(deck: Vec<u8>) -> Self {
123        assert_eq!(deck.len(), 54);
124        Solitaire {
125            initial_deck: deck.clone(),
126            deck: RefCell::new(deck),
127        }
128    }
129
130    /// Creates a Solitaire cipher with an unkeyed (sorted) deck.
131    ///
132    /// This initializes the deck in standard order (1, 2, 3, ..., 54).
133    /// An unkeyed deck provides no security and is primarily used for testing
134    /// and demonstrating the algorithm's operation.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use old_crypto_rs::Solitaire;
140    /// use old_crypto_rs::Block;
141    ///
142    /// let cipher = Solitaire::new_unkeyed();
143    /// let mut output = vec![0u8; 5];
144    /// cipher.encrypt(&mut output, b"HELLO");
145    /// ```
146    ///
147    /// # Security Warning
148    ///
149    /// Never use an unkeyed deck for actual encryption as it provides no security.
150    /// 
151    pub fn new_unkeyed() -> Self {
152        let deck: Vec<u8> = (1..=54).collect();
153        Self::new(deck)
154    }
155
156    /// Creates a Solitaire cipher with a deck keyed by a passphrase.
157    ///
158    /// The passphrase is used to shuffle the initially sorted deck through a series
159    /// of deck transformations. Each alphabetic character in the passphrase (case-insensitive)
160    /// contributes to the deck's final permutation. Non-alphabetic characters are ignored.
161    ///
162    /// # Arguments
163    ///
164    /// * `passphrase` - A string used to key the deck. Only ASCII alphabetic characters
165    ///   are processed; all others are skipped.
166    ///
167    /// # Algorithm
168    ///
169    /// For each alphabetic character in the passphrase:
170    /// 1. Convert to uppercase and get its position (A=1, B=2, ..., Z=26)
171    /// 2. Advance the deck through one complete cycle (steps 1-4)
172    /// 3. Perform a count cut using the character's numeric value
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use old_crypto_rs::Solitaire;
178    /// use old_crypto_rs::Block;
179    ///
180    /// // Passphrase can contain spaces and punctuation (ignored)
181    /// let cipher = Solitaire::new_with_passphrase("My Secret Key!");
182    ///
183    /// // Same as above (non-alpha characters ignored)
184    /// let cipher2 = Solitaire::new_with_passphrase("MYSECRETKEY");
185    /// ```
186    /// 
187    pub fn new_with_passphrase(passphrase: &str) -> Self {
188        let mut deck: Vec<u8> = (1..=54).collect();
189        for ch in passphrase.chars() {
190            if !ch.is_ascii_alphabetic() {
191                continue;
192            }
193            let val = (ch.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
194            Self::advance_deck(&mut deck);
195            Self::count_cut(&mut deck, val);
196        }
197        Self::new(deck)
198    }
199
200    /// Generates the next keystream value from the deck.
201    ///
202    /// This method performs one complete cycle of the Solitaire algorithm to produce
203    /// a single keystream byte. If a joker is generated as output, the process repeats
204    /// until a valid card (1-52) is produced.
205    ///
206    /// # Arguments
207    ///
208    /// * `deck` - A mutable reference to the current deck state
209    ///
210    /// # Returns
211    ///
212    /// A value between 1 and 26 representing the keystream output for this step.
213    /// Cards 1-26 map directly to 1-26, while cards 27-52 are reduced modulo 26
214    /// (i.e., card 27 becomes 1, card 28 becomes 2, etc.).
215    ///
216    /// # Algorithm Steps
217    ///
218    /// 1. Advance the deck through all transformation steps
219    /// 2. Look at the top card's value to determine a count position
220    /// 3. Output the card at that position (treating jokers as value 53)
221    /// 4. If output is a joker, repeat from step 1
222    /// 5. Convert the output card to a value in range 1-26
223    /// 
224    fn step(deck: &mut Vec<u8>) -> u8 {
225        loop {
226            Self::advance_deck(deck);
227
228            // Step 5: Generate output card
229            // Use the top card's value to index into the deck
230            let first_val = deck[0];
231            let count = if first_val > 52 { 53 } else { first_val } as usize;
232            let output_card = deck[count];
233
234            if output_card <= 52 {
235                // Convert card value (1-52) to keystream value (1-26)
236                // Cards 1-26 stay as-is, cards 27-52 wrap around
237                return if output_card > 26 { output_card - 26 } else { output_card };
238            }
239            // If output card is a joker, discard and repeat the entire process
240        }
241    }
242
243    /// Advances the deck through one complete transformation cycle.
244    ///
245    /// This performs the four main steps of the Solitaire algorithm that transform
246    /// the deck state. These steps must be executed in order:
247    ///
248    /// 1. **Move Joker A down**: Move card 53 one position toward the bottom
249    /// 2. **Move Joker B down**: Move card 54 two positions toward the bottom
250    /// 3. **Triple cut**: Swap the cards above the first joker with the cards below the second joker
251    /// 4. **Count cut**: Cut the deck based on the bottom card's value
252    ///
253    /// # Arguments
254    ///
255    /// * `deck` - A mutable reference to the deck to be transformed
256    ///
257    /// # Details
258    ///
259    /// The triple cut treats the two jokers as boundaries, dividing the deck into
260    /// three sections: top (before first joker), middle (between jokers, inclusive),
261    /// and bottom (after second joker). The top and bottom sections are swapped.
262    ///
263    /// The count cut uses the value of the bottom card to determine how many cards
264    /// to move from the top to just above the bottom card.
265    /// 
266    fn advance_deck(deck: &mut Vec<u8>) {
267        // Step 1: Move Joker A (card 53) one position down
268        // If at the bottom, it wraps to position 1 (not 0)
269        Self::move_joker(deck, 53, 1);
270
271        // Step 2: Move Joker B (card 54) two positions down
272        // If at or near the bottom, it wraps around similarly
273        Self::move_joker(deck, 54, 2);
274
275        // Step 3: Triple cut
276        // Find positions of both jokers
277        let pos_a = deck.iter().position(|&x| x == 53).unwrap();
278        let pos_b = deck.iter().position(|&x| x == 54).unwrap();
279
280        // Determine which joker is closer to the top
281        let (top_j, bot_j) = if pos_a < pos_b {
282            (pos_a, pos_b)
283        } else {
284            (pos_b, pos_a)
285        };
286
287        // Rebuild deck: [bottom section] + [middle section with jokers] + [top section]
288        let mut new_deck = Vec::with_capacity(54);
289        new_deck.extend_from_slice(&deck[bot_j + 1..]); // Cards after bottom joker
290        new_deck.extend_from_slice(&deck[top_j..bot_j + 1]); // Both jokers and cards between
291        new_deck.extend_from_slice(&deck[..top_j]); // Cards before top joker
292        *deck = new_deck;
293
294        // Step 4: Count cut
295        // Use the bottom card's value to determine cut position
296        let last_val = deck[53];
297        let count = if last_val > 52 { 53 } else { last_val } as usize;
298        Self::count_cut(deck, count);
299    }
300
301    /// Performs a count cut on the deck.
302    ///
303    /// This operation moves the top `count` cards to just above the bottom card,
304    /// which always remains at the bottom of the deck. If count is 53 or greater,
305    /// no operation is performed (the deck remains unchanged).
306    ///
307    /// # Arguments
308    ///
309    /// * `deck` - A mutable reference to the deck to be cut
310    /// * `count` - The number of cards to cut from the top (0-53)
311    ///
312    /// # Algorithm
313    ///
314    /// Given a deck like `[A B C D E F ... Y Z]` and count=3:
315    /// - Before: `[A B C D E ... Y Z]`
316    /// - After:  `[D E ... Y A B C Z]`
317    ///
318    /// The bottom card (Z) never moves.
319    /// 
320    fn count_cut(deck: &mut Vec<u8>, count: usize) {
321        if count < 53 {
322            let last_val = deck[53];
323            let mut new_deck = Vec::with_capacity(54);
324            new_deck.extend_from_slice(&deck[count..53]);
325            new_deck.extend_from_slice(&deck[..count]);
326            new_deck.push(last_val);
327            *deck = new_deck;
328        }
329    }
330
331    /// Moves a joker card down through the deck by a specified number of positions.
332    ///
333    /// The joker moves toward the bottom of the deck. When it reaches the bottom position
334    /// (index 53), the next move wraps it around to position 1 (not position 0, which would
335    /// make it the top card). This wrap-around behavior treats the deck as circular for
336    /// joker movement purposes.
337    ///
338    /// # Arguments
339    ///
340    /// * `deck` - A mutable reference to the deck
341    /// * `joker` - The value of the joker to move (either 53 or 54)
342    /// * `n` - The number of positions to move down
343    ///
344    /// # Examples
345    ///
346    /// Moving Joker A one position: `[... A B ...]` becomes `[... B A ...]`
347    /// Moving Joker at bottom: `[X Y ... Z A]` becomes `[X A Y ... Z]` (A wraps to position 1)
348    /// 
349    fn move_joker(deck: &mut Vec<u8>, joker: u8, n: usize) {
350        for _ in 0..n {
351            let pos = deck.iter().position(|&x| x == joker).unwrap();
352            if pos == 53 {
353                // Joker is at the bottom; wrap to position 1 (not 0)
354                let card = deck.remove(53);
355                deck.insert(1, card);
356            } else {
357                // Swap with the next card down
358                deck.swap(pos, pos + 1);
359            }
360        }
361    }
362
363    /// Resets the deck to its initial state.
364    ///
365    /// This method restores the deck to the configuration it had when the cipher
366    /// was first created. This is essential for ensuring that encryption and decryption
367    /// use the same keystream, as both operations start from the same initial deck state.
368    ///
369    /// # Implementation Note
370    ///
371    /// The deck state is maintained in a `RefCell` to allow interior mutability
372    /// while keeping the `encrypt` and `decrypt` methods with `&self` receivers
373    /// (required by the `Block` trait).
374    /// 
375    fn reset(&self) {
376        *self.deck.borrow_mut() = self.initial_deck.clone();
377    }
378}
379
380impl Block for Solitaire {
381    fn block_size(&self) -> usize {
382        1
383    }
384
385    /// Encrypts plaintext using the Solitaire cipher algorithm.
386    ///
387    /// This method generates a keystream by advancing the deck state and combines it
388    /// with the plaintext to produce ciphertext. The deck is reset to its initial state
389    /// before encryption begins, ensuring consistent keystream generation.
390    ///
391    /// # Arguments
392    ///
393    /// * `dst` - A mutable byte slice where the encrypted output will be written.
394    ///           Must be at least as long as `src`.
395    /// * `src` - A byte slice containing the plaintext to encrypt.
396    ///           Only ASCII alphabetic characters are encrypted; all others pass through unchanged.
397    ///
398    /// # Returns
399    ///
400    /// The number of bytes written to `dst` (always equal to `src.len()`).
401    ///
402    /// # Algorithm
403    ///
404    /// For each character in the source:
405    /// - If alphabetic: Convert to uppercase, generate keystream byte, add values (mod 26)
406    /// - If non-alphabetic: Copy unchanged to destination
407    ///
408    /// The encryption formula is: `C = (P + K - 1) mod 26 + 1`, where:
409    /// - P is the plaintext character value (A=1, B=2, ..., Z=26)
410    /// - K is the keystream value (1-26)
411    /// - C is the resulting ciphertext character value
412    ///
413    /// # Examples
414    ///
415    /// ```
416    /// use old_crypto_rs::Solitaire;
417    /// use old_crypto_rs::Block;
418    ///
419    /// let cipher = Solitaire::new_unkeyed();
420    /// let plaintext = b"HELLO";
421    /// let mut ciphertext = vec![0u8; plaintext.len()];
422    /// cipher.encrypt(&mut ciphertext, plaintext);
423    /// // ciphertext now contains the encrypted result
424    /// ```
425    ///
426    /// # Notes
427    ///
428    /// - Input case is normalized to uppercase
429    /// - Non-alphabetic characters (spaces, punctuation, digits) are preserved as-is
430    /// - The deck is reset before encryption, so multiple calls produce the same output
431    ///
432    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
433        // Reset deck to the initial state for consistent keystream generation
434        self.reset();
435
436        for (i, &ch) in src.iter().enumerate() {
437            if ch.is_ascii_alphabetic() {
438                // Convert plaintext character to number (A=1, B=2, ..., Z=26)
439                let p = (ch.to_ascii_uppercase() - b'A' + 1) as u8;
440
441                // Generate next keystream value (1-26)
442                let k = Self::step(&mut self.deck.borrow_mut());
443
444                // Add plaintext and keystream values (with modulo 26 wrap-around)
445                // Formula: C = (P + K - 1) mod 26 + 1
446                let c = (p + k - 1) % 26 + 1;
447
448                // Convert back to ASCII uppercase letter
449                dst[i] = c + b'A' - 1;
450            } else {
451                // Non-alphabetic characters pass through unchanged
452                dst[i] = ch;
453            }
454        }
455        src.len()
456    }
457
458    /// Decrypts ciphertext using the Solitaire cipher algorithm.
459    ///
460    /// This method generates the same keystream used during encryption and subtracts it
461    /// from the ciphertext to recover the original plaintext. The deck is reset to its
462    /// initial state before decryption begins, ensuring the keystream matches encryption.
463    ///
464    /// # Arguments
465    ///
466    /// * `dst` - A mutable byte slice where the decrypted output will be written.
467    ///           Must be at least as long as `src`.
468    /// * `src` - A byte slice containing the ciphertext to decrypt.
469    ///           Only ASCII alphabetic characters are decrypted; all others pass through unchanged.
470    ///
471    /// # Returns
472    ///
473    /// The number of bytes written to `dst` (always equal to `src.len()`).
474    ///
475    /// # Algorithm
476    ///
477    /// For each character in the source:
478    /// - If alphabetic: Convert to uppercase, generate keystream byte, subtract values (mod 26)
479    /// - If non-alphabetic: Copy unchanged to destination
480    ///
481    /// The decryption formula is: `P = (C - K) mod 26`, where:
482    /// - C is the ciphertext character value (A=1, B=2, ..., Z=26)
483    /// - K is the keystream value (1-26)
484    /// - P is the resulting plaintext character value
485    ///
486    /// Special handling ensures proper modulo arithmetic for negative results.
487    ///
488    /// # Examples
489    ///
490    /// ```
491    /// use old_crypto_rs::Solitaire;
492    /// use old_crypto_rs::Block;
493    ///
494    /// let cipher = Solitaire::new_unkeyed();
495    /// let ciphertext = b"EXKYI";
496    /// let mut plaintext = vec![0u8; ciphertext.len()];
497    /// cipher.decrypt(&mut plaintext, ciphertext);
498    /// assert_eq!(&plaintext, b"AAAAA");
499    /// ```
500    ///
501    /// # Notes
502    ///
503    /// - Must use the same deck configuration (initial state) as encryption
504    /// - Input case is normalized to uppercase
505    /// - Non-alphabetic characters are preserved unchanged
506    /// - The deck is reset before decryption to generate the correct keystream
507    ///
508    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
509        // Reset deck to initial state for consistent keystream generation
510        self.reset();
511
512        for (i, &ch) in src.iter().enumerate() {
513            if ch.is_ascii_alphabetic() {
514                // Convert ciphertext character to number (A=1, B=2, ..., Z=26)
515                let c = (ch.to_ascii_uppercase() - b'A' + 1) as u8;
516
517                // Generate next keystream value (1-26) - must match encryption
518                let k = Self::step(&mut self.deck.borrow_mut());
519
520                // Subtract keystream from ciphertext (with modulo 26 wrap-around)
521                // Formula: P = (C - K) mod 26, handling negative results
522                let p = if c > k { c - k } else { c + 26 - k };
523
524                // Convert back to ASCII uppercase letter
525                dst[i] = p + b'A' - 1;
526            } else {
527                // Non-alphabetic characters pass through unchanged
528                dst[i] = ch;
529            }
530        }
531        src.len()
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[test]
540    fn test_solitaire_keystream() {
541        let s = Solitaire::new_unkeyed();
542        let mut deck = s.initial_deck.clone();
543        let mut actual = Vec::new();
544        for _ in 0..10 {
545            actual.push(Solitaire::step(&mut deck));
546        }
547        // Wikipedia says for unkeyed deck: 4, 49, 10, (skip 53), 24, 8, 51, 44, 6, 33, 10...
548        // However, standard implementations (including this one) diverge slightly after 8 steps.
549        // We match up to the 8th value.
550        let expected = [4, 23, 10, 24, 8, 25, 18, 6, 4, 7];
551        assert_eq!(actual, expected);
552    }
553
554    #[test]
555    fn test_solitaire_wikipedia_example_1() {
556        // Example: Plaintext "AAAAA" with unkeyed deck -> "EXKYI"
557        let s = Solitaire::new_unkeyed();
558        let src = b"AAAAA";
559        let mut dst = vec![0u8; src.len()];
560        s.encrypt(&mut dst, src);
561        assert_eq!(std::str::from_utf8(&dst).unwrap(), "EXKYI");
562    }
563
564    #[test]
565    fn test_solitaire_wikipedia_example_2() {
566        // Example: Plaintext "AAAAAAAAAA" with unkeyed deck -> "EXKYIZSGEH"
567        // (Wikipedia says "EXKYIZSUIK", but that seems inconsistent with their own keystream)
568        let s = Solitaire::new_unkeyed();
569        let src = b"AAAAAAAAAA";
570        let mut dst = vec![0u8; src.len()];
571        s.encrypt(&mut dst, src);
572        assert_eq!(std::str::from_utf8(&dst).unwrap(), "EXKYIZSGEH");
573    }
574
575    #[test]
576    fn test_solitaire_wikipedia_example_3() {
577        // Round trip test
578        let s = Solitaire::new_unkeyed();
579        let src = b"EXKYIZSGEH";
580        let mut dst = vec![0u8; src.len()];
581        s.decrypt(&mut dst, src);
582        assert_eq!(std::str::from_utf8(&dst).unwrap(), "AAAAAAAAAA");
583    }
584
585    #[test]
586    fn test_solitaire_wikipedia_example_4() {
587        // Example: "CLEAN" with unkeyed deck -> "GIOYV"
588        let s = Solitaire::new_unkeyed();
589        let src = b"CLEAN";
590        let mut dst = vec![0u8; src.len()];
591        s.encrypt(&mut dst, src);
592        assert_eq!(std::str::from_utf8(&dst).unwrap(), "GIOYV");
593        
594        let mut dec = vec![0u8; src.len()];
595        s.decrypt(&mut dec, &dst);
596        assert_eq!(std::str::from_utf8(&dec).unwrap(), "CLEAN");
597    }
598
599    #[test]
600    fn test_solitaire_extra_examples() {
601        let tests = [
602            ("", "AAAAAAAAAAAAAAA", "EXKYIZSGEHUNTIQ"),
603            ("f", "AAAAAAAAAAAAAAA", "XYIUQBMHKKJBEGY"),
604            ("fo", "AAAAAAAAAAAAAAA", "TUJYMBERLGXNDIW"),
605            ("foo", "AAAAAAAAAAAAAAA", "ITHZUJIWGRFARMW"),
606            ("a", "AAAAAAAAAAAAAAA", "XODALGSCULIQNSC"),
607            ("aa", "AAAAAAAAAAAAAAA", "OHGWMXXCAIMCIQP"),
608            ("aaa", "AAAAAAAAAAAAAAA", "DCSQYHBQZNGDRUT"),
609            ("b", "AAAAAAAAAAAAAAA", "XQEEMOITLZVDSQS"),
610            ("bc", "AAAAAAAAAAAAAAA", "QNGRKQIHCLGWSCE"),
611            ("bcd", "AAAAAAAAAAAAAAA", "FMUBYBMAXHNQXCJ"),
612            ("cryptonomicon", "AAAAAAAAAAAAAAAAAAAAAAAAA", "SUGSRSXSWQRMXOHIPBFPXARYQ"),
613            ("cryptonomicon", "SOLITAIRE", "KIRAKSFJA"),
614        ];
615
616        for (pass, pt, ct) in tests {
617            let s = if pass.is_empty() {
618                Solitaire::new_unkeyed()
619            } else {
620                Solitaire::new_with_passphrase(pass)
621            };
622            let mut dst = vec![0u8; pt.len()];
623            s.encrypt(&mut dst, pt.as_bytes());
624            assert_eq!(std::str::from_utf8(&dst).unwrap(), ct, "Failed for key '{}'", pass);
625        }
626    }
627}