cipher_crypt/
columnar_transposition.rs1use crate::common::alphabet::Alphabet;
9use crate::common::cipher::Cipher;
10use crate::common::{alphabet, keygen};
11
12pub struct ColumnarTransposition {
15 keystream: String,
16 null_char: Option<char>,
17 derived_key: Vec<(char, Vec<char>)>,
18}
19
20impl Cipher for ColumnarTransposition {
21 type Key = (String, Option<char>);
22 type Algorithm = ColumnarTransposition;
23
24 fn new(key: (String, Option<char>)) -> ColumnarTransposition {
39 if let Some(null_char) = key.1 {
40 if key.0.contains(null_char) {
41 panic!("The `keystream` contains a `null_char`.");
42 }
43 }
44
45 ColumnarTransposition {
46 derived_key: keygen::columnar_key(&key.0),
47 keystream: key.0,
48 null_char: key.1,
49 }
50 }
51
52 fn encrypt(&self, message: &str) -> Result<String, &'static str> {
75 if let Some(null_char) = self.null_char {
76 if message.contains(null_char) {
77 return Err("Message contains null characters.");
78 }
79 }
80
81 let mut key = self.derived_key.clone();
82
83 let mut i = 0;
85 let mut chars = message.trim_end().chars(); loop {
87 if let Some(c) = chars.next() {
88 key[i].1.push(c);
89 } else if i > 0 {
90 if let Some(null_char) = self.null_char {
91 key[i].1.push(null_char)
92 }
93 } else {
94 break;
95 }
96
97 i = (i + 1) % key.len();
98 }
99
100 key.sort_by(|a, b| {
102 alphabet::STANDARD
103 .find_position(a.0)
104 .unwrap()
105 .cmp(&alphabet::STANDARD.find_position(b.0).unwrap())
106 });
107
108 let ciphertext: String = key
110 .iter()
111 .map(|column| column.1.iter().collect::<String>())
112 .collect();
113
114 Ok(ciphertext)
115 }
116
117 fn decrypt(&self, ciphertext: &str) -> Result<String, &'static str> {
147 let mut key = self.derived_key.clone();
148
149 let mut chars = ciphertext.chars();
151 let max_col_size: usize =
153 (ciphertext.chars().count() as f32 / self.keystream.len() as f32).ceil() as usize;
154
155 let offset = key.len() - (ciphertext.chars().count() % key.len());
160
161 let offset_cols = if self.null_char.is_none() && offset != key.len() {
163 key.iter()
164 .map(|e| e.0)
165 .rev()
166 .take(offset)
167 .collect::<String>()
168 } else {
169 String::from("")
170 };
171
172 key.sort_by(|a, b| {
174 alphabet::STANDARD
175 .find_position(a.0)
176 .unwrap()
177 .cmp(&alphabet::STANDARD.find_position(b.0).unwrap())
178 });
179
180 'outer: for column in &mut key {
181 loop {
182 let offset_num = if offset_cols.contains(column.0) { 1 } else { 0 };
183 if column.1.len() >= max_col_size - offset_num {
185 break;
186 } else if let Some(c) = chars.next() {
187 column.1.push(c);
188 } else {
189 break 'outer; }
191 }
192 }
193
194 let mut plaintext = String::new();
195 for i in 0..max_col_size {
196 for chr in self.keystream.chars() {
197 if let Some(column) = key.iter().find(|x| x.0 == chr) {
199 if i < column.1.len() {
200 let c = column.1[i];
201 if let Some(null_char) = self.null_char {
203 if c == null_char && !c.is_whitespace() {
204 break;
205 }
206 }
207 plaintext.push(c);
208 }
209 } else {
210 return Err("Could not find column during decryption.");
211 }
212 }
213 }
214
215 Ok(plaintext.trim_end().to_string())
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn simple() {
225 let message = "wearediscovered";
226
227 let key_word = String::from("zebras");
228 let null_char = Some('\u{0}');
229 let ct = ColumnarTransposition::new((key_word, null_char));
230
231 assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
232 }
233
234 #[test]
235 fn simple_no_padding() {
236 let message = "wearediscovered";
237
238 let key_word = String::from("zebras");
239 let null_char = None;
240 let ct = ColumnarTransposition::new((key_word, null_char));
241
242 assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
243 }
244
245 #[test]
246 fn with_utf8() {
247 let message = "Peace, Freedom 🗡️ and Liberty!";
248
249 let key_word = String::from("zebras");
250 let null_char = Some('\u{0}');
251 let ct = ColumnarTransposition::new((key_word, null_char));
252 let encrypted = ct.encrypt(message).unwrap();
253 assert_eq!(ct.decrypt(&encrypted).unwrap(), message);
254 }
255
256 #[test]
257 fn with_utf8_no_padding() {
258 let message = "Peace, Freedom 🗡️ and Liberty!";
259
260 let key_word = String::from("zebras");
261 let null_char = None;
262 let ct = ColumnarTransposition::new((key_word, null_char));
263 let encrypted = ct.encrypt(message).unwrap();
264 assert_eq!(ct.decrypt(&encrypted).unwrap(), message);
265 }
266
267 #[test]
268 fn single_column() {
269 let message = "we are discovered";
270
271 let key_word = String::from("z");
272 let null_char = Some('\u{0}');
273 let ct = ColumnarTransposition::new((key_word, null_char));
274 assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
275 }
276
277 #[test]
278 fn single_column_no_padding() {
279 let message = "we are discovered";
280
281 let key_word = String::from("z");
282 let null_char = None;
283 let ct = ColumnarTransposition::new((key_word, null_char));
284 assert_eq!(ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(), message);
285 }
286
287 #[test]
288 fn trailing_spaces() {
289 let message = "we are discovered "; let key_word = String::from("z");
292 let null_char = None;
293 let ct = ColumnarTransposition::new((key_word, null_char));
294
295 assert_eq!(
296 ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(),
297 "we are discovered"
298 );
299 }
300
301 #[test]
302 fn plaintext_containing_padding() {
303 let key_word = String::from("zebras");
304 let null_char = Some(' ');
305 let ct = ColumnarTransposition::new((key_word, null_char));
306
307 let plain_text = "This will fail because of spaces.";
308 assert!(ct.encrypt(plain_text).is_err());
309 }
310
311 #[test]
312 fn trailing_spaces_no_padding() {
313 let message = "we are discovered "; let key_word = String::from("z");
316 let null_char = None;
317 let ct = ColumnarTransposition::new((key_word, null_char));
318
319 assert_eq!(
320 ct.decrypt(&ct.encrypt(message).unwrap()).unwrap(),
321 "we are discovered"
322 );
323 }
324
325 #[test]
326 #[should_panic]
327 fn padding_in_key() {
328 ColumnarTransposition::new((String::from("zebras"), Some('z')));
329 }
330}