old_crypto_rs/
chaocipher.rs

1//! Chaocipher implementation.
2//!
3//! The Chaocipher is a cipher method invented by John Francis Byrne in 1918 and described in his
4//! 1953 autobiographical Silent Years. He believed Chaocipher was simple, yet unbreakable.
5//!
6//! The algorithm uses two alphabets (called the left and right alphabets, or cipher and plaintext
7//! alphabets) which are permuted after each character is processed. The permutation involves
8//! rotating both alphabets and performing specific shifts at fixed positions (zenith and nadir).
9//!
10//! # Example
11//!
12//! ```
13//! use old_crypto_rs::{Block, Chaocipher};
14//!
15//! let pkey = "PTLNBQDEOYSFAVZKGJRIHWXUMC";
16//! let ckey = "HXUCZVAMDSLKPEFJRIGTWOBNYQ";
17//! let cipher = Chaocipher::new(pkey, ckey).unwrap();
18//!
19//! let plaintext = b"HELLO";
20//! let mut ciphertext = vec![0u8; plaintext.len()];
21//! cipher.encrypt(&mut ciphertext, plaintext);
22//! ```
23//!
24use std::cell::RefCell;
25
26use crate::Block;
27use crate::helpers::REGULAR_ALPHABET;
28
29use eyre::Result;
30use crate::error::Error;
31
32const ZENITH: usize = 0;
33const NADIR: usize = 13;
34
35/// A Chaocipher instance with two permutation alphabets.
36///
37/// The `Chaocipher` struct maintains two 26-character alphabet keys (plaintext and cipher keys)
38/// and an internal state that is updated after each character encryption/decryption.
39/// The state is kept in a `RefCell` to allow interior mutability during const operations.
40///
41pub struct Chaocipher {
42    /// The plaintext alphabet key (right alphabet)
43    pkey: String,
44    /// The cipher alphabet key (left alphabet)
45    ckey: String,
46    /// Internal mutable state containing the working alphabets
47    state: RefCell<ChaocipherState>,
48}
49
50/// Internal state of the Chaocipher algorithm.
51///
52/// Contains the two working alphabets that are permuted after each character operation.
53///
54struct ChaocipherState {
55    /// The plaintext working alphabet (right alphabet)
56    pw: Vec<u8>,
57    /// The cipher working alphabet (left alphabet)
58    cw: Vec<u8>,
59}
60
61impl Chaocipher {
62    /// Creates a new Chaocipher instance with the provided keys.
63    ///
64    /// Both keys must be exactly 26 characters long (matching the standard English alphabet).
65    ///
66    /// # Arguments
67    ///
68    /// * `pkey` - The plaintext alphabet key (right alphabet)
69    /// * `ckey` - The cipher alphabet key (left alphabet)
70    ///
71    /// # Returns
72    ///
73    /// Returns `Ok(Chaocipher)` if the keys are valid, or `Err(String)` if either key
74    /// has an incorrect length.
75    ///
76    /// # Example
77    ///
78    /// ```
79    /// use old_crypto_rs::Chaocipher;
80    ///
81    /// let cipher = Chaocipher::new(
82    ///     "PTLNBQDEOYSFAVZKGJRIHWXUMC",
83    ///     "HXUCZVAMDSLKPEFJRIGTWOBNYQ"
84    /// ).unwrap();
85    /// ```
86    ///
87    pub fn new(pkey: &str, ckey: &str) -> Result<Self> {
88        if pkey.len() != REGULAR_ALPHABET.len() || ckey.len() != REGULAR_ALPHABET.len() {
89            return Err(Error::AlphabetTooShort(REGULAR_ALPHABET.len()).into());
90        }
91
92        Ok(Chaocipher {
93            pkey: pkey.to_string(),
94            ckey: ckey.to_string(),
95            state: RefCell::new(ChaocipherState {
96                pw: pkey.as_bytes().to_vec(),
97                cw: ckey.as_bytes().to_vec(),
98            }),
99        })
100    }
101
102    /// Performs a left circular shift on an alphabet by n positions.
103    ///
104    /// # Arguments
105    ///
106    /// * `a` - The alphabet slice to shift
107    /// * `n` - The number of positions to shift (will be taken modulo the alphabet length)
108    ///
109    fn lshift_n(a: &mut [u8], n: usize) {
110        if a.is_empty() { return; }
111        let n = n % a.len();
112        a.rotate_left(n);
113    }
114
115    /// Advances the internal state by permuting both alphabets.
116    ///
117    /// This is the core permutation step of the Chaocipher algorithm. After finding a character
118    /// at position `idx`, both alphabets are rotated and specific positions (between zenith and
119    /// nadir) are shifted to create the permutation.
120    ///
121    /// The cipher alphabet is shifted left by `idx` positions, then a character is extracted
122    /// and rotated within a specific range. The plaintext alphabet is shifted by `idx + 1`
123    /// positions with a similar extraction and rotation.
124    ///
125    /// # Arguments
126    ///
127    /// * `state` - The current cipher state to be modified
128    /// * `idx` - The position of the character that was just processed
129    ///
130    fn advance(state: &mut ChaocipherState, idx: usize) {
131        // First we shift the left alphabet (cw)
132        Self::lshift_n(&mut state.cw, idx);
133        let l = state.cw[ZENITH + 1];
134        state.cw[ZENITH + 1..NADIR + 1].rotate_left(1);
135        state.cw[NADIR] = l;
136
137        // Then we shift the right alphabet (pw)
138        Self::lshift_n(&mut state.pw, idx + 1);
139        let l = state.pw[ZENITH + 2];
140        state.pw[ZENITH + 2..NADIR + 1].rotate_left(1);
141        state.pw[NADIR] = l;
142    }
143
144    /// Encodes or decodes a single character.
145    ///
146    /// This method handles both encryption and decryption by finding the character in one
147    /// alphabet and returning the corresponding character from the other alphabet at the
148    /// same position. The state is then advanced for the next character.
149    ///
150    /// # Arguments
151    ///
152    /// * `is_encrypt` - If true, encrypts; if false, decrypts
153    /// * `ch` - The character to process
154    ///
155    /// # Returns
156    ///
157    /// The encrypted or decrypted character
158    ///
159    fn encode_both(&self, is_encrypt: bool, ch: u8) -> u8 {
160        let mut state = self.state.borrow_mut();
161        let idx = if is_encrypt {
162            state.pw.iter().position(|&x| x == ch).unwrap_or(0)
163        } else {
164            state.cw.iter().position(|&x| x == ch).unwrap_or(0)
165        };
166
167        let pt = if is_encrypt {
168            state.cw[idx]
169        } else {
170            state.pw[idx]
171        };
172
173        Self::advance(&mut state, idx);
174        pt
175    }
176
177    /// Resets the cipher state to the initial key configuration.
178    ///
179    /// This method restores both working alphabets to their original key values,
180    /// allowing the cipher to be reused for multiple messages.
181    ///
182    fn reset(&self) {
183        let mut state = self.state.borrow_mut();
184        state.pw = self.pkey.as_bytes().to_vec();
185        state.cw = self.ckey.as_bytes().to_vec();
186    }
187}
188
189impl Block for Chaocipher {
190    /// Returns the block size of the cipher.
191    ///
192    /// Chaocipher operates on single characters, so the block size is always 1.
193    ///
194    fn block_size(&self) -> usize {
195        1
196    }
197
198    /// Encrypts the source data into the destination buffer.
199    ///
200    /// The cipher state is reset before encryption begins, ensuring that each encryption
201    /// operation starts from the initial key configuration.
202    ///
203    /// # Arguments
204    ///
205    /// * `dst` - The destination buffer for encrypted data (must be at least as long as `src`)
206    /// * `src` - The source plaintext data to encrypt
207    ///
208    /// # Returns
209    ///
210    /// The number of bytes encrypted (equal to the length of `src`)
211    ///
212    /// # Example
213    ///
214    /// ```
215    /// use old_crypto_rs::{Block, Chaocipher};
216    ///
217    /// let cipher = Chaocipher::new(
218    ///     "PTLNBQDEOYSFAVZKGJRIHWXUMC",
219    ///     "HXUCZVAMDSLKPEFJRIGTWOBNYQ"
220    /// ).unwrap();
221    ///
222    /// let plaintext = b"HELLO";
223    /// let mut ciphertext = vec![0u8; plaintext.len()];
224    /// cipher.encrypt(&mut ciphertext, plaintext);
225    /// ```
226    ///
227    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
228        self.reset();
229        for (i, &ch) in src.iter().enumerate() {
230            dst[i] = self.encode_both(true, ch);
231        }
232        src.len()
233    }
234
235    /// Decrypts the source data into the destination buffer.
236    ///
237    /// The cipher state is reset before decryption begins, ensuring that each decryption
238    /// operation starts from the initial key configuration.
239    ///
240    /// # Arguments
241    ///
242    /// * `dst` - The destination buffer for decrypted data (must be at least as long as `src`)
243    /// * `src` - The source ciphertext data to decrypt
244    ///
245    /// # Returns
246    ///
247    /// The number of bytes decrypted (equal to the length of `src`)
248    ///
249    /// # Example
250    ///
251    /// ```
252    /// use old_crypto_rs::{Block, Chaocipher};
253    ///
254    /// let cipher = Chaocipher::new(
255    ///     "PTLNBQDEOYSFAVZKGJRIHWXUMC",
256    ///     "HXUCZVAMDSLKPEFJRIGTWOBNYQ"
257    /// ).unwrap();
258    ///
259    /// let ciphertext = b"OAHQH";
260    /// let mut plaintext = vec![0u8; ciphertext.len()];
261    /// cipher.decrypt(&mut plaintext, ciphertext);
262    /// ```
263    ///
264    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
265        self.reset();
266        for (i, &ch) in src.iter().enumerate() {
267            dst[i] = self.encode_both(false, ch);
268        }
269        src.len()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    use rstest::rstest;
278
279    const PLAIN_TXT: &str = "WELLDONEISBETTERTHANWELLSAID";
280    const CIPHER_TXT: &str = "OAHQHCNYNXTSZJRRHJBYHQKSOUJY";
281    const LPLAIN_TXT: &str = "IFYOUCANREADTHISYOUEITHERDOWNLOADEDMYOWNIMPLEMENTATIONOFCHAOCIPHERORYOUWROTEONEOFYOUROWNINEITHERCASELETMEKNOWANDACCEPTMYCONGRATULATIONSX";
282    const LCIPHER_TXT: &str = "TLMAGOONSKJBJYBQVGDQCDUNWNMZPLOYCWPCWKWQRBOYADSLQBKYCDGXJOLONKTTLRUZZJQGJBQNRQHQRREUIYIDHZOMVWZMVYUFQOGSNNUVYTJGQPSQTBRWFHLTCLVVBPMYYQVC";
283    const KEY_PLAIN: &str = "PTLNBQDEOYSFAVZKGJRIHWXUMC";
284    const KEY_CIPHER: &str = "HXUCZVAMDSLKPEFJRIGTWOBNYQ";
285
286    #[test]
287    fn test_new_cipher() {
288        let c = Chaocipher::new(REGULAR_ALPHABET, REGULAR_ALPHABET).unwrap();
289        assert_eq!(c.block_size(), 1);
290    }
291
292    #[test]
293    fn test_new_cipher_bad_len() {
294        assert!(Chaocipher::new("AB", "CD").is_err());
295    }
296
297    #[rstest]
298    #[case(PLAIN_TXT, CIPHER_TXT)]
299    #[case(LPLAIN_TXT, LCIPHER_TXT)]
300    fn test_chaocipher_encrypt(#[case] pt: &str, #[case] ct: &str) {
301        let c = Chaocipher::new(KEY_PLAIN, KEY_CIPHER).unwrap();
302        let src = pt.as_bytes();
303        let mut dst = vec![0u8; src.len()];
304        c.encrypt(&mut dst, src);
305        assert_eq!(dst, ct.as_bytes());
306    }
307
308    #[rstest]
309    #[case(CIPHER_TXT, PLAIN_TXT)]
310    #[case(LCIPHER_TXT, LPLAIN_TXT)]
311    fn test_chaocipher_decrypt(#[case] ct: &str, #[case] pt: &str) {
312        let c = Chaocipher::new(KEY_PLAIN, KEY_CIPHER).unwrap();
313        let src = ct.as_bytes();
314        let mut dst = vec![0u8; src.len()];
315        c.decrypt(&mut dst, src);
316        assert_eq!(dst, pt.as_bytes());
317    }
318
319    #[rstest]
320    #[case('A', 12, "PFJRIGTWOBNYQEHXUCZVAMDSLK", "VZGJRIHWXUMCPKTLNBQDEOYSFA")]
321    #[case('W', 21, "ONYQHXUCZVAMDBSLKPEFJRIGTW", "XUCPTLNBQDEOYMSFAVZKGJRIHW")]
322    fn test_advance(#[case] c_find: char, #[case] expected_idx: usize, #[case] expected_cw: &str, #[case] expected_pw: &str) {
323        let c = Chaocipher::new(KEY_PLAIN, KEY_CIPHER).unwrap();
324        let idx = KEY_PLAIN.find(c_find).unwrap();
325        assert_eq!(idx, expected_idx);
326
327        {
328            let mut state = c.state.borrow_mut();
329            Chaocipher::advance(&mut state, idx);
330        }
331        assert_eq!(String::from_utf8_lossy(&c.state.borrow().cw), expected_cw);
332        assert_eq!(String::from_utf8_lossy(&c.state.borrow().pw), expected_pw);
333    }
334}