old_crypto_rs/square.rs
1//! Square Cipher implementation for classical cryptography.
2//!
3//! The Square Cipher (also known as Polybius Square) is a fractionating
4//! substitution cipher that replaces each plaintext character with a pair of characters
5//! (bigram) from a given character set.
6//!
7//! # Algorithm
8//!
9//! 1. A key is combined with a base alphabet (BASE36) and condensed to remove duplicates
10//! 2. The condensed alphabet is arranged in a square grid
11//! 3. Each character is encoded as coordinates (row, column) using the character set
12//! 4. Decryption reverses the process by looking up bigrams in the grid
13//!
14//! # Example
15//!
16//! ```
17//! use old_crypto_rs::{Block, SquareCipher};
18//!
19//! let cipher = SquareCipher::new("PORTABLE", "ADFGVX").unwrap();
20//! let plaintext = b"ATTACK";
21//! let mut ciphertext = vec![0u8; plaintext.len() * 2];
22//! cipher.encrypt(&mut ciphertext, plaintext);
23//! ```
24//!
25use crate::Block;
26use crate::helpers;
27use crate::helpers::REGULAR_ALPHABET;
28
29use eyre::Result;
30use crate::error::Error;
31
32/// Compact encoding entry for a single plaintext byte.
33///
34/// `len` is 0 for unmapped bytes, or 2 for the bigram length.
35/// `bytes` stores the bigram bytes for the code.
36#[derive(Copy, Clone, Debug, Default)]
37struct EncEntry {
38 len: u8,
39 bytes: [u8; 2],
40}
41
42/// A Square Cipher that implements fractionating substitution.
43///
44/// The cipher maintains both encryption and decryption mappings between
45/// single characters and character pairs (bigrams). The key determines
46/// the arrangement of characters in the square, while the character set
47/// (chrs) determines the symbols used for encoding coordinates.
48///
49/// # Fields
50///
51/// * `key` - The keyword used to initialize the cipher square
52/// * `chrs` - The character set used for bigram generation (e.g., "ADFGVX" or "012345")
53/// * `alpha` - The condensed alphabet used to populate the cipher square
54/// * `enc_table` - Fast encryption table from plaintext byte to bigram bytes
55/// * `dec_table` - Fast decryption table from bigram bytes to plaintext byte
56///
57#[derive(Debug)]
58pub struct SquareCipher {
59 key: String,
60 chrs: String,
61 alpha: Vec<u8>,
62 enc_table: [EncEntry; 256],
63 dec_table: Box<[u8; 256 * 256]>,
64}
65
66impl SquareCipher {
67 /// Creates a new Square Cipher with the given key and character set.
68 ///
69 /// The key is combined with BASE36 alphabet and condensed to remove duplicate characters.
70 /// The character set determines which symbols will be used for the bigram encoding.
71 ///
72 /// # Arguments
73 ///
74 /// * `key` - A non-empty keyword to initialize the cipher square
75 /// * `chrs` - A non-empty character set for bigram generation (length should match square dimensions)
76 ///
77 /// # Returns
78 ///
79 /// * `Ok(SquareCipher)` - Successfully created cipher
80 /// * `Err(String)` - Error message if key or chrs is empty
81 ///
82 /// # Example
83 ///
84 /// ```
85 /// use old_crypto_rs::SquareCipher;
86 ///
87 /// // Create ADFGVX cipher with "PORTABLE" key
88 /// let cipher = SquareCipher::new("PORTABLE", "ADFGVX").unwrap();
89 ///
90 /// // Create numeric variant with "ARABESQUE" key
91 /// let cipher2 = SquareCipher::new("ARABESQUE", "012345").unwrap();
92 /// ```
93 ///
94 /// # Errors
95 ///
96 /// Returns an error if either `key` or `chrs` is an empty string.
97 ///
98 pub fn new(key: &str, chrs: &str) -> Result<Self> {
99 if key.is_empty() || chrs.is_empty() {
100 return Err(Error::EmptyKeys.into());
101 }
102
103 // Step 1: Condense key + alphabet (letters only)
104 //
105 let condensed_letters = helpers::condense(&format!("{}{}", key, REGULAR_ALPHABET));
106
107 // Step 2: Insert digits after corresponding letters
108 // 1 after A, 2 after B, 3 after C, 4 after D, 5 after E,
109 // 6 after F, 7 after G, 8 after H, 9 after I, 0 after J
110 //
111 // This follows <https://en.wikipedia.org/wiki/ADFGVX_cipher method>. That way, numbers
112 // positions are less predictable
113 //
114 let mut alpha = Vec::new();
115 for ch in condensed_letters.chars() {
116 alpha.push(ch as u8);
117 match ch {
118 'A' => alpha.push(b'1'),
119 'B' => alpha.push(b'2'),
120 'C' => alpha.push(b'3'),
121 'D' => alpha.push(b'4'),
122 'E' => alpha.push(b'5'),
123 'F' => alpha.push(b'6'),
124 'G' => alpha.push(b'7'),
125 'H' => alpha.push(b'8'),
126 'I' => alpha.push(b'9'),
127 'J' => alpha.push(b'0'),
128 _ => {}
129 }
130 }
131
132 let mut c = SquareCipher {
133 key: key.to_string(),
134 chrs: chrs.to_string(),
135 alpha,
136 enc_table: [EncEntry::default(); 256],
137 dec_table: Box::new([0; 256 * 256]),
138 };
139 c.expand_key();
140 Ok(c)
141 }
142
143 /// Expands the key into encryption and decryption lookup tables.
144 ///
145 /// This method generates all possible bigrams from the character set and maps them
146 /// to/from the condensed alphabet. Each character in the alphabet is assigned
147 /// coordinates (i, j) which are encoded as a bigram using characters from `chrs`.
148 ///
149 /// For example, with chrs="ADFGVX" and a 6x6 grid:
150 /// - Position (0,0) → "AA"
151 /// - Position (0,1) → "AD"
152 /// - Position (1,0) → "DA"
153 /// - etc.
154 ///
155 fn expand_key(&mut self) {
156 let mut bigr = vec![0u8; 2];
157 let klen = self.chrs.len();
158 let chrs_bytes = self.chrs.as_bytes();
159
160 // Generate all bigrams by iterating through character set twice (i, j)
161 for i in 0..klen {
162 for j in 0..klen {
163 // Create bigram: first char from row i, second char from column j
164 bigr[0] = chrs_bytes[i];
165 bigr[1] = chrs_bytes[j];
166
167 // Calculate linear index in the square (row * width + column)
168 let ind = i * klen + j;
169
170 // Only map if we haven't exceeded the alphabet length
171 if ind < self.alpha.len() {
172 // Forward mapping: alphabet character → bigram
173 let pt = self.alpha[ind];
174 let entry = EncEntry {
175 len: 2,
176 bytes: [bigr[0], bigr[1]],
177 };
178 self.enc_table[pt as usize] = entry;
179 // Reverse mapping: bigram → alphabet character
180 let idx = ((bigr[0] as usize) << 8) | (bigr[1] as usize);
181 self.dec_table[idx] = pt;
182 }
183 }
184 }
185 }
186}
187
188impl Block for SquareCipher {
189 /// Returns the block size for this cipher.
190 ///
191 /// The block size is equal to the key length. This determines how many
192 /// characters are processed together during encryption/decryption operations.
193 ///
194 /// # Returns
195 ///
196 /// The length of the cipher key in bytes.
197 ///
198 fn block_size(&self) -> usize {
199 self.key.len()
200 }
201
202 /// Encrypts plaintext into ciphertext using the Square Cipher.
203 ///
204 /// Each byte in the source is replaced by a two-character bigram, effectively
205 /// doubling the length of the output. The bigram represents the row and column
206 /// coordinates of the character in the cipher square.
207 ///
208 /// # Arguments
209 ///
210 /// * `dst` - Destination buffer for ciphertext (must be at least 2 * src.len())
211 /// * `src` - Source plaintext bytes to encrypt
212 ///
213 /// # Returns
214 ///
215 /// The number of bytes written to dst (always 2 * src.len())
216 ///
217 /// # Note
218 ///
219 /// Characters not found in the encryption table are silently skipped.
220 ///
221 fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
222 for (i, &ch) in src.iter().enumerate() {
223 let entry = self.enc_table[ch as usize];
224 if entry.len == 2 {
225 // Write the two-character bigram to destination
226 dst[i * 2] = entry.bytes[0];
227 dst[i * 2 + 1] = entry.bytes[1];
228 }
229 }
230 src.len() * 2
231 }
232
233 /// Decrypts ciphertext back into plaintext using the Square Cipher.
234 ///
235 /// Processes the source in pairs of characters (bigrams), looking up each
236 /// bigram in the decryption table to recover the original character.
237 ///
238 /// # Arguments
239 ///
240 /// * `dst` - Destination buffer for plaintext (must be at least src.len() / 2)
241 /// * `src` - Source ciphertext bytes to decrypt (must have even length)
242 ///
243 /// # Returns
244 ///
245 /// The number of bytes written to dst (always src.len() / 2)
246 ///
247 /// # Note
248 ///
249 /// Bigrams not found in the decryption table are silently skipped.
250 ///
251 fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
252 // Process source in steps of 2 (each bigram)
253 for i in (0..src.len()).step_by(2) {
254 let idx = ((src[i] as usize) << 8) | (src[i + 1] as usize);
255 let pt = self.dec_table[idx];
256 if pt != 0 {
257 // Write the recovered character to destination
258 dst[i / 2] = pt;
259 }
260 }
261 src.len() / 2
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_expand_key() {
271 let test_data = [
272 ("PORTABLE", "ADFGVX"),
273 ("ARABESQUE", "012345"),
274 ];
275
276 for (key, chrs) in test_data {
277 let _c = SquareCipher::new(key, chrs).unwrap();
278 // In Rust we don't need to test multiple versions of expand_key
279 // we just test that the maps are correct.
280 // Since the maps are private and would be tedious to recreate here,
281 // we can test the functional encryption/decryption which depends on them.
282 }
283 }
284
285 #[test]
286 fn test_new_cipher() {
287 let c = SquareCipher::new("PORTABLE", "ADFGVX");
288 assert!(c.is_ok());
289 }
290
291 #[test]
292 fn test_new_cipher_empty_key() {
293 let c = SquareCipher::new("", "012345");
294 assert!(c.is_err());
295 }
296
297 #[test]
298 fn test_new_cipher_empty_chrs() {
299 let c = SquareCipher::new("SUBWAY", "");
300 assert!(c.is_err());
301 }
302
303 #[test]
304 fn test_square_cipher_block_size() {
305 let test_data = [
306 ("PORTABLE", "ADFGVX"),
307 ("ARABESQUE", "012345"),
308 ];
309 for (key, chrs) in test_data {
310 let c = SquareCipher::new(key, chrs).unwrap();
311 assert_eq!(c.block_size(), key.len());
312 }
313 }
314
315 #[test]
316 fn test_square_cipher_encrypt() {
317 let test_data = [
318 ("PORTABLE", "ADFGVX", "ATTACKATDAWN", "AVAGAGAVDXVDAVAGFDAVXFVG"),
319 ("ARABESQUE", "012345", "ATTACKATDAWN", "005050001440005020005243"),
320 ("NACHTBOMMENWERPER", "ADFGVX", "ATTACKAT1200AM", "ADDDDDADAGVGADDDAFDGVFVFADDX")
321 ];
322
323 for (key, chrs, pt, ct) in test_data {
324 let c = SquareCipher::new(key, chrs).unwrap();
325 let src = pt.as_bytes();
326 let mut dst = vec![0u8; 2 * pt.len()];
327 c.encrypt(&mut dst, src);
328 assert_eq!(String::from_utf8(dst).unwrap(), ct);
329 }
330 }
331
332 #[test]
333 fn test_square_cipher_decrypt() {
334 let test_data = [
335 ("PORTABLE", "ADFGVX", "ATTACKATDAWN", "AVAGAGAVDXVDAVAGFDAVXFVG"),
336 ("ARABESQUE", "012345", "ATTACKATDAWN", "005050001440005020005243"),
337 ("NACHTBOMMENWERPER", "ADFGVX", "ATTACKAT1200AM", "ADDDDDADAGVGADDDAFDGVFVFADDX")
338 ];
339
340 for (key, chrs, pt, ct) in test_data {
341 let c = SquareCipher::new(key, chrs).unwrap();
342 let src = ct.as_bytes();
343 let mut dst = vec![0u8; src.len() / 2];
344 c.decrypt(&mut dst, src);
345 assert_eq!(String::from_utf8(dst).unwrap(), pt);
346 }
347 }
348}