old_crypto_rs/transposition/
irregular.rs

1//! An irregular transposition cipher used in the VIC cipher.
2//!
3//! This cipher is a variant of columnar transposition where the grid is filled in an irregular
4//! pattern, creating triangular areas that are filled after the regular areas. It's specifically
5//! designed for use in the VIC cipher.
6//!
7//! The cipher uses two special positions (rank 0 and rank 1) in the key to define triangular
8//! areas. These areas are filled separately from the rest of the grid, creating a more complex
9//! transposition pattern.
10//!
11//! # Examples
12//!
13//! ```
14//! use old_crypto_rs::{Block, IrregularTransposition};
15//!
16//! let cipher = IrregularTransposition::new("SUBWAY").unwrap();
17//! let plaintext = b"ATTACKATDAWN";
18//! let mut ciphertext = vec![0u8; plaintext.len()];
19//! cipher.encrypt(&mut ciphertext, plaintext);
20//! ```
21
22use crate::{helpers, Block};
23
24use eyre::Result;
25use crate::error::Error;
26
27#[derive(Debug)]
28pub struct IrregularTransposition {
29    #[allow(dead_code)]
30    key: String,
31    tkey: Vec<u8>,
32    rank_pos: [usize; 2],
33    tkey_order: Vec<usize>,
34}
35
36impl IrregularTransposition {
37    /// Creates a new irregular transposition cipher.
38    ///
39    /// The key is converted to a numeric representation where each character's position
40    /// in alphabetical order determines its rank. The positions of ranks 0 and 1 define
41    /// the triangular areas used in the irregular transposition.
42    ///
43    /// # Arguments
44    ///
45    /// * `key` - The key string used for transposition. Must not be empty.
46    ///
47    /// # Returns
48    ///
49    /// Returns `Ok(IrregularTransposition)` if the key is valid, or an error message
50    /// if the key is empty.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the key is empty.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use old_crypto_rs::IrregularTransposition;
60    ///
61    /// let cipher = IrregularTransposition::new("SUBWAY").unwrap();
62    /// ```
63    pub fn new(key: &str) -> Result<Self> {
64        if key.is_empty() {
65            return Err(Error::EmptyKeys.into());
66        }
67        let tkey = helpers::to_numeric(key);
68        let pos0 = tkey.iter().position(|&x| x == 0).unwrap();
69        let pos1 = tkey.iter().position(|&x| x == 1).unwrap();
70        
71        let klen = tkey.len();
72        let mut tkey_order = vec![0; klen];
73        for i in 0..klen {
74            tkey_order[i] = tkey.iter().position(|&x| x == i as u8).unwrap();
75        }
76
77        Ok(IrregularTransposition {
78            key: key.to_string(),
79            tkey,
80            rank_pos: [pos0, pos1],
81            tkey_order,
82        })
83    }
84
85    /// Computes which cells are "irregular" (triangular areas) for a given message length.
86    ///
87    /// The triangular areas start at the positions of ranks 0 and 1 in the key, and expand
88    /// to the right as row numbers increase. A cell at position (r, c) is in a triangular
89    /// area if it's at or beyond `rank_pos[0] + r` or `rank_pos[1] + r`, and within the
90    /// grid width.
91    ///
92    /// # Arguments
93    ///
94    /// * `r` - The row index (0-based)
95    /// * `c` - The column index (0-based)
96    ///
97    /// # Returns
98    ///
99    /// Returns `true` if the cell is in a triangular area, `false` otherwise.
100    #[inline]
101    fn is_in_triangular_area(&self, r: usize, c: usize) -> bool {
102        // The triangle starts at (row 0, start_col)
103        // It expands to the right: at row `i`, it covers columns `start_col + i` to `klen - 1`
104        (c >= self.rank_pos[0] + r || c >= self.rank_pos[1] + r) && c < self.tkey.len()
105    }
106}
107
108impl Block for IrregularTransposition {
109    fn block_size(&self) -> usize {
110        self.tkey.len()
111    }
112
113    /// Encrypts the source data using irregular transposition.
114    ///
115    /// The encryption process works in three phases:
116    ///
117    /// 1. **Fill non-triangular areas**: The plaintext is written into a grid row by row,
118    ///    filling only the cells that are NOT in the triangular areas (defined by ranks 0 and 1).
119    ///    This creates the "regular" part of the transposition.
120    ///
121    /// 2. **Fill triangular areas**: After the regular areas are filled, the remaining plaintext
122    ///    continues filling the triangular areas row by row. These are the cells at positions
123    ///    (r, c) where `c >= rank_pos[0] + r` or `c >= rank_pos[1] + r`.
124    ///
125    /// 3. **Read column by column**: The ciphertext is generated by reading the grid column by
126    ///    column in the order specified by `tkey_order` (alphabetical order of the key). Only
127    ///    active cells (those that were filled) are read.
128    ///
129    /// # Arguments
130    ///
131    /// * `dst` - The destination buffer where encrypted data will be written
132    /// * `src` - The source plaintext data to encrypt
133    ///
134    /// # Returns
135    ///
136    /// Returns the number of bytes written to `dst` (equal to the length of `src`).
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use old_crypto_rs::{Block, IrregularTransposition};
142    ///
143    /// let cipher = IrregularTransposition::new("SUBWAY").unwrap();
144    /// let plaintext = b"ATTACKATDAWN";
145    /// let mut ciphertext = vec![0u8; plaintext.len()];
146    /// let n = cipher.encrypt(&mut ciphertext, plaintext);
147    /// assert_eq!(n, plaintext.len());
148    /// ```
149    /// 
150    fn encrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
151        let klen = self.tkey.len();
152        let len = src.len();
153        if klen == 0 || len == 0 {
154            return 0;
155        }
156        let rows = (len + klen - 1) / klen;
157
158        let mut grid = vec![0u8; rows * klen];
159        let mut active = vec![0u8; (rows * klen + 7) / 8];
160        let mut src_idx = 0;
161
162        // Phase 1: Fill non-triangular areas row by row
163        for r in 0..rows {
164            let row_off = r * klen;
165            for c in 0..klen {
166                if !self.is_in_triangular_area(r, c) && src_idx < len {
167                    let idx = row_off + c;
168                    grid[idx] = src[src_idx];
169                    active[idx >> 3] |= 1 << (idx & 7);
170                    src_idx += 1;
171                }
172            }
173        }
174
175        // Phase 2: Fill triangular areas row by row
176        for r in 0..rows {
177            let row_off = r * klen;
178            for c in 0..klen {
179                if self.is_in_triangular_area(r, c) && src_idx < len {
180                    let idx = row_off + c;
181                    grid[idx] = src[src_idx];
182                    active[idx >> 3] |= 1 << (idx & 7);
183                    src_idx += 1;
184                }
185            }
186        }
187
188        // Read out column by column according to tkey
189        let mut dst_idx = 0;
190        for &col in &self.tkey_order {
191            for r in 0..rows {
192                let idx = r * klen + col;
193                if (active[idx >> 3] & (1 << (idx & 7))) != 0 {
194                    dst[dst_idx] = grid[idx];
195                    dst_idx += 1;
196                }
197            }
198        }
199
200        len
201    }
202
203    /// Decrypts the source data using irregular transposition.
204    ///
205    /// The decryption process reverses the encryption:
206    ///
207    /// 1. **Determine active cells**: First, we determine which cells in the grid are active
208    ///    by simulating the filling process (non-triangular areas first, then triangular areas).
209    ///    This tells us which cells contain actual ciphertext data.
210    ///
211    /// 2. **Fill grid from columns**: The ciphertext is read column by column in the order
212    ///    specified by `tkey_order`, filling the grid's active cells. This reverses the
213    ///    column-wise reading done during encryption.
214    ///
215    /// 3. **Read in two phases**: The plaintext is recovered by reading the grid in two phases:
216    ///    - First phase: Read row by row from non-triangular areas
217    ///    - Second phase: Read row by row from triangular areas
218    ///    This reverses the two-phase filling done during encryption.
219    ///
220    /// # Arguments
221    ///
222    /// * `dst` - The destination buffer where decrypted data will be written
223    /// * `src` - The source ciphertext data to decrypt
224    ///
225    /// # Returns
226    ///
227    /// Returns the number of bytes written to `dst` (equal to the length of `src`).
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use old_crypto_rs::{Block, IrregularTransposition};
233    ///
234    /// let cipher = IrregularTransposition::new("SUBWAY").unwrap();
235    /// let ciphertext = b"ATWATADCAKN";
236    /// let mut plaintext = vec![0u8; ciphertext.len()];
237    /// let n = cipher.decrypt(&mut plaintext, ciphertext);
238    /// assert_eq!(n, ciphertext.len());
239    /// ```
240    /// 
241    fn decrypt(&self, dst: &mut [u8], src: &[u8]) -> usize {
242        let klen = self.tkey.len();
243        let len = src.len();
244        if klen == 0 || len == 0 {
245            return 0;
246        }
247        let rows = (len + klen - 1) / klen;
248
249        // Determine active cells
250        let mut active = vec![0u8; (rows * klen + 7) / 8];
251        let mut count = 0;
252        for r in 0..rows {
253            let row_off = r * klen;
254            for c in 0..klen {
255                if !self.is_in_triangular_area(r, c) && count < len {
256                    let idx = row_off + c;
257                    active[idx >> 3] |= 1 << (idx & 7);
258                    count += 1;
259                }
260            }
261        }
262        for r in 0..rows {
263            let row_off = r * klen;
264            for c in 0..klen {
265                if self.is_in_triangular_area(r, c) && count < len {
266                    let idx = row_off + c;
267                    active[idx >> 3] |= 1 << (idx & 7);
268                    count += 1;
269                }
270            }
271        }
272
273        // Fill grid from src column by column
274        let mut grid = vec![0u8; rows * klen];
275        let mut src_idx = 0;
276        for &col in &self.tkey_order {
277            for r in 0..rows {
278                let idx = r * klen + col;
279                if (active[idx >> 3] & (1 << (idx & 7))) != 0 {
280                    grid[idx] = src[src_idx];
281                    src_idx += 1;
282                }
283            }
284        }
285
286        // Read out row by row, first non-mask then mask
287        let mut dst_idx = 0;
288        for r in 0..rows {
289            let row_off = r * klen;
290            for c in 0..klen {
291                let idx = row_off + c;
292                if ((active[idx >> 3] & (1 << (idx & 7))) != 0) && !self.is_in_triangular_area(r, c) {
293                    dst[dst_idx] = grid[idx];
294                    dst_idx += 1;
295                }
296            }
297        }
298        for r in 0..rows {
299            let row_off = r * klen;
300            for c in 0..klen {
301                let idx = row_off + c;
302                if ((active[idx >> 3] & (1 << (idx & 7))) != 0) && self.is_in_triangular_area(r, c) {
303                    dst[dst_idx] = grid[idx];
304                    dst_idx += 1;
305                }
306            }
307        }
308
309        len
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_irregular_transposition_mask() {
319        let key = "94735236270398134";
320        let c = IrregularTransposition::new(key).unwrap();
321
322        // Row 0:
323        // Rank 0 is at pos 10.
324        // Rank 1 is at pos 14.
325        for col in 10..17 {
326            assert!(c.is_in_triangular_area(0, col), "Row 0 col {} should be true", col);
327        }
328        for col in 0..10 {
329            assert!(!c.is_in_triangular_area(0, col), "Row 0 col {} should be false", col);
330        }
331
332        // Row 1: col 11 to 16 are true for the first triangle.
333        for col in 11..17 {
334            assert!(c.is_in_triangular_area(1, col), "Row 1 col {} should be true", col);
335        }
336        assert!(!c.is_in_triangular_area(1, 10));
337
338        // Row 0: rank 1 is at pos 14.
339        assert!(c.is_in_triangular_area(0, 14));
340    }
341}