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}