old_crypto_rs/
nihilist.rs

1//! Nihilist cipher implementation.
2//!
3//! The Nihilist cipher is a classical encryption method that combines a straddling checkerboard
4//! with columnar transposition for super-encipherment. It was historically used by Russian
5//! Nihilist revolutionaries in the 1880s.
6//!
7//! # Examples
8//!
9//! ```
10//! use old_crypto_rs::{Block, Nihilist};
11//!
12//! let cipher = Nihilist::new("ARABESQUE", "SUBWAY", "37").unwrap();
13//! let plaintext = b"IFYOUCANREADTHIS";
14//! let mut ciphertext = vec![0u8; 22];
15//!
16//! cipher.encrypt(&mut ciphertext, plaintext);
17//! ```
18
19use crate::Block;
20use crate::straddling::StraddlingCheckerboard;
21use crate::transposition::Transposition;
22
23use eyre::Result;
24
25/// Nihilist cipher combining straddling checkerboard and transposition.
26///
27/// This cipher performs a two-stage encryption:
28/// 1. Converts plaintext to digits using a straddling checkerboard
29/// 2. Applies columnar transposition to the resulting digits
30///
31/// The decryption process reverses these steps in opposite order.
32pub struct Nihilist {
33    /// Straddling checkerboard for initial text-to-digits conversion
34    sc: StraddlingCheckerboard,
35    /// Transposition cipher for super-encipherment
36    transp: Transposition,
37}
38
39impl Nihilist {
40    /// Creates a new Nihilist cipher with the specified keys.
41    ///
42    /// # Arguments
43    ///
44    /// * `key1` - The key for the straddling checkerboard (e.g., "ARABESQUE")
45    /// * `key2` - The key for the transposition cipher (e.g., "SUBWAY")
46    /// * `chrs` - Two characters defining the blank positions in the checkerboard (e.g., "37")
47    ///
48    /// # Returns
49    ///
50    /// Returns `Ok(Nihilist)` if both keys are valid, or `Err(String)` with an error message
51    /// if either key is invalid or empty.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use old_crypto_rs::Nihilist;
57    ///
58    /// let cipher = Nihilist::new("ARABESQUE", "SUBWAY", "37").unwrap();
59    /// ```
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if:
64    /// - `key1` is empty or invalid for the straddling checkerboard
65    /// - `key2` is empty or invalid for the transposition cipher
66    /// - `chrs` is not exactly two characters or contains invalid positions
67    pub fn new(key1: &str, key2: &str, chrs: &str) -> Result<Self> {
68        let sc = StraddlingCheckerboard::new(key1, chrs)?;
69        let transp = Transposition::new(key2)?;
70
71        Ok(Nihilist {
72            sc,
73            transp,
74        })
75    }
76}
77
78impl Block for Nihilist {
79    /// Returns the block size of the cipher.
80    ///
81    /// The block size is determined by the transposition cipher's key length.
82    ///
83    /// # Returns
84    ///
85    /// The number of characters that can be processed in one block.
86    ///
87    fn block_size(&self) -> usize {
88        self.transp.block_size()
89    }
90
91    /// Encrypts the source data into the destination buffer.
92    ///
93    /// The encryption process:
94    /// 1. Converts plaintext to digits using the straddling checkerboard
95    /// 2. Applies columnar transposition to the resulting digits
96    ///
97    /// # Arguments
98    ///
99    /// * `dst` - Destination buffer for the ciphertext (must be large enough)
100    /// * `src` - Source plaintext to encrypt
101    ///
102    /// # Returns
103    ///
104    /// The number of bytes written to `dst`.
105    ///
106    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
107        let mut buf = vec![0u8; 2 * src.len()];
108        let n = self.sc.encrypt(&mut buf, src);
109        self.transp.encrypt(dst, &buf[..n])
110    }
111
112    /// Decrypts the source ciphertext into the destination buffer.
113    ///
114    /// The decryption process:
115    /// 1. Reverses the columnar transposition
116    /// 2. Converts the resulting digits back to plaintext using the straddling checkerboard
117    ///
118    /// # Arguments
119    ///
120    /// * `dst` - Destination buffer for the plaintext (must be large enough)
121    /// * `src` - Source ciphertext to decrypt
122    ///
123    /// # Returns
124    ///
125    /// The number of bytes written to `dst`.
126    /// 
127    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
128        let mut buf = vec![0u8; src.len()];
129        let n = self.transp.decrypt(&mut buf, src);
130        self.sc.decrypt(dst, &buf[..n])
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_new_cipher() {
140        let c = Nihilist::new("ARABESQUE", "SUBWAY", "37").unwrap();
141        assert_eq!(c.block_size(), 6);
142    }
143
144    #[test]
145    fn test_new_cipher_bad_keys() {
146        assert!(Nihilist::new("PORTABLE", "", "89").is_err());
147        assert!(Nihilist::new("", "SUBWAY", "62").is_err());
148    }
149
150    #[test]
151    fn test_nihilist_encrypt() {
152        let c = Nihilist::new("ARABESQUE", "SUBWAY", "37").unwrap();
153        let pt = "IFYOUCANREADTHIS";
154        let ct = "1037306631738227035749";
155        let mut dst = vec![0u8; ct.len()];
156        c.encrypt(&mut dst, pt.as_bytes());
157        assert_eq!(String::from_utf8_lossy(&dst), ct);
158    }
159
160    #[test]
161    fn test_nihilist_decrypt() {
162        let c = Nihilist::new("ARABESQUE", "SUBWAY", "37").unwrap();
163        let pt = "IFYOUCANREADTHIS";
164        let ct = "1037306631738227035749";
165        let mut dst = vec![0u8; pt.len()];
166        c.decrypt(&mut dst, ct.as_bytes());
167        assert_eq!(String::from_utf8_lossy(&dst).trim_matches('\0'), pt);
168    }
169}