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}