ethers_wallet_rs/hd_wallet/
bip39.rs

1use std::{collections::HashMap, io::Write, str::FromStr};
2
3use rand::{rngs::OsRng, CryptoRng, Rng};
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7/// The minimum number of words in a mnemonic.
8#[allow(unused)]
9const MIN_NB_WORDS: usize = 12;
10
11/// The maximum number of words in a mnemonic.
12const MAX_NB_WORDS: usize = 24;
13
14#[derive(Debug, Error)]
15pub enum Bip39Error {
16    #[error("Invalid entropy length {0}, expect entropy len [16,64] and a multiple of 4")]
17    InvalidEntropyLength(usize),
18
19    #[error("Invalid num of mnemonic words {0}, should be 12,15,18,21 or 24")]
20    MnemonicNumOfWords(usize),
21
22    #[error("Invalid mnemonic word {0}, not found in dictionary")]
23    MnemonicWord(String),
24
25    #[error("mnemonic checksum mismatch")]
26    Checksum,
27}
28
29#[derive(Debug, Clone)]
30pub struct Dictionary {
31    word_list: Vec<String>,
32    indexer: HashMap<String, usize>,
33}
34
35impl Dictionary {
36    pub fn new(word_list: Vec<String>) -> Self {
37        let mut indexer = HashMap::<String, usize>::new();
38
39        for (index, word) in word_list.iter().enumerate() {
40            indexer.insert(word.to_owned(), index);
41        }
42
43        Self { word_list, indexer }
44    }
45}
46
47impl FromStr for Dictionary {
48    type Err = serde_json::Error;
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        Ok(Self::new(serde_json::from_str(s)?))
51    }
52}
53
54#[derive(Debug, Clone)]
55pub struct Bip39Generator {
56    dic: Dictionary,
57}
58
59impl Bip39Generator {
60    pub fn new(dic: Dictionary) -> Self {
61        Self { dic }
62    }
63
64    pub fn gen_mnemonic<const LEN: usize>(&self) -> Result<String, Bip39Error> {
65        self.gen_mnemonic_with::<rand::rngs::OsRng, LEN>(&mut OsRng)
66    }
67
68    pub fn gen_mnemonic_with<EG, const LEN: usize>(
69        &self,
70        rng: &mut EG,
71    ) -> Result<String, Bip39Error>
72    where
73        EG: CryptoRng + Rng,
74    {
75        if LEN < 16 || LEN > 64 || LEN % 4 != 0 {
76            return Err(Bip39Error::InvalidEntropyLength(LEN));
77        }
78
79        let mut entropy = [0; LEN];
80
81        rng.fill_bytes(&mut entropy);
82
83        self.mnemonic_from_entropy(&entropy)
84    }
85
86    pub fn mnemonic_from_entropy(&self, entropy: &[u8]) -> Result<String, Bip39Error> {
87        const MAX_ENTROPY_BITS: usize = 256;
88        const MIN_ENTROPY_BITS: usize = 128;
89        const MAX_CHECKSUM_BITS: usize = 8;
90
91        let nb_bytes = entropy.len();
92        let nb_bits = nb_bytes * 8;
93
94        if nb_bits % 32 != 0 {
95            return Err(Bip39Error::InvalidEntropyLength(nb_bytes));
96        }
97        if nb_bits < MIN_ENTROPY_BITS || nb_bits > MAX_ENTROPY_BITS {
98            return Err(Bip39Error::InvalidEntropyLength(nb_bytes));
99        }
100
101        let mut hasher = Sha256::new();
102
103        hasher.write(&entropy).expect("Sha256 entropy");
104
105        let check = hasher.finalize();
106
107        let mut bits = [false; MAX_ENTROPY_BITS + MAX_CHECKSUM_BITS];
108        for i in 0..nb_bytes {
109            for j in 0..8 {
110                bits[i * 8 + j] = (entropy[i] & (1 << (7 - j))) > 0;
111            }
112        }
113        for i in 0..nb_bytes / 4 {
114            bits[8 * nb_bytes + i] = (check[i / 8] & (1 << (7 - (i % 8)))) > 0;
115        }
116
117        let mut words = vec![];
118
119        let nb_words = nb_bytes * 3 / 4;
120        for i in 0..nb_words {
121            let mut idx = 0;
122            for j in 0..11 {
123                if bits[i * 11 + j] {
124                    idx += 1 << (10 - j);
125                }
126            }
127
128            words.push(self.dic.word_list[idx].clone());
129        }
130
131        Ok(words.join(" "))
132    }
133
134    pub fn mnemonic_check<S>(&self, mnemonic: S) -> Result<(), Bip39Error>
135    where
136        S: AsRef<str>,
137    {
138        let mnemonic = mnemonic.as_ref();
139
140        let words = mnemonic.split(" ").collect::<Vec<_>>();
141
142        let num_of_words = words.len();
143
144        if num_of_words % 3 != 0 || num_of_words < 12 || num_of_words > 24 {
145            return Err(Bip39Error::MnemonicNumOfWords(num_of_words));
146        }
147
148        let mut bits = [false; MAX_NB_WORDS * 11];
149
150        for (i, word) in words.iter().enumerate() {
151            if let Some(index) = self.dic.indexer.get(*word) {
152                for j in 0..11 {
153                    bits[i * 11 + j] = index >> (10 - j) & 1 == 1;
154                }
155            } else {
156                return Err(Bip39Error::MnemonicWord(word.to_string()));
157            }
158        }
159
160        let mut entropy = [0u8; MAX_NB_WORDS / 3 * 4];
161        let nb_bytes_entropy = num_of_words / 3 * 4;
162        for i in 0..nb_bytes_entropy {
163            for j in 0..8 {
164                if bits[i * 8 + j] {
165                    entropy[i] += 1 << (7 - j);
166                }
167            }
168        }
169
170        let mut hasher = Sha256::new();
171
172        hasher.write(&entropy[0..nb_bytes_entropy]).expect("");
173
174        let check = hasher.finalize();
175
176        for i in 0..nb_bytes_entropy / 4 {
177            if bits[8 * nb_bytes_entropy + i] != ((check[i / 8] & (1 << (7 - (i % 8)))) > 0) {
178                return Err(Bip39Error::Checksum);
179            }
180        }
181
182        Ok(())
183    }
184}
185
186pub mod languages {
187    use super::Dictionary;
188
189    pub fn en_us() -> Dictionary {
190        include_str!("bip39/en_us.json")
191            .parse()
192            .expect("Load dictionary")
193    }
194}
195
196#[cfg(test)]
197mod tests {
198
199    use ethers_types_rs::bytes::bytes_from_str;
200
201    use super::{languages::en_us, Bip39Generator};
202
203    #[test]
204    fn test_checksum() {
205        let _ = pretty_env_logger::try_init();
206
207        let gen = Bip39Generator::new(en_us());
208
209        let mnemonic = gen.gen_mnemonic::<16>().expect("Generate mnemonic");
210
211        log::debug!("mnemonic: \r\n {}", mnemonic);
212
213        gen.mnemonic_check(mnemonic).expect("Mnemonic check");
214    }
215
216    #[test]
217    fn test_with_entropy() {
218        let entropy = bytes_from_str("0x7b6228a8803ad9883bca8b46c55a2c3f").expect("Parse entropy");
219
220        let gen = Bip39Generator::new(en_us());
221
222        let mnemonic = gen.mnemonic_from_entropy(&entropy).expect("From entropy");
223
224        assert_eq!(
225            mnemonic,
226            "kiwi bacon clay about pulse series upset fabric egg client mention lazy"
227        );
228    }
229
230    #[test]
231    fn teset_vectors() {
232        let test_vectors = [
233			(
234				"00000000000000000000000000000000",
235				"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
236				"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
237			),
238			(
239				"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
240				"legal winner thank year wave sausage worth useful legal winner thank yellow",
241				"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607",
242			),
243			(
244				"80808080808080808080808080808080",
245				"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
246				"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8",
247			),
248			(
249				"ffffffffffffffffffffffffffffffff",
250				"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
251				"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069",
252			),
253			(
254				"000000000000000000000000000000000000000000000000",
255				"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
256				"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa",
257			),
258			(
259				"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
260				"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
261				"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd",
262			),
263			(
264				"808080808080808080808080808080808080808080808080",
265				"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
266				"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65",
267			),
268			(
269				"ffffffffffffffffffffffffffffffffffffffffffffffff",
270				"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
271				"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528",
272			),
273			(
274				"0000000000000000000000000000000000000000000000000000000000000000",
275				"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
276				"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8",
277			),
278			(
279				"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
280				"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
281				"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87",
282			),
283			(
284				"8080808080808080808080808080808080808080808080808080808080808080",
285				"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
286				"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f",
287			),
288			(
289				"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
290				"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
291				"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad",
292			),
293			(
294				"9e885d952ad362caeb4efe34a8e91bd2",
295				"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
296				"274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028",
297			),
298			(
299				"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
300				"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
301				"628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac",
302			),
303			(
304				"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
305				"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
306				"64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440",
307			),
308			(
309				"c0ba5a8e914111210f2bd131f3d5e08d",
310				"scheme spot photo card baby mountain device kick cradle pact join borrow",
311				"ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612",
312			),
313			(
314				"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
315				"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
316				"fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d",
317			),
318			(
319				"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
320				"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
321				"72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d",
322			),
323			(
324				"23db8160a31d3e0dca3688ed941adbf3",
325				"cat swing flag economy stadium alone churn speed unique patch report train",
326				"deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5",
327			),
328			(
329				"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
330				"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
331				"4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02",
332			),
333			(
334				"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
335				"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
336				"26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d",
337			),
338			(
339				"f30f8c1da665478f49b001d94c5fc452",
340				"vessel ladder alter error federal sibling chat ability sun glass valve picture",
341				"2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f",
342			),
343			(
344				"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
345				"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
346				"7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88",
347			),
348			(
349				"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
350				"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
351				"01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998",
352			)
353		];
354
355        let gen = Bip39Generator::new(en_us());
356
357        for vector in &test_vectors {
358            let entropy = bytes_from_str(&vector.0).unwrap();
359            let mnemonic_expected = vector.1;
360
361            let mnemonic = gen.mnemonic_from_entropy(&entropy).expect("From entropy");
362
363            assert_eq!(mnemonic, mnemonic_expected);
364        }
365    }
366}