1use super::bip39_wordlists;
12use super::error::CompatError;
13use crate::primitives::hash::{pbkdf2_hmac_sha512, sha256};
14use crate::primitives::random::random_bytes;
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum Language {
19 English,
20 Japanese,
21 Korean,
22 Spanish,
23 French,
24 Italian,
25 Czech,
26 ChineseSimplified,
27 ChineseTraditional,
28}
29
30fn get_wordlist(lang: Language) -> &'static [&'static str; 2048] {
32 match lang {
33 Language::English => bip39_wordlists::english::ENGLISH,
34 Language::Japanese => bip39_wordlists::japanese::JAPANESE,
35 Language::Korean => bip39_wordlists::korean::KOREAN,
36 Language::Spanish => bip39_wordlists::spanish::SPANISH,
37 Language::French => bip39_wordlists::french::FRENCH,
38 Language::Italian => bip39_wordlists::italian::ITALIAN,
39 Language::Czech => bip39_wordlists::czech::CZECH,
40 Language::ChineseSimplified => bip39_wordlists::chinese_simplified::CHINESE_SIMPLIFIED,
41 Language::ChineseTraditional => bip39_wordlists::chinese_traditional::CHINESE_TRADITIONAL,
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct Mnemonic {
48 words: Vec<String>,
49 entropy: Vec<u8>,
50 language: Language,
51}
52
53impl Mnemonic {
54 pub fn from_entropy(entropy: &[u8], language: Language) -> Result<Self, CompatError> {
59 let ent_bits = entropy.len() * 8;
60 if !(128..=256).contains(&ent_bits) || !ent_bits.is_multiple_of(32) {
61 return Err(CompatError::InvalidEntropy(format!(
62 "entropy must be 128-256 bits in 32-bit increments, got {ent_bits} bits"
63 )));
64 }
65
66 let checksum_bits = ent_bits / 32;
67 let checksum = sha256(entropy);
68
69 let total_bits = ent_bits + checksum_bits;
71 let wordlist = get_wordlist(language);
72 let mut words = Vec::with_capacity(total_bits / 11);
73
74 for i in 0..(total_bits / 11) {
75 let mut index: u32 = 0;
76 for j in 0..11 {
77 let bit_pos = i * 11 + j;
78 let bit = if bit_pos < ent_bits {
79 (entropy[bit_pos / 8] >> (7 - (bit_pos % 8))) & 1
81 } else {
82 let cs_pos = bit_pos - ent_bits;
84 (checksum[cs_pos / 8] >> (7 - (cs_pos % 8))) & 1
85 };
86 index = (index << 1) | bit as u32;
87 }
88 words.push(wordlist[index as usize].to_string());
89 }
90
91 Ok(Mnemonic {
92 words,
93 entropy: entropy.to_vec(),
94 language,
95 })
96 }
97
98 pub fn from_random(bits: usize, language: Language) -> Result<Self, CompatError> {
102 if !(128..=256).contains(&bits) || !bits.is_multiple_of(32) {
103 return Err(CompatError::InvalidEntropy(format!(
104 "bits must be 128-256 in 32-bit increments, got {bits}"
105 )));
106 }
107 let entropy = random_bytes(bits / 8);
108 Self::from_entropy(&entropy, language)
109 }
110
111 pub fn from_string(mnemonic: &str, language: Language) -> Result<Self, CompatError> {
116 let separator = if language == Language::Japanese {
117 "\u{3000}"
118 } else {
119 " "
120 };
121
122 let word_strs: Vec<&str> = mnemonic.split(separator).collect();
123 let word_count = word_strs.len();
124
125 if !(12..=24).contains(&word_count) || !word_count.is_multiple_of(3) {
127 return Err(CompatError::InvalidMnemonic(format!(
128 "invalid word count: {word_count} (must be 12, 15, 18, 21, or 24)"
129 )));
130 }
131
132 let wordlist = get_wordlist(language);
133
134 let mut indices = Vec::with_capacity(word_count);
136 for word in &word_strs {
137 match wordlist.iter().position(|w| w == word) {
138 Some(idx) => indices.push(idx as u32),
139 None => {
140 return Err(CompatError::InvalidMnemonic(format!(
141 "word not in wordlist: {word}"
142 )));
143 }
144 }
145 }
146
147 let total_bits = word_count * 11;
149 let ent_bits = (total_bits * 32) / 33; let checksum_bits = ent_bits / 32;
151 let ent_bytes = ent_bits / 8;
152
153 let mut bits_vec: Vec<u8> = Vec::with_capacity(total_bits);
155 for idx in &indices {
156 for j in (0..11).rev() {
157 bits_vec.push(((idx >> j) & 1) as u8);
158 }
159 }
160
161 let mut entropy = vec![0u8; ent_bytes];
163 for i in 0..ent_bits {
164 if bits_vec[i] == 1 {
165 entropy[i / 8] |= 1 << (7 - (i % 8));
166 }
167 }
168
169 let checksum = sha256(&entropy);
171 for i in 0..checksum_bits {
172 let expected_bit = (checksum[i / 8] >> (7 - (i % 8))) & 1;
173 let actual_bit = bits_vec[ent_bits + i];
174 if expected_bit != actual_bit {
175 return Err(CompatError::InvalidMnemonic(
176 "checksum mismatch".to_string(),
177 ));
178 }
179 }
180
181 Ok(Mnemonic {
182 words: word_strs.iter().map(|s| s.to_string()).collect(),
183 entropy,
184 language,
185 })
186 }
187
188 pub fn check(&self) -> bool {
190 let ent_bits = self.entropy.len() * 8;
191 let checksum_bits = ent_bits / 32;
192 let checksum = sha256(&self.entropy);
193
194 let wordlist = get_wordlist(self.language);
196 let total_bits = ent_bits + checksum_bits;
197
198 for i in 0..(total_bits / 11) {
199 let mut index: u32 = 0;
200 for j in 0..11 {
201 let bit_pos = i * 11 + j;
202 let bit = if bit_pos < ent_bits {
203 (self.entropy[bit_pos / 8] >> (7 - (bit_pos % 8))) & 1
204 } else {
205 let cs_pos = bit_pos - ent_bits;
206 (checksum[cs_pos / 8] >> (7 - (cs_pos % 8))) & 1
207 };
208 index = (index << 1) | bit as u32;
209 }
210 if self.words[i] != wordlist[index as usize] {
211 return false;
212 }
213 }
214
215 true
216 }
217
218 pub fn to_seed(&self, passphrase: &str) -> Vec<u8> {
223 let mnemonic_str = self.to_phrase();
224 let salt = format!("mnemonic{passphrase}");
225 pbkdf2_hmac_sha512(mnemonic_str.as_bytes(), salt.as_bytes(), 2048, 64)
226 }
227
228 pub fn to_phrase(&self) -> String {
232 let separator = if self.language == Language::Japanese {
233 "\u{3000}"
234 } else {
235 " "
236 };
237 self.words.join(separator)
238 }
239
240 pub fn words(&self) -> &[String] {
242 &self.words
243 }
244
245 pub fn entropy(&self) -> &[u8] {
247 &self.entropy
248 }
249}
250
251impl std::fmt::Display for Mnemonic {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 write!(f, "{}", self.to_phrase())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 fn hex_to_bytes(hex: &str) -> Vec<u8> {
262 (0..hex.len())
263 .step_by(2)
264 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
265 .collect()
266 }
267
268 fn bytes_to_hex(bytes: &[u8]) -> String {
269 bytes.iter().map(|b| format!("{b:02x}")).collect()
270 }
271
272 #[derive(serde::Deserialize)]
273 struct TestVector {
274 entropy: String,
275 mnemonic: String,
276 passphrase: String,
277 seed: String,
278 }
279
280 #[derive(serde::Deserialize)]
281 struct TestVectors {
282 vectors: Vec<TestVector>,
283 }
284
285 fn load_vectors() -> TestVectors {
286 let json = include_str!("../../test-vectors/bip39_vectors.json");
287 serde_json::from_str(json).expect("failed to parse BIP39 test vectors")
288 }
289
290 #[test]
292 fn test_from_entropy_128bit() {
293 let vectors = load_vectors();
294 let v = &vectors.vectors[0]; let entropy = hex_to_bytes(&v.entropy);
296 let m = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
297 assert_eq!(m.to_string(), v.mnemonic);
298 assert_eq!(m.words().len(), 12);
299 }
300
301 #[test]
303 fn test_from_entropy_256bit() {
304 let vectors = load_vectors();
305 let v = &vectors.vectors[8]; let entropy = hex_to_bytes(&v.entropy);
307 let m = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
308 assert_eq!(m.to_string(), v.mnemonic);
309 assert_eq!(m.words().len(), 24);
310 }
311
312 #[test]
314 fn test_to_seed_with_trezor_passphrase() {
315 let vectors = load_vectors();
316 let v = &vectors.vectors[0];
317 let m = Mnemonic::from_string(&v.mnemonic, Language::English).unwrap();
318 let seed = m.to_seed(&v.passphrase);
319 assert_eq!(bytes_to_hex(&seed), v.seed);
320 }
321
322 #[test]
324 fn test_to_seed_empty_passphrase() {
325 let m = Mnemonic::from_string(
327 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
328 Language::English,
329 ).unwrap();
330 let seed = m.to_seed("");
331 assert_eq!(seed.len(), 64);
333 let trezor_seed = m.to_seed("TREZOR");
334 assert_ne!(seed, trezor_seed);
335 }
336
337 #[test]
339 fn test_check_valid() {
340 let m = Mnemonic::from_string(
341 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
342 Language::English,
343 ).unwrap();
344 assert!(m.check());
345 }
346
347 #[test]
349 fn test_check_invalid_checksum() {
350 let result = Mnemonic::from_string(
352 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
353 Language::English,
354 );
355 assert!(result.is_err());
356 }
357
358 #[test]
360 fn test_from_random_128() {
361 let m = Mnemonic::from_random(128, Language::English).unwrap();
362 assert_eq!(m.words().len(), 12);
363 assert!(m.check());
364 }
365
366 #[test]
368 fn test_from_random_256() {
369 let m = Mnemonic::from_random(256, Language::English).unwrap();
370 assert_eq!(m.words().len(), 24);
371 assert!(m.check());
372 }
373
374 #[test]
376 fn test_from_string_roundtrip() {
377 let mnemonic_str =
378 "legal winner thank year wave sausage worth useful legal winner thank yellow";
379 let m = Mnemonic::from_string(mnemonic_str, Language::English).unwrap();
380 assert_eq!(m.to_string(), mnemonic_str);
381 }
382
383 #[test]
385 fn test_all_vectors_entropy_to_mnemonic() {
386 let vectors = load_vectors();
387 for (i, v) in vectors.vectors.iter().enumerate() {
388 let entropy = hex_to_bytes(&v.entropy);
389 let m = Mnemonic::from_entropy(&entropy, Language::English).unwrap();
390 assert_eq!(m.to_string(), v.mnemonic, "Vector {i} mnemonic mismatch");
391 }
392 }
393
394 #[test]
396 fn test_all_vectors_seed_derivation() {
397 let vectors = load_vectors();
398 for (i, v) in vectors.vectors.iter().enumerate() {
399 let m = Mnemonic::from_string(&v.mnemonic, Language::English).unwrap();
400 let seed = m.to_seed(&v.passphrase);
401 assert_eq!(bytes_to_hex(&seed), v.seed, "Vector {i} seed mismatch");
402 }
403 }
404}