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}