cipher_crypt/fractionated_morse.rs
1//! The Fractionated Morse cipher builds upon Morse code, a well-known method for encoding text
2//! which can then be sent across simple visual or audio channels.
3//!
4//! The Fractionated Morse cipher does not produce a one-to-one mapping of plaintext characters to
5//! ciphertext characters and is therefore slightly more secure than a simple substitution cipher.
6//! In addition to this, it allows many non-alphabetic symbols to be encoded.
7//!
8//!
9use crate::common::cipher::Cipher;
10use crate::common::{alphabet, keygen, morse};
11
12// The fractionated morse trigraph 'alphabet'. Each sequence represents a letter of the alphabet.
13const TRIGRAPH_ALPHABET: [&str; 26] = [
14 "...", "..-", "..|", ".-.", ".--", ".-|", ".|.", ".|-", ".||", "-..", "-.-", "-.|", "--.",
15 "---", "--|", "-|.", "-|-", "-||", "|..", "|.-", "|.|", "|-.", "|--", "|-|", "||.", "||-",
16];
17
18/// A Fractionated Morse cipher.
19///
20/// This struct is created by the `new()` method. See its documentation for more.
21pub struct FractionatedMorse {
22 keyed_alphabet: String,
23}
24
25impl Cipher for FractionatedMorse {
26 type Key = String;
27 type Algorithm = FractionatedMorse;
28
29 /// Initialise a Fractionated Morse cipher given a specific key.
30 ///
31 /// # Panics
32 /// * The `key` is empty.
33 /// * The `key` contains a non-alphabetic symbol.
34 ///
35 fn new(key: String) -> FractionatedMorse {
36 if key.is_empty() {
37 panic!("Key is empty.");
38 }
39
40 let keyed_alphabet = keygen::keyed_alphabet(&key, &alphabet::STANDARD, true);
41 FractionatedMorse { keyed_alphabet }
42 }
43
44 /// Encrypt a message using a Fractionated Morse cipher.
45 ///
46 /// Morse code supports the characters `a-z`, `A-Z`, `0-9` and the special characters
47 /// `@ ( ) . , : ' " ! ? - ; =`. This function will return `Err` if the message contains any
48 /// symbols that do not meet this criteria. As morse code does not preserve case, all messages
49 /// will be transposed to uppercase automatically.
50 ///
51 /// # Examples
52 /// Basic usage:
53 ///
54 /// ```
55 /// use cipher_crypt::{Cipher, FractionatedMorse};
56 ///
57 /// let fm = FractionatedMorse::new(String::from("key"));;
58 /// assert_eq!("CPSUJISWHSSPFANR", fm.encrypt("AttackAtDawn!").unwrap());
59 /// ```
60 ///
61 fn encrypt(&self, message: &str) -> Result<String, &'static str> {
62 // Encryption process
63 // (1) The message is encoded in Morse using `|` as a character separator and finishing
64 // with the sequence `||`.
65 // (2) Dots are added to the end of the Morse string until the length is a multiple of 3.
66 // (3) The message is split into groups of 3 and the substitution 0 for '.', 1 for '-'
67 // and 2 for '|' is made to produce a series of ternary numbers between 0 and 26.
68 // (4) The keyed alphabet is obtained from the key.
69 // (5) The numbers obtained in step 3 are converted to letters using the keyed alphabet.
70 // (6) The letters are then concatenated to form the ciphertext.
71 //
72 // Example: Key: `alphabet`, Plaintext: `hello`
73 // (1) The Morse message `....|.|.-..|.-..|---||` is produced.
74 // (2) Two dots are added to give `....|.|.-..|.-..|---||..`
75 // (3) ... -> 000 -> 0
76 // .|. -> 020 -> 6
77 // |.- -> 201 -> 19
78 // ..| -> 002 -> 2
79 // and so on.
80 // (4) The alphabet `alphbetcdfgijkmnoqrsuvwxyz` is produced.
81 // (5) 0(a), 6(t), 19(s), 2(p)
82 // (6) The ciphertext `atsphcmr` is produced.
83 let mut morse = FractionatedMorse::encode_to_morse(message)?;
84
85 //Pad the morse so that it can be interpreted properly as a fractionated message
86 FractionatedMorse::pad(&mut morse);
87 FractionatedMorse::encrypt_morse(&self.keyed_alphabet, &morse)
88 }
89
90 /// Decrypt a message using a Fractionated Morse cipher.
91 ///
92 /// The Fractionated Morse alphabet only contains the normal alphabetic characters `a-z`,
93 /// therefore this function will return `Err` if the message contains any non-alphabetic
94 /// characters. Furthermore, it is possible that a purely alphabetic message will not produce
95 /// valid Morse code, in which case an `Err` will again be returned.
96 ///
97 /// # Examples
98 /// Basic usage:
99 ///
100 /// ```
101 /// use cipher_crypt::{Cipher, FractionatedMorse};
102 ///
103 /// let fm = FractionatedMorse::new(String::from("key"));;
104 /// assert_eq!("ATTACKATDAWN!", fm.decrypt("cpsujiswhsspfanr").unwrap());
105 /// ```
106 ///
107 fn decrypt(&self, cipher_text: &str) -> Result<String, &'static str> {
108 // Decryption process:
109 // (1) The keyed alphabet is obtained from the key.
110 // (2) Each ciphertext char is located by index in the keyed alphabet.
111 // (3) The indices are convert to 3 digit ternary and the substitution '.' for 0,
112 // '-' for 1 and '|' for 2 is made to produce a trigraph for each letter.
113 // (4) These trigraphs are substituted for each letter in the message and concatenated
114 // to produce a Morse string.
115 // (5) The Morse message is decoded.
116 //
117 // Example: Key: `alphabet`, Ciphertext: `atsphcmr`
118 // (1) The alphabet `alphbetcdfgijkmnoqrsuvwxyz` is produced.
119 // (2) a(0), t(6), s(19), p(2), h(3), c(7), m(14), r(18)
120 // (3) 0 -> 000 -> ...
121 // 6 -> 020 -> .|.
122 // 19 -> 201 -> |.-
123 // 2 -> 002 -> ..|
124 // and so on.
125 // (4) The Morse message `....|.|.-..|.-..|---||..` is produced.
126 // (5) The plaintext `hello i` is recovered.
127 let seq = FractionatedMorse::decrypt_morse(&self.keyed_alphabet, cipher_text)?;
128 FractionatedMorse::decode_morse(&seq)
129 }
130}
131
132impl FractionatedMorse {
133 /// Takes a message and converts it to Morse code, using the character `|` as a separator.
134 /// The transposed sequence is ended with two separators `||`. This function returns `Err`
135 /// if an unsupported symbol is present. The support characters are `a-z`, `A-Z`, `0-9` and
136 /// the special characters `@ ( ) . , : ' " ! ? - ; =`.
137 fn encode_to_morse(message: &str) -> Result<String, &'static str> {
138 if message
139 .chars()
140 .any(|c| morse::encode_character(c).is_none())
141 {
142 return Err("Unsupported character detected in message.");
143 }
144
145 let mut morse: String = message
146 .chars()
147 .map(|c| format!("{}{}", morse::encode_character(c).unwrap(), '|'))
148 .collect();
149
150 morse.push('|'); // Finish the Morse message with a double separator `||`.
151 Ok(morse)
152 }
153
154 /// Takes a morse sequence and converts it to an alphabetic string using the fractionated
155 /// morse method.
156 ///
157 /// This function returns `Err` if an invalid fractionated morse trigraph is encountered.
158 fn encrypt_morse(key: &str, morse: &str) -> Result<String, &'static str> {
159 let mut ciphertext = String::new();
160
161 // Loop over each trigraph and decode it to an alphabetic character
162 for trigraph in morse.as_bytes().chunks(3) {
163 match TRIGRAPH_ALPHABET
164 .iter()
165 .position(|&t| t.as_bytes() == trigraph)
166 {
167 Some(pos) => ciphertext.push(key.chars().nth(pos).unwrap()), //Safe unwrap
168 None => return Err("Unknown trigraph sequence within the morse code."),
169 }
170 }
171
172 Ok(ciphertext)
173 }
174
175 /// Takes ciphertext and converts it to a sequence of trigraph symbols.
176 ///
177 /// return `Err` if a non-alphabetic symbol is present in the message.
178 fn decrypt_morse(key: &str, ciphertext: &str) -> Result<String, &'static str> {
179 if ciphertext
180 .to_uppercase()
181 .chars()
182 .any(|c| key.chars().position(|k| k == c).is_none())
183 {
184 return Err("Ciphertext cannot contain non-alphabetic symbols.");
185 }
186
187 Ok(ciphertext
188 .to_uppercase()
189 .chars()
190 .map(|c| TRIGRAPH_ALPHABET[key.chars().position(|k| k == c).unwrap()])
191 .collect::<String>())
192 }
193
194 /// Takes a sequence of trigraphs, which is then interpreted as morse code so that it may be
195 /// converted back to plaintext.This function returns `Err` if an invalid morse character is
196 /// encountered.
197 fn decode_morse(sequence: &str) -> Result<String, &'static str> {
198 let mut plaintext = String::new();
199 let mut trigraphs = String::from(sequence);
200
201 // Remove character separators from the beginning of the message if present
202 while trigraphs.starts_with('|') {
203 trigraphs.remove(0);
204 }
205
206 // Loop over every Morse character
207 for morse_seq in trigraphs.split('|') {
208 // A double separator signifies message end. As we are splitting on '|',
209 // the sequence '||' will produce an empty string.
210 if morse_seq == "" {
211 break;
212 }
213
214 // Find the Morse character in the alphabet and decode it.
215 match morse::decode_sequence(morse_seq) {
216 Some(c) => plaintext.push_str(&c),
217 None => return Err("Unknown morsecode sequence in trigraphs."),
218 }
219 }
220
221 Ok(plaintext)
222 }
223
224 /// Takes a morse sequence and pads it with dots to a length that is a multiple of 3.
225 /// This allows it to be interpreted as a Fractionated Morse message.
226 fn pad(morse_sequence: &mut String) {
227 while morse_sequence.len() % 3 != 0 {
228 morse_sequence.push('.');
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn encrypt_test() {
239 let message = "attackatdawn";
240 let f = FractionatedMorse::new(String::from("key"));
241 assert_eq!("CPSUJISWHSSPG", f.encrypt(message).unwrap());
242 }
243
244 #[test]
245 fn decrypt_test() {
246 let message = "cpsujiswhsspg";
247 let f = FractionatedMorse::new(String::from("key"));
248 assert_eq!("ATTACKATDAWN", f.decrypt(message).unwrap());
249 }
250
251 #[test]
252 fn encrypt_mixed_case() {
253 let message = "AttackAtDawn";
254 let f = FractionatedMorse::new(String::from("OranGE"));
255 assert_eq!("EPTVIHTXFTTPD", f.encrypt(message).unwrap());
256 }
257
258 #[test]
259 fn decrypt_mixed_case() {
260 let message = "EPtvihtXFttPD";
261 let f = FractionatedMorse::new(String::from("OranGE"));
262 assert_eq!("ATTACKATDAWN", f.decrypt(message).unwrap());
263 }
264
265 #[test]
266 fn encrypt_punctuation() {
267 let m = "Testingpunctuation!Willitwork?";
268 let f = FractionatedMorse::new(String::from("Punctuation"));
269 assert_eq!(m.to_uppercase(), f.decrypt(&f.encrypt(m).unwrap()).unwrap());
270 }
271
272 #[test]
273 #[should_panic]
274 fn encrypt_no_key() {
275 FractionatedMorse::new(String::from(""));
276 }
277
278 #[test]
279 fn encrypt_long_key() {
280 let message = "defendtheeastwall";
281 let f = FractionatedMorse::new(String::from("nnhhyqzabguuxwdrvvctspefmjoklii"));
282 assert_eq!("XMHBJJGEYBFEGFTTXFYE", f.encrypt(message).unwrap());
283 }
284
285 #[test]
286 fn exhaustive_encrypt_decrypt() {
287 let m = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.,:\'\"!?@-;()=";
288 let f = FractionatedMorse::new(String::from("exhaustive"));
289 assert_eq!(m.to_uppercase(), f.decrypt(&f.encrypt(m).unwrap()).unwrap());
290 }
291
292 #[test]
293 #[should_panic]
294 fn bad_key() {
295 FractionatedMorse::new(String::from("bad key"));
296 }
297
298 #[test]
299 fn encrypt_bad_message() {
300 let message = "Spaces are not supported.";
301 let f = FractionatedMorse::new(String::from("test"));
302 assert!(f.encrypt(message).is_err());
303 }
304
305 #[test]
306 fn decrypt_bad_message() {
307 let message = "badmessagefordecryption";
308 let f = FractionatedMorse::new(String::from("test"));
309 assert!(f.decrypt(message).is_err());
310 }
311}