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}