crypto_wallet_gen/mnemonics/
bip39.rs

1use 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    // wagyu_bitcoin::mnemonic::BitcoinMnemonic::to_seed() is private, so we need to use the bip39 crate instead.
15    mnemonic: _Mnemonic,
16}
17
18impl MnemonicFactory for Bip39Mnemonic {
19    fn generate() -> Result<Self> {
20        const ENTROPY_LENGTH: usize = 32;
21        // XOR an OS rng and a pseudo rng to get our entropy. Probably not necessary but doesn't hurt either.
22        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        // created with https://iancoleman.io/bip39/
72        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        // created with https://iancoleman.io/bip39/
81        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        // created with https://iancoleman.io/bip39/
90        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        // created with https://iancoleman.io/bip39/
99        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        // created with https://iancoleman.io/bip39/
108        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        // created with https://iancoleman.io/bip39/
117        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        // created with https://iancoleman.io/bip39/
126        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        // created with https://iancoleman.io/bip39/
135        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        // created with https://iancoleman.io/bip39/
144        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        // created with https://iancoleman.io/bip39/
153        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}