1use std::fmt;
2
3use rand_core::{CryptoRng, RngCore};
4use sha2::Digest;
5
6mod words;
7use words::BIP39_WORDS;
8
9pub const NUM_PBKDF2_ROUNDS: u32 = 2048;
10pub const NUM_WORDS_SHORT: usize = 12;
11pub const NUM_WORDS_LONG: usize = 24;
12
13pub const NUM_ENTROPY_BITS_SHORT: usize = 128;
14pub const NUM_ENTROPY_BITS_LONG: usize = 256;
15pub const NUM_BITS_PER_WORD: usize = 11;
16pub const NUM_BITS_PER_BYTE: usize = 8;
17
18pub enum SeedPhraseType {
21 MinimumLength,
23 MaximumLength,
25}
26
27impl SeedPhraseType {
28 pub fn from_length(len: usize) -> anyhow::Result<Self> {
29 match len {
30 NUM_WORDS_SHORT => Ok(SeedPhraseType::MinimumLength),
31 NUM_WORDS_LONG => Ok(SeedPhraseType::MaximumLength),
32 _ => Err(anyhow::anyhow!("invalid seed phrase length")),
33 }
34 }
35
36 pub fn from_randomness_length(len: usize) -> anyhow::Result<Self> {
37 match len * NUM_BITS_PER_BYTE {
38 NUM_ENTROPY_BITS_SHORT => Ok(SeedPhraseType::MinimumLength),
39 NUM_ENTROPY_BITS_LONG => Ok(SeedPhraseType::MaximumLength),
40 _ => Err(anyhow::anyhow!("invalid randomness length")),
41 }
42 }
43
44 pub fn num_words(&self) -> usize {
45 match self {
46 SeedPhraseType::MinimumLength => NUM_WORDS_SHORT,
47 SeedPhraseType::MaximumLength => NUM_WORDS_LONG,
48 }
49 }
50
51 pub fn num_checksum_bits(&self) -> usize {
52 match self {
53 SeedPhraseType::MinimumLength => NUM_ENTROPY_BITS_SHORT / 32,
54 SeedPhraseType::MaximumLength => NUM_ENTROPY_BITS_LONG / 32,
55 }
56 }
57
58 pub fn num_entropy_bits(&self) -> usize {
59 match self {
60 SeedPhraseType::MinimumLength => NUM_ENTROPY_BITS_SHORT,
61 SeedPhraseType::MaximumLength => NUM_ENTROPY_BITS_LONG,
62 }
63 }
64
65 pub fn num_total_bits(&self) -> usize {
66 self.num_entropy_bits() + self.num_checksum_bits()
67 }
68}
69
70#[derive(Clone, Debug)]
72pub struct SeedPhrase(pub Vec<String>);
73
74impl SeedPhrase {
75 pub fn generate<R: RngCore + CryptoRng>(mut rng: R) -> Self {
77 let mut randomness = [0u8; NUM_ENTROPY_BITS_LONG / NUM_BITS_PER_BYTE];
78 rng.fill_bytes(&mut randomness);
79 Self::from_randomness(&randomness)
80 }
81
82 pub fn short_generate<R: RngCore + CryptoRng>(mut rng: R) -> Self {
84 let mut randomness = [0u8; NUM_ENTROPY_BITS_SHORT / NUM_BITS_PER_BYTE];
85 rng.fill_bytes(&mut randomness);
86 Self::from_randomness(&randomness)
87 }
88
89 pub fn from_randomness(randomness: &[u8]) -> Self {
91 let seed_phrase_type = SeedPhraseType::from_randomness_length(randomness.len())
94 .expect("can get seed phrase type from randomness length");
95
96 let num_entropy_bits = seed_phrase_type.num_entropy_bits();
97 let mut bits = vec![false; seed_phrase_type.num_total_bits()];
98 for (i, bit) in bits[0..num_entropy_bits].iter_mut().enumerate() {
99 *bit = (randomness[i / NUM_BITS_PER_BYTE] & (1 << (7 - (i % NUM_BITS_PER_BYTE)))) > 0
100 }
101
102 let mut hasher = sha2::Sha256::new();
109 hasher.update(randomness);
110 let checksum = hasher.finalize()[0];
111 for (i, bit) in bits[num_entropy_bits..].iter_mut().enumerate() {
112 *bit = (checksum & (1 << (7 - (i % NUM_BITS_PER_BYTE)))) > 0
113 }
114
115 let mut words = vec![String::new(); seed_phrase_type.num_words()];
118 for (i, word) in words.iter_mut().enumerate() {
119 let bits_this_word = &bits[i * NUM_BITS_PER_WORD..(i + 1) * NUM_BITS_PER_WORD];
120 let word_index = convert_bits_to_usize(bits_this_word);
121 *word = BIP39_WORDS[word_index].to_string();
122 }
123 SeedPhrase(words)
124 }
125
126 pub fn length(&self) -> usize {
128 self.0.len()
129 }
130
131 fn verify_checksum(&self) -> anyhow::Result<()> {
133 let seed_phrase_type = SeedPhraseType::from_length(self.length())?;
134 let mut bits = vec![false; seed_phrase_type.num_total_bits()];
135 for (i, word) in self.0.iter().enumerate() {
136 if !BIP39_WORDS.contains(&word.as_str()) {
137 anyhow::bail!("invalid word in BIP39 seed phrase");
138 }
139
140 let word_index = BIP39_WORDS
141 .iter()
142 .position(|&x| x == word)
143 .expect("can get index of word");
144 let word_bits = &mut bits[i * NUM_BITS_PER_WORD..(i + 1) * NUM_BITS_PER_WORD];
145 word_bits
146 .iter_mut()
147 .enumerate()
148 .for_each(|(j, bit)| *bit = (word_index >> (NUM_BITS_PER_WORD - 1 - j)) & 1 == 1);
149 }
150
151 let mut randomness = vec![0u8; seed_phrase_type.num_entropy_bits() / NUM_BITS_PER_BYTE];
152 for (i, random_byte) in randomness.iter_mut().enumerate() {
153 let bits_this_byte = &bits[i * NUM_BITS_PER_BYTE..(i + 1) * NUM_BITS_PER_BYTE];
154 *random_byte = convert_bits_to_usize(bits_this_byte) as u8;
155 }
156
157 let mut hasher = sha2::Sha256::new();
158 hasher.update(randomness);
159 let calculated_checksum = hasher.finalize()[0];
160
161 let mut calculated_checksum_bits = vec![false; seed_phrase_type.num_checksum_bits()];
162 for (i, bit) in calculated_checksum_bits.iter_mut().enumerate() {
163 *bit = (calculated_checksum & (1 << (7 - (i % NUM_BITS_PER_BYTE)))) > 0
164 }
165
166 let checksum_bits = &bits[seed_phrase_type.num_entropy_bits()..];
167 for (expected_bit, checksum_bit) in checksum_bits.iter().zip(calculated_checksum_bits) {
168 if checksum_bit != *expected_bit {
169 return Err(anyhow::anyhow!("seed phrase checksum did not validate"));
170 }
171 }
172 Ok(())
173 }
174}
175
176impl fmt::Display for SeedPhrase {
177 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178 for (i, word) in self.0.iter().enumerate() {
179 if i > 0 {
180 f.write_str(" ")?;
181 }
182 f.write_str(word)?;
183 }
184 Ok(())
185 }
186}
187
188impl std::str::FromStr for SeedPhrase {
189 type Err = anyhow::Error;
190
191 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 let words = s
193 .split_whitespace()
194 .map(|w| w.to_lowercase())
195 .collect::<Vec<String>>();
196
197 if words.len() != NUM_WORDS_LONG && words.len() != NUM_WORDS_SHORT {
198 anyhow::bail!(
199 "seed phrases should have {} or {} words",
200 NUM_WORDS_LONG,
201 NUM_WORDS_SHORT
202 );
203 }
204
205 let seed_phrase = SeedPhrase(words);
206 seed_phrase.verify_checksum()?;
207
208 Ok(seed_phrase)
209 }
210}
211
212fn convert_bits_to_usize(bits: &[bool]) -> usize {
213 bits.iter()
214 .enumerate()
215 .map(|(i, bit)| if *bit { 1 << (bits.len() - 1 - i) } else { 0 })
216 .sum::<usize>()
217}
218
219#[cfg(test)]
220mod tests {
221 use hmac::Hmac;
222 use pbkdf2::pbkdf2;
223 use std::str::FromStr;
224
225 use super::*;
226
227 #[test]
228 fn bip39_mnemonic_derivation() {
229 let randomness_arr = [
231 "00000000000000000000000000000000",
232 "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
233 "80808080808080808080808080808080",
234 "ffffffffffffffffffffffffffffffff",
235 "9e885d952ad362caeb4efe34a8e91bd2",
236 "c0ba5a8e914111210f2bd131f3d5e08d",
237 "23db8160a31d3e0dca3688ed941adbf3",
238 "f30f8c1da665478f49b001d94c5fc452",
239 "0000000000000000000000000000000000000000000000000000000000000000",
240 "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
241 "8080808080808080808080808080808080808080808080808080808080808080",
242 "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
243 "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
244 "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
245 "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
246 "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
247 ];
248 let expected_phrase_arr = [
249 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
250 "legal winner thank year wave sausage worth useful legal winner thank yellow",
251 "letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
252 "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
253 "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
254 "scheme spot photo card baby mountain device kick cradle pact join borrow",
255 "cat swing flag economy stadium alone churn speed unique patch report train",
256 "vessel ladder alter error federal sibling chat ability sun glass valve picture",
257 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
258 "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
259 "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
260 "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
261 "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
262 "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
263 "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
264 "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
265 ];
266 let seed_result_arr = [
267 "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
268 "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
269 "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
270 "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
271 "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
272 "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
273 "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
274 "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
275 "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
276 "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
277 "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
278 "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
279 "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
280 "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
281 "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
282 "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
283 ];
284
285 for (i, (hex_randomness, expected_phrase)) in randomness_arr
286 .iter()
287 .zip(expected_phrase_arr.iter())
288 .enumerate()
289 {
290 let randomness = hex::decode(hex_randomness).expect("can decode test vector");
291 let actual_phrase = SeedPhrase::from_randomness(&randomness[..]);
292 assert_eq!(actual_phrase.to_string(), *expected_phrase);
293 actual_phrase
294 .verify_checksum()
295 .expect("checksum should validate");
296
297 let password = format!("{actual_phrase}");
298 let mut seed_bytes = [0u8; 64];
299 pbkdf2::<Hmac<sha2::Sha512>>(
300 password.as_bytes(),
301 "mnemonicTREZOR".as_bytes(),
302 NUM_PBKDF2_ROUNDS,
303 &mut seed_bytes,
304 )
305 .expect("seed phrase hash always succeeds");
306 let seed_result = hex::encode(seed_bytes);
307 assert_eq!(seed_result, seed_result_arr[i]);
308 }
309 }
310
311 #[test]
312 fn seed_phrase_from_str() {
313 let invalid_phrases = [
314 "too short",
315 "zoo zoooooooo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth vote", ];
318 for phrase in invalid_phrases {
319 assert!(SeedPhrase::from_str(phrase).is_err());
320 }
321
322 let valid_phrases = [
323 "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
324 "ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO zoo ZOO VOTE",
325 "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title"
326 ];
327 for phrase in valid_phrases {
328 assert!(SeedPhrase::from_str(phrase).is_ok());
329 }
330 }
331}