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}