mnemonic_16bit/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![deny(unused_must_use)]
4#![deny(unused_mut)]
5
6//! mnemonic-16bit is a mnemonic library that will take any binary data and convert it into a
7//! phrase which is more human friendly. Each word of the phrase maps to 16 bits, where the first
8//! 10 bits are represented by one word from the seed15 dictionary, and the remaining 6 bits are
9//! represented by a number between 0 and 63. If the number is '64', that signifies that the word
10//! only represents 1 byte instead of 2. Only the final word of a phrase may use the numerical
11//! suffix 64.
12//!
13//! ```
14//! use mnemonic_16bit::{binary_to_phrase, phrase_to_binary};
15//!
16//! fn main() {
17//!     let my_data = [0u8; 2];
18//!     let phrase = binary_to_phrase(&my_data); // "abbey0"
19//!     let data = phrase_to_binary(&phrase).unwrap();
20//!     assert!(data[..] == my_data[..]);
21//! }
22//! ```
23
24use anyhow::{bail, Context, Error, Result};
25use dictionary_1024::{word_at_index, index_of_word};
26
27/// binary_to_phrase will convert a binary string to a phrase.
28pub fn binary_to_phrase(data: &[u8]) -> String {
29    // Base case, no data means no mnemonic.
30    let mut phrase = "".to_string();
31    if data.len() == 0 {
32        return phrase;
33    }
34
35    // Parse out all of the even-numbered bytes.
36    let mut i = 0;
37    while i+1 < data.len() {
38        // Determine the dictionary offset.
39        let mut word_index = data[i] as u16;
40        word_index *= 4;
41        let word_bits = data[i+1] / 64;
42        word_index += word_bits as u16;
43        let word = word_at_index(word_index as usize);
44
45        // Determine the accompanying number.
46        let num = data[i+1] % 64;
47
48        // Compose the word into the phrase.
49        if phrase.len() != 0 {
50            phrase += " ";
51        }
52        phrase += &word;
53        phrase += &format!("{}", num);
54        i += 2;
55    }
56
57    // Parse out the final word.
58    if data.len() % 2 == 1 {
59        let word = word_at_index(data[i] as usize);
60        if phrase.len() != 0 {
61            phrase += " ";
62        }
63        phrase += &word;
64        phrase += "64";
65    }
66
67    phrase
68}
69
70/// phrase_to_binary is the inverse of binary_to_phrase, it will take a mnonmic-16bit phrase and
71/// parse it into a set of bytes.
72pub fn phrase_to_binary(phrase: &str) -> Result<Vec<u8>, Error> {
73    if phrase == "" {
74        return Ok(vec![0u8; 0]);
75    }
76
77    // Parse the words one at a time.
78    let mut finalized = false;
79    let mut result: Vec<u8> = Vec::new();
80    let words = phrase.split(" ");
81    for word in words {
82        if finalized {
83            bail!("only the last word may contain the number '64'");
84        }
85
86        // Make sure there are only numeric characters at the end of the string.
87        let mut digits = 0;
88        for c in word.chars() {
89            if digits > 0 && !c.is_ascii_digit() {
90                bail!("number must appear as suffix only");
91            }
92            if digits > 1 {
93                bail!("number must be at most 2 digits");
94            }
95            if c.is_ascii_digit() {
96                digits += 1;
97            }
98        }
99        if digits == 0 {
100            bail!("word must have a numerical suffix");
101        }
102
103        // We have validated the word, now we need to parse the bytes. We start with the numerical
104        // suffix because that indicates whether we are pulling 8 bits from the word or 10.
105        let numerical_suffix;
106        if digits == 1 {
107            numerical_suffix = &word[word.len()-1..];
108        } else {
109            numerical_suffix = &word[word.len()-2..];
110        }
111
112        // Parse the rest of the data based on whether the final digit is 64 or less.
113        if numerical_suffix == "64" {
114            finalized = true;
115            let word_index = index_of_word(word).context(format!("invalid word {} in phrase", word))?;
116            if word_index > 255 {
117                bail!("final word is invalid, needs to be among the first 255 words in the dictionary");
118            }
119            result.push(word_index as u8);
120        } else {
121            let mut bits = index_of_word(word).context(format!("invalid word {} in phrase", word))? as u16;
122            bits *= 64;
123            let numerical_bits: u16 = numerical_suffix.parse().unwrap();
124            if numerical_bits > 64 {
125                bail!("numerical suffix must have a value [0, 64]");
126            }
127            bits += numerical_bits;
128            result.push((bits / 256) as u8);
129            result.push((bits % 256) as u8);
130        }
131    }
132
133    Ok(result)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use userspace_rng::Csprng;
140    use rand_core::RngCore;
141
142    #[test]
143    // Try a bunch of binary arrays and see that they all correctly convert into phrases and then
144    // back into the same binary.
145    fn check_seed_phrases() {
146        // Try empty array.
147        let basic = [0u8; 0];
148        let phrase = binary_to_phrase(&basic);
149        let result = phrase_to_binary(&phrase).unwrap();
150        assert!(basic[..] == result[..]);
151
152        // Try all possible 1 byte values.
153        for i in 0..=255 {
154            let basic = [i as u8; 1];
155            let phrase = binary_to_phrase(&basic);
156            let result = phrase_to_binary(&phrase).unwrap();
157            assert!(basic[..] == result[..]);
158        }
159
160        // Try zero values for all possible array sizes 0-255.
161        for i in 0..=255 {
162            let basic = vec![0u8; i];
163            let phrase = binary_to_phrase(&basic);
164            let result = phrase_to_binary(&phrase).unwrap();
165            assert!(basic[..] == result[..]);
166        }
167
168        // Try random data for all array sizes 0-255, 8 variations each size.
169        let mut rng = Csprng {};
170        for _ in 0..8 {
171            for i in 0..=255 {
172                let mut basic = vec![0u8; i];
173                rng.fill_bytes(&mut basic);
174                let phrase = binary_to_phrase(&basic);
175                let result = phrase_to_binary(&phrase).unwrap();
176                assert!(basic[..] == result[..]);
177            }
178        }
179
180        // Try all possible 2 byte values.
181        for i in 0..=255 {
182            for j in 0..=255 {
183                let mut basic = [0u8; 2];
184                basic[0] = i;
185                basic[1] = j;
186                let phrase = binary_to_phrase(&basic);
187                let result = phrase_to_binary(&phrase).unwrap();
188                assert!(basic[..] == result[..]);
189            }
190        }
191    }
192
193    #[test]
194    // Check a variety of invalid phrases.
195    fn check_bad_phrases() {
196        phrase_to_binary("a").unwrap_err();
197        phrase_to_binary("a64").unwrap_err();
198        phrase_to_binary("abbey").unwrap_err();
199        phrase_to_binary("abbey65").unwrap_err();
200        phrase_to_binary("yacht64").unwrap_err();
201        phrase_to_binary("sugar21 ab55 mob32").unwrap_err();
202        phrase_to_binary("sugar21 toffee mob32").unwrap_err();
203
204        // This one should work even though we trucated the words.
205        phrase_to_binary("sug21 tof21 mob32").unwrap();
206    }
207}