1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3#![deny(unused_must_use)]
4#![deny(unused_mut)]
5
6use crate::Seed;
12use anyhow::{bail, Error, Result};
13use dictionary_1024::{index_of_word, word_at_index, words_match};
14use sha2::{Digest, Sha256};
15
16
17pub const SEED_ENTROPY_WORDS: usize = 13;
20
21pub const SEED_CHECKSUM_WORDS: usize = 2;
26
27pub fn seed_to_seed_phrase(seed: Seed) -> String {
29 let mut phrase: String = "".to_string();
31 let mut current_byte = 0;
32 let mut current_bit = 0;
33 for i in 0..SEED_ENTROPY_WORDS {
34 let mut bits = 10;
36 if i == SEED_ENTROPY_WORDS - 1 {
37 bits = 8;
38 }
39
40 let mut word_index: usize = 0;
42 for j in 0..bits {
43 let bit_is_set = (seed[current_byte] & (1 << (8 - current_bit - 1))) > 0;
45 if bit_is_set {
46 word_index |= 1 << (bits - j - 1);
47 }
48
49 current_bit += 1;
51 if current_bit == 8 {
52 current_bit = 0;
53 current_byte += 1;
54 }
55 }
56
57 if i != 0 {
59 phrase += " ";
60 }
61 phrase += &word_at_index(word_index);
62 }
63
64 let checksum_words = seed_to_checksum_words(seed);
66 phrase += " ";
67 phrase += &checksum_words[0];
68 phrase += " ";
69 phrase += &checksum_words[1];
70 phrase
71}
72
73pub fn seed_phrase_to_seed(phrase: &str) -> Result<Seed, Error> {
75 let all_words: Vec<&str> = phrase.split(' ').collect();
77 let expected_words = SEED_ENTROPY_WORDS + SEED_CHECKSUM_WORDS;
78 if all_words.len() != expected_words {
79 bail!(
80 "expecting {} words but got {} words",
81 expected_words,
82 all_words.len()
83 );
84 }
85
86 let mut seed: Seed = [0u8; 16];
89 let mut current_byte = 0;
90 let mut current_bit = 0;
91 for i in 0..SEED_ENTROPY_WORDS {
92 let word_index = index_of_word(all_words[i])?;
93
94 let mut bits = 10;
96 if i == SEED_ENTROPY_WORDS - 1 {
97 bits = 8;
98 if word_index > 255 {
99 bail!(
100 "seed phrase is not valid: {} cannot be the 13th word prefix",
101 &all_words[SEED_ENTROPY_WORDS - 1]
102 );
103 }
104 }
105 for j in 0..bits {
106 let bit_is_set = (word_index & (1 << (bits - j - 1))) > 0;
108 if bit_is_set {
109 seed[current_byte] |= 1 << (8 - current_bit - 1);
110 }
111
112 current_bit += 1;
114 if current_bit == 8 {
115 current_bit = 0;
116 current_byte += 1;
117 }
118 }
119 }
120
121 let checksum_words = seed_to_checksum_words(seed);
123 if !words_match(&checksum_words[0], all_words[SEED_ENTROPY_WORDS]) {
124 bail!(
125 "first checksum word is incorrect, expecting prefix {} but got {}",
126 checksum_words[0],
127 all_words[SEED_ENTROPY_WORDS]
128 );
129 }
130 if !words_match(&checksum_words[1], all_words[SEED_ENTROPY_WORDS + 1]) {
131 bail!(
132 "second checksum word is incorrect, expecting prefix {} but got {}",
133 checksum_words[1],
134 all_words[SEED_ENTROPY_WORDS + 1]
135 );
136 }
137
138 Ok(seed)
140}
141
142fn seed_to_checksum_words(seed: Seed) -> [String; SEED_CHECKSUM_WORDS] {
144 let mut hasher = Sha256::new();
146 hasher.update(&seed);
147 let r = hasher.finalize();
148 let mut result = [0u8; 32];
149 result.copy_from_slice(&r);
150
151 let mut word1: usize = (result[0] as usize) << 8;
153 word1 += result[1] as usize;
154 word1 >>= 6;
155 let mut word2: usize = (result[1] as usize) << 10;
156 word2 &= 0xffff;
157 word2 += (result[2] as usize) << 2;
158 word2 >>= 6;
159 [word_at_index(word1), word_at_index(word2)]
160}
161
162pub fn valid_seed_phrase(phrase: &str) -> Result<(), Error> {
164 match seed_phrase_to_seed(phrase) {
165 Ok(_) => Ok(()),
166 Err(e) => bail!("seed phrase invalid: {}", e),
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::random_seed;
174
175 fn verify_conversion(seed: Seed) {
178 let phrase = seed_to_seed_phrase(seed);
179 println!("{}", phrase);
180 valid_seed_phrase(&phrase).unwrap();
181 let seed_conf = match seed_phrase_to_seed(&phrase) {
182 Ok(s) => s,
183 Err(e) => panic!("verify_conversion failed: {}\n\t{:?}", e, seed),
184 };
185 if seed != seed_conf {
186 panic!(
187 "seed conversion failed: \n\t{:?}\n\t{:?}\n\t{}",
188 seed, seed_conf, phrase
189 );
190 }
191 }
192
193 #[test]
194 fn check_unhappy_seeds() {
196 let good_seed = random_seed();
197 let good_phrase = seed_to_seed_phrase(good_seed);
198
199 let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
201 let wai0 = word_at_index(0);
202 let wai1 = word_at_index(1);
203 let wai2 = word_at_index(2);
204 phrase_words[0] = &wai0;
205 phrase_words[1] = &wai1;
206 phrase_words[2] = &wai2;
207 let bad_phrase = phrase_words.join(" ");
208 valid_seed_phrase(&bad_phrase).unwrap_err();
209
210 let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
212 phrase_words[0] = "ab";
213 let bad_phrase = phrase_words.join(" ");
214 valid_seed_phrase(&bad_phrase).unwrap_err();
215
216 let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
218 if phrase_words[14] == word_at_index(0) {
219 phrase_words[14] = &wai1;
220 } else {
221 phrase_words[14] = &wai0;
222 }
223 let bad_phrase = phrase_words.join(" ");
224 valid_seed_phrase(&bad_phrase).unwrap_err();
225
226 let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
228 phrase_words[0] = "abx";
229 let bad_phrase = phrase_words.join(" ");
230 valid_seed_phrase(&bad_phrase).unwrap_err();
231
232 let mut phrase_words: Vec<&str> = good_phrase.split(" ").collect();
234 phrase_words.push(&wai0);
235 let bad_phrase = phrase_words.join(" ");
236 valid_seed_phrase(&bad_phrase).unwrap_err();
237
238 let phrase_words: Vec<&str> = good_phrase.split(" ").collect();
240 let bad_phrase = phrase_words[..14].join(" ");
241 valid_seed_phrase(&bad_phrase).unwrap_err();
242 }
243
244 #[test]
245 fn check_seed_phrases() {
248 let mut seed = [0u8; 16];
250 verify_conversion(seed);
251 seed[0] = 185;
252 verify_conversion(seed);
253 seed[1] = 46;
254 verify_conversion(seed);
255 seed[2] = 7;
256 verify_conversion(seed);
257 seed[3] = 1;
258 verify_conversion(seed);
259 seed[4] = 254;
260 verify_conversion(seed);
261 seed[5] = 2;
262 verify_conversion(seed);
263
264 for _ in 0..1000 {
266 let seed = random_seed();
267 verify_conversion(seed);
268 }
269
270 for _ in 0..1000 {
274 let seed = random_seed();
275 let phrase = seed_to_seed_phrase(seed);
276 let mut words: Vec<&str> = phrase.split(" ").collect();
277
278 let word_index = index_of_word(words[12]).unwrap();
280 if word_index > 255 {
281 panic!("seed generated randomly with 13th word out of bounds");
282 }
283 let wai = word_at_index(word_index + 256);
285 words[12] = &wai;
286 let mut altered_phrase = words[0].to_string();
287 for i in 1..words.len() {
288 altered_phrase += " ";
289 altered_phrase += words[i];
290 }
291 match valid_seed_phrase(&altered_phrase) {
292 Ok(()) => panic!("phrase should not be valid after manipulation"),
293 Err(_) => {}
294 };
295 }
296 }
297}