old_crypto_rs/wheatstone.rs
1//! Implementation of the Wheatstone cipher machine.
2//!
3//! The Wheatstone cipher (also known as the Playfair Double) is a mechanical cipher
4//! device invented by Charles Wheatstone in the 1860s. It uses two rotating alphabetic
5//! wheels - one for plaintext (with 27 positions including a special character) and
6//! one for ciphertext (with 26 positions) - along with a pointer mechanism to
7//! encrypt and decrypt messages.
8//!
9//! # Example
10//!
11//! ```no_run
12//! use old_crypto_rs::{Block, Wheatstone};
13//!
14//! let cipher = Wheatstone::new(b'M', "CIPHER", "MACHINE").unwrap();
15//! let plaintext = b"HELLO";
16//! let mut ciphertext = vec![0u8; plaintext.len()];
17//! cipher.encrypt(&mut ciphertext, plaintext);
18//! ```
19
20use std::cell::RefCell;
21
22use crate::Block;
23use crate::helpers;
24use crate::helpers::{fix_double, REGULAR_ALPHABET};
25
26use eyre::Result;
27use crate::error::Error;
28
29const LEN_PL: usize = REGULAR_ALPHABET.len() + 1;
30const LEN_CT: usize = REGULAR_ALPHABET.len();
31
32/// Wheatstone cipher machine implementation.
33///
34/// This struct represents a Wheatstone cipher with two keyed alphabets:
35/// - A plaintext wheel (27 characters, including '+' as a separator)
36/// - A ciphertext wheel (26 characters, standard alphabet)
37///
38/// The cipher maintains internal state to track the current positions
39/// of both wheels during encryption and decryption operations.
40pub struct Wheatstone {
41 /// Plaintext wheel alphabet (27 characters)
42 aplw: Vec<u8>,
43 /// Ciphertext wheel alphabet (26 characters)
44 actw: Vec<u8>,
45 /// Starting character position on the ciphertext wheel
46 start: u8,
47 /// Internal mutable state for wheel positions
48 state: RefCell<WheatstoneState>,
49}
50
51/// Internal state for tracking wheel positions during encryption/decryption.
52///
53/// The Wheatstone cipher needs to maintain state between character operations
54/// as each encryption/decryption affects the position of the wheels for the
55/// next character.
56struct WheatstoneState {
57 /// Current position on the plaintext wheel (0-26)
58 curpos: usize,
59 /// Current position on the ciphertext wheel (0-25)
60 ctpos: usize,
61}
62
63impl Wheatstone {
64 /// Creates a new Wheatstone cipher with the provided configuration.
65 ///
66 /// # Arguments
67 ///
68 /// * `start` - The starting character on the ciphertext wheel (typically 'A'-'Z')
69 /// * `pkey` - The plaintext key used to shuffle the plaintext alphabet
70 /// * `ckey` - The ciphertext key used to shuffle the ciphertext alphabet
71 ///
72 /// # Returns
73 ///
74 /// Returns `Ok(Wheatstone)` if the cipher is successfully created, or
75 /// `Err(String)` if either key is empty.
76 ///
77 /// # Example
78 ///
79 /// ```no_run
80 /// use old_crypto_rs::Wheatstone;
81 ///
82 /// let cipher = Wheatstone::new(b'M', "CIPHER", "MACHINE").unwrap();
83 /// ```
84 ///
85 pub fn new(start: u8, pkey: &str, ckey: &str) -> Result<Self> {
86 if pkey.is_empty() || ckey.is_empty() {
87 return Err(Error::EmptyKeys.into());
88 }
89
90 // Transform with key
91 let pkey_shuffled = format!("+{}", helpers::shuffle(pkey, REGULAR_ALPHABET));
92 let ckey_shuffled = helpers::shuffle(ckey, REGULAR_ALPHABET);
93
94 let aplw = pkey_shuffled.as_bytes().to_vec();
95 let actw = ckey_shuffled.as_bytes().to_vec();
96
97 let ctpos = actw.iter().position(|&x| x == start).unwrap_or(0);
98
99 Ok(Wheatstone {
100 aplw,
101 actw,
102 start,
103 state: RefCell::new(WheatstoneState {
104 curpos: 0,
105 ctpos,
106 }),
107 })
108 }
109
110 /// Encodes a single character using the Wheatstone cipher mechanism.
111 ///
112 /// This method finds the character on the plaintext wheel, calculates
113 /// the offset from the current position, advances both wheels by this
114 /// offset, and returns the character at the new ciphertext wheel position.
115 ///
116 /// # Arguments
117 ///
118 /// * `ch` - The plaintext character to encode (as a byte)
119 ///
120 /// # Returns
121 ///
122 /// The encoded ciphertext character (as a byte)
123 ///
124 fn encode(&self, ch: u8) -> u8 {
125 let mut state = self.state.borrow_mut();
126 let a = self.aplw.iter().position(|&x| x == ch).unwrap_or(0);
127 let off = if a <= state.curpos {
128 (a + LEN_PL) - state.curpos
129 } else {
130 a - state.curpos
131 };
132 state.curpos = a;
133 state.ctpos = (state.ctpos + off) % LEN_CT;
134 self.actw[state.ctpos]
135 }
136
137 /// Decodes a single character using the Wheatstone cipher mechanism.
138 ///
139 /// This method finds the character on the ciphertext wheel, calculates
140 /// the offset from the current position, advances both wheels by this
141 /// offset, and returns the character at the new plaintext wheel position.
142 ///
143 /// # Arguments
144 ///
145 /// * `ch` - The ciphertext character to decode (as a byte)
146 ///
147 /// # Returns
148 ///
149 /// The decoded plaintext character (as a byte)
150 ///
151 fn decode(&self, ch: u8) -> u8 {
152 let mut state = self.state.borrow_mut();
153 let a = self.actw.iter().position(|&x| x == ch).unwrap_or(0);
154 let off = if a <= state.ctpos {
155 (a + LEN_CT) - state.ctpos
156 } else {
157 a - state.ctpos
158 };
159 state.ctpos = a;
160 state.curpos = (state.curpos + off) % LEN_PL;
161 self.aplw[state.curpos]
162 }
163
164 /// Resets the cipher state to the initial configuration.
165 ///
166 /// This method resets both wheel positions to their starting values:
167 /// - Plaintext wheel position to 0
168 /// - Ciphertext wheel position to the location of the start character
169 ///
170 /// This is called automatically before each encrypt/decrypt operation
171 /// to ensure consistent results.
172 ///
173 fn reset(&self) {
174 let mut state = self.state.borrow_mut();
175 state.curpos = 0;
176 state.ctpos = self.actw.iter().position(|&x| x == self.start).unwrap_or(0);
177 }
178}
179
180impl Block for Wheatstone {
181 fn block_size(&self) -> usize {
182 1
183 }
184
185 /// Encrypts plaintext using the Wheatstone cipher.
186 ///
187 /// This method encrypts the entire source buffer character by character,
188 /// writing the ciphertext to the destination buffer. The cipher state is
189 /// automatically reset before encryption to ensure consistent results.
190 ///
191 /// # Arguments
192 ///
193 /// * `dst` - Mutable slice where the ciphertext will be written (must be at least as long as `src`)
194 /// * `src` - Slice containing the plaintext to encrypt
195 ///
196 /// # Returns
197 ///
198 /// The number of bytes encrypted (equal to `src.len()`)
199 ///
200 /// # Example
201 ///
202 /// ```no_run
203 /// use old_crypto_rs::{Block, Wheatstone};
204 ///
205 /// let cipher = Wheatstone::new(b'M', "CIPHER", "MACHINE").unwrap();
206 /// let plaintext = b"HELLO";
207 /// let mut ciphertext = vec![0u8; plaintext.len()];
208 /// let len = cipher.encrypt(&mut ciphertext, plaintext);
209 /// assert_eq!(len, plaintext.len());
210 /// ```
211 ///
212 fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
213 self.reset();
214 let src = fix_double(&String::from_utf8_lossy(src), 'Q');
215 for (i, &ch) in src.as_bytes().iter().enumerate() {
216 dst[i] = self.encode(ch);
217 }
218 src.len()
219 }
220
221 /// Decrypts ciphertext using the Wheatstone cipher.
222 ///
223 /// This method decrypts the entire source buffer character by character,
224 /// writing the plaintext to the destination buffer. The cipher state is
225 /// automatically reset before decryption to ensure consistent results.
226 ///
227 /// # Arguments
228 ///
229 /// * `dst` - Mutable slice where the plaintext will be written (must be at least as long as `src`)
230 /// * `src` - Slice containing the ciphertext to decrypt
231 ///
232 /// # Returns
233 ///
234 /// The number of bytes decrypted (equal to `src.len()`)
235 ///
236 /// # Example
237 ///
238 /// ```no_run
239 /// use old_crypto_rs::{Block, Wheatstone};
240 ///
241 /// let cipher = Wheatstone::new(b'M', "CIPHER", "MACHINE").unwrap();
242 /// let ciphertext = b"BYVLQ";
243 /// let mut plaintext = vec![0u8; ciphertext.len()];
244 /// let len = cipher.decrypt(&mut plaintext, ciphertext);
245 /// assert_eq!(len, ciphertext.len());
246 /// ```
247 ///
248 fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
249 self.reset();
250 for (i, &ch) in src.iter().enumerate() {
251 dst[i] = self.decode(ch);
252 }
253 src.len()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 const PLAIN_TXT: &str = "CHARLES+WHEATSTONE+HAD+A+REMARKABLY+FERTILE+MIND";
262 const CIPHER_TXT: &str = "BYVLQKWAMNLCYXIOUBFLHTXGHFPBJHZZLUEZFHIVBVRTFVRQ";
263 const KEY1: &str = "CIPHER";
264 const KEY2: &str = "MACHINE";
265 const LPLAIN_TXT: &str = "IFYOUCANREADTHISYOUEITHERDOWNLOADEDMYOWNIMPLEMENTATIONOFCHAOCIPHERORYOUWROTEONEOFYOUROWNINEITHERCASELETMEKNOWANDACCEPTMYCONGRATULATIONSX";
266 const LCIPHER_TXT: &str = "PIPTIADNMEWJYKGHGVEOIZUVWEPWVKCIMWBOKXHCLDAOGCRGPMWDNJKJVJDLDQYZEBMOXBKVAVSOABVDBJWBQFQPTWFEMPQRZNTXBHVWGLHIJLGFMMLBZHXYCDUTUOCYNYQJYABYX";
267
268 #[test]
269 fn test_new_wheatstone_cipher() {
270 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
271 assert_eq!(c.aplw.len(), 27);
272 assert_eq!(c.actw.len(), 26);
273 }
274
275 #[test]
276 fn test_wheatstone_encode() {
277 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
278 assert_eq!(c.state.borrow().curpos, 0);
279 assert_eq!(c.state.borrow().ctpos, 0);
280
281 // Round 1
282 assert_eq!(c.encode(b'C'), b'B');
283 assert_eq!(c.state.borrow().curpos, 1);
284 assert_eq!(c.state.borrow().ctpos, 1);
285
286 // Round 2
287 assert_eq!(c.encode(b'H'), b'Y');
288 assert_eq!(c.state.borrow().curpos, 15);
289 assert_eq!(c.state.borrow().ctpos, 15);
290
291 // Round 3
292 assert_eq!(c.encode(b'A'), b'V');
293 assert_eq!(c.state.borrow().curpos, 2);
294 assert_eq!(c.state.borrow().ctpos, 3);
295
296 // Round 4
297 assert_eq!(c.encode(b'R'), b'L');
298 assert_eq!(c.state.borrow().curpos, 23);
299 assert_eq!(c.state.borrow().ctpos, 24);
300 }
301
302 #[test]
303 fn test_wheatstone_decode() {
304 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
305 assert_eq!(c.state.borrow().curpos, 0);
306 assert_eq!(c.state.borrow().ctpos, 0);
307
308 assert_eq!(c.decode(b'B'), b'C');
309 assert_eq!(c.state.borrow().curpos, 1);
310 assert_eq!(c.state.borrow().ctpos, 1);
311
312 assert_eq!(c.decode(b'Y'), b'H');
313 assert_eq!(c.state.borrow().curpos, 15);
314 assert_eq!(c.state.borrow().ctpos, 15);
315
316 assert_eq!(c.decode(b'V'), b'A');
317 assert_eq!(c.state.borrow().curpos, 2);
318 assert_eq!(c.state.borrow().ctpos, 3);
319
320 assert_eq!(c.decode(b'L'), b'R');
321 assert_eq!(c.state.borrow().curpos, 23);
322 assert_eq!(c.state.borrow().ctpos, 24);
323 }
324
325 #[test]
326 fn test_wheatstone_encrypt() {
327 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
328 let src = PLAIN_TXT.as_bytes();
329 let mut dst = vec![0u8; src.len()];
330 c.encrypt(&mut dst, src);
331 assert_eq!(dst, CIPHER_TXT.as_bytes());
332 }
333
334 #[test]
335 fn test_wheatstone_encrypt_start_a() {
336 let c = Wheatstone::new(b'A', KEY1, KEY2).unwrap();
337 let src = PLAIN_TXT.as_bytes();
338 let mut dst = vec![0u8; src.len()];
339 c.encrypt(&mut dst, src);
340 assert_eq!(String::from_utf8_lossy(&dst), "DZWORUXCALOHZYNPVDGOIMYJIGQDKIEEOVBEGINWDWSMGWSR");
341 }
342
343 #[test]
344 fn test_wheatstone_encrypt_long() {
345 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
346 let plain = fix_double(LPLAIN_TXT, 'Q');
347 let src = LPLAIN_TXT.as_bytes();
348 let mut dst = vec![0u8; plain.len()];
349 c.encrypt(&mut dst, src);
350 assert_eq!(String::from_utf8_lossy(&dst), LCIPHER_TXT);
351 }
352
353 #[test]
354 fn test_wheatstone_decrypt() {
355 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
356 let src = CIPHER_TXT.as_bytes();
357 let mut dst = vec![0u8; src.len()];
358 c.decrypt(&mut dst, src);
359 assert_eq!(String::from_utf8_lossy(&dst), PLAIN_TXT);
360 }
361
362 #[test]
363 fn test_wheatstone_decrypt_long() {
364 let c = Wheatstone::new(b'M', KEY1, KEY2).unwrap();
365 let plain = fix_double(LPLAIN_TXT, 'Q');
366 let src = LCIPHER_TXT.as_bytes();
367 let mut dst = vec![0u8; src.len()];
368 c.decrypt(&mut dst, src);
369 assert_eq!(String::from_utf8_lossy(&dst), plain);
370 }
371}