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}