crypto_wallet_gen/mnemonics/
bip39.rs1use anyhow::Result;
2use bip39::{Language, Mnemonic as _Mnemonic, Seed as _Seed};
3use rand::RngCore;
4
5use super::{Mnemonic, MnemonicFactory};
6use crate::bip32::HDPrivKey;
7use crate::random::secure_rng;
8use crate::seed::Seed;
9
10const LANG: Language = Language::English;
11
12#[derive(Debug)]
13pub struct Bip39Mnemonic {
14 mnemonic: _Mnemonic,
16}
17
18impl MnemonicFactory for Bip39Mnemonic {
19 fn generate() -> Result<Self> {
20 const ENTROPY_LENGTH: usize = 32;
21 let mut rng = secure_rng()?;
23 let mut entropy: [u8; ENTROPY_LENGTH] = [0; ENTROPY_LENGTH];
24 rng.fill_bytes(&mut entropy);
25 let mnemonic = _Mnemonic::from_entropy(&entropy, LANG).expect("Invalid key length");
26 Ok(Self { mnemonic })
27 }
28
29 fn from_phrase(phrase: &str) -> Result<Self> {
30 let mnemonic = _Mnemonic::from_phrase(phrase, LANG)?;
31 Ok(Self { mnemonic })
32 }
33
34 fn validate(phrase: &str) -> Result<()> {
35 _Mnemonic::validate(phrase, LANG)
36 }
37}
38
39impl Mnemonic for Bip39Mnemonic {
40 fn phrase(&self) -> &str {
41 self.mnemonic.phrase()
42 }
43
44 fn into_phrase(self) -> String {
45 self.mnemonic.into_phrase()
46 }
47
48 fn to_private_key(&self, password: &str) -> Result<HDPrivKey> {
49 let seed = Seed::from_bytes(_Seed::new(&self.mnemonic, password).as_bytes().to_vec());
50 HDPrivKey::new(seed)
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 fn expect_generated_key_is(expected_key: &str, phrase: &str, password: &str) {
59 assert_eq!(
60 expected_key,
61 Bip39Mnemonic::from_phrase(phrase)
62 .unwrap()
63 .to_private_key(password)
64 .unwrap()
65 .to_base58()
66 );
67 }
68
69 #[test]
70 fn twelve_words_without_password() {
71 expect_generated_key_is(
73 "xprv9s21ZrQH143K2cidnrzfWcHRJ23QxfAEoFdVkBgbT9mns2FPMBWZwnXZbhXsVXgSzmE2JqHmVhAna7E7L6WQ6DKagT3f6fA6bwVwkWtaSLp",
74 "lunch blanket cruise chair question good market allow blue celery little void",
75 "");
76 }
77
78 #[test]
79 fn twelve_words_with_password() {
80 expect_generated_key_is(
82 "xprv9s21ZrQH143K3wy3DhgTQ44zJb99zRLbhtrp6t3pitm9jTwaFMghhdNosoeCTy7GDJSSh3F9aenvk6WQDAU37yhqTHybANPvLgAE9s9vL7X",
83 "lunch blanket cruise chair question good market allow blue celery little void",
84 "my password");
85 }
86
87 #[test]
88 fn fifteen_words_without_password() {
89 expect_generated_key_is(
91 "xprv9s21ZrQH143K3zBjoLR71dBPE3pKi62h97rKgh5J6TdveEMFB71MukBF12jB8vWhXzV8DYbxL9V3PqdRQBsKkYtjf3BZonWcV7WHvByhpk3",
92 "mirror distance build unaware current concert link chapter resemble tuition main rent echo drum dolphin",
93 "");
94 }
95
96 #[test]
97 fn fifteen_words_with_password() {
98 expect_generated_key_is(
100 "xprv9s21ZrQH143K4a1FCVYWCbiLVFXj2m9k2MwU19Kc7nFzyFzXLnRV2Ka5pNT4Tw1DPMXWjXSFbZbzvpv9MGDMfTuiMUCnSATsaq8gA5kfERZ",
101 "mirror distance build unaware current concert link chapter resemble tuition main rent echo drum dolphin",
102 "my password");
103 }
104
105 #[test]
106 fn eighteen_words_without_password() {
107 expect_generated_key_is(
109 "xprv9s21ZrQH143K4SCLnE8JFuAe7q83dNbnd1VhH7pLchL5wXYQRg9bJguPcX8fCTDWiMndRLt7FCZA9zozQCKGn5CnCbx3zErw48XvYEnMTvg",
110 "blush section drift canoe reform friend rose cherry assume supreme home hub goat arena jazz absurd emotion hidden",
111 "");
112 }
113
114 #[test]
115 fn eighteen_words_with_password() {
116 expect_generated_key_is(
118 "xprv9s21ZrQH143K3hzQrTYxsE8ASRQnymatPj7QnK83E9yL7c7ynLU4kx6LN7MCpy9vwei6stthAh6nBB8TmWxDr7FssJMGt2YN3jfT9Ksj6ih",
119 "blush section drift canoe reform friend rose cherry assume supreme home hub goat arena jazz absurd emotion hidden",
120 "my password");
121 }
122
123 #[test]
124 fn twentyone_words_without_password() {
125 expect_generated_key_is(
127 "xprv9s21ZrQH143K3LDb5bbmmEHpowwV9JgcSJ7nJPmiNCMbS2EisLt1iHXrYnWubffdpCgTgKR4Km6VVrPwgf4TgSzD4QNpgJ3L1cAAEEeVuw7",
128 "include disagree sentence junior gospel engage whip old boost scrap someone helmet list best afraid favorite gold antenna before peasant buffalo",
129 "");
130 }
131
132 #[test]
133 fn twentyone_words_with_password() {
134 expect_generated_key_is(
136 "xprv9s21ZrQH143K4MHsTjvdG9QEjbKwrZGKsjNxxCSwkDjnVM91M6d4e5XR2bnva5GNgSf2pdvg9JubTa9UMNEDisAKD6Dg7DW74xPgr91KcNA",
137 "include disagree sentence junior gospel engage whip old boost scrap someone helmet list best afraid favorite gold antenna before peasant buffalo",
138 "my password");
139 }
140
141 #[test]
142 fn twentyfour_words_without_password() {
143 expect_generated_key_is(
145 "xprv9s21ZrQH143K3ss3HXZFVjYApfQkdokhpiGjpXnm8y8sfbtb4ydSwsPXUSj7g1mY8VhJH3iY1ZUrgdbcFmvmEhzq6R35WW4JNBSZz4uLCXN",
146 "table car outdoor twist dutch auction monitor rude pumpkin very disease ability hope area metal brisk luggage tell ribbon profit various lake topic exist",
147 "");
148 }
149
150 #[test]
151 fn twentyfour_words_with_password() {
152 expect_generated_key_is(
154 "xprv9s21ZrQH143K4XjwHvT3EEwu2fc9T3YVyXTq96SUnpRviKA49y1Lf4UxPd3t5DNRj6xffnhZM2pRVYr3BjUCQ8RCvJEWxQqUBeTRWKuNqp2",
155 "table car outdoor twist dutch auction monitor rude pumpkin very disease ability hope area metal brisk luggage tell ribbon profit various lake topic exist",
156 "my password");
157 }
158
159 #[test]
160 fn generated_phrase_is_24_words() {
161 let phrase = Bip39Mnemonic::generate().unwrap().into_phrase();
162 assert_eq!(23, phrase.chars().filter(|a| *a == ' ').count());
163 }
164
165 #[test]
166 fn generated_phrase_is_valid() {
167 Bip39Mnemonic::validate(Bip39Mnemonic::generate().unwrap().phrase()).unwrap();
168 }
169
170 #[test]
171 fn validate_valid_24word_phrase() {
172 Bip39Mnemonic::validate("desert armed renew matrix congress order remove lab travel shallow there tool symbol three radio exhibit pledge alcohol quit host rare noble dose eager").unwrap();
173 }
174
175 #[test]
176 fn validate_valid_21word_phrase() {
177 Bip39Mnemonic::validate("morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm hobby").unwrap();
178 }
179
180 #[test]
181 fn validate_valid_18word_phrase() {
182 Bip39Mnemonic::validate("slice lift violin movie shield copy tail arrow idle lift knock fossil leave lawsuit tennis sight travel vivid").unwrap();
183 }
184
185 #[test]
186 fn validate_valid_15word_phrase() {
187 Bip39Mnemonic::validate("call oval opinion exhibit limit write fine prepare sleep possible extend language split kidney desert").unwrap();
188 }
189
190 #[test]
191 fn validate_valid_12word_phrase() {
192 Bip39Mnemonic::validate(
193 "tornado ginger error because arrange lake scale unfold palm theme frozen sick",
194 )
195 .unwrap();
196 }
197
198 #[test]
199 fn validate_invalid_20word_phrase() {
200 let err = Bip39Mnemonic::validate(
201 "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm",
202 )
203 .unwrap_err();
204 assert!(err
205 .to_string()
206 .contains("invalid number of words in phrase"))
207 }
208
209 #[test]
210 fn validate_invalid_21word_phrase() {
211 let err = Bip39Mnemonic::validate(
212 "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm prepare",
213 )
214 .unwrap_err();
215 assert!(err.to_string().contains("invalid checksum"))
216 }
217
218 #[test]
219 fn from_invalid_20word_phrase() {
220 let err = Bip39Mnemonic::from_phrase(
221 "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm",
222 )
223 .unwrap_err();
224 assert!(err
225 .to_string()
226 .contains("invalid number of words in phrase"))
227 }
228
229 #[test]
230 fn from_invalid_21word_phrase() {
231 let err = Bip39Mnemonic::from_phrase(
232 "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm prepare",
233 )
234 .unwrap_err();
235 assert!(err.to_string().contains("invalid checksum"))
236 }
237}