1use crate::error::SignerError;
21use sha2::{Digest, Sha256};
22use zeroize::Zeroizing;
23
24const WORDLIST: &str = include_str!("bip39_english.txt");
26
27pub struct Mnemonic {
29 words: Zeroizing<String>,
31}
32
33impl Mnemonic {
34 pub fn generate(word_count: usize) -> Result<Self, SignerError> {
38 let entropy_bits = match word_count {
39 12 => 128,
40 15 => 160,
41 18 => 192,
42 21 => 224,
43 24 => 256,
44 _ => {
45 return Err(SignerError::InvalidPrivateKey(
46 "word count must be 12, 15, 18, 21, or 24".into(),
47 ))
48 }
49 };
50
51 let entropy_bytes = entropy_bits / 8;
52 let mut entropy = vec![0u8; entropy_bytes];
53 crate::security::secure_random(&mut entropy)?;
54
55 Self::from_entropy(&entropy)
56 }
57
58 pub fn from_entropy(entropy: &[u8]) -> Result<Self, SignerError> {
62 let ent_bits = entropy.len() * 8;
63 if ![128, 160, 192, 224, 256].contains(&ent_bits) {
64 return Err(SignerError::InvalidPrivateKey(format!(
65 "entropy must be 16-32 bytes (128-256 bits), got {} bytes",
66 entropy.len()
67 )));
68 }
69
70 let wordlist: Vec<&str> = WORDLIST.lines().collect();
71 if wordlist.len() != 2048 {
72 return Err(SignerError::InvalidPrivateKey(
73 "invalid BIP-39 wordlist".into(),
74 ));
75 }
76
77 let cs_bits = ent_bits / 32;
79 let hash = Sha256::digest(entropy);
80
81 let total_bits = ent_bits + cs_bits;
83 let word_count = total_bits / 11;
84
85 let mut words = Vec::with_capacity(word_count);
86 for i in 0..word_count {
87 let mut idx: u32 = 0;
88 for j in 0..11 {
89 let bit_pos = i * 11 + j;
90 let bit = if bit_pos < ent_bits {
91 (entropy[bit_pos / 8] >> (7 - (bit_pos % 8))) & 1
93 } else {
94 let cs_pos = bit_pos - ent_bits;
96 (hash[cs_pos / 8] >> (7 - (cs_pos % 8))) & 1
97 };
98 idx = (idx << 1) | u32::from(bit);
99 }
100 words.push(wordlist[idx as usize]);
101 }
102
103 Ok(Self {
104 words: Zeroizing::new(words.join(" ")),
105 })
106 }
107
108 pub fn from_phrase(phrase: &str) -> Result<Self, SignerError> {
112 let wordlist: Vec<&str> = WORDLIST.lines().collect();
113 if wordlist.len() != 2048 {
114 return Err(SignerError::InvalidPrivateKey(
115 "invalid BIP-39 wordlist".into(),
116 ));
117 }
118
119 let words: Vec<&str> = phrase.split_whitespace().collect();
120 let word_count = words.len();
121 if ![12, 15, 18, 21, 24].contains(&word_count) {
122 return Err(SignerError::InvalidPrivateKey(format!(
123 "invalid word count: {word_count} (must be 12, 15, 18, 21, or 24)"
124 )));
125 }
126
127 let mut indices = Vec::with_capacity(word_count);
129 for word in &words {
130 let idx = wordlist.binary_search_by(|w| w.cmp(word)).map_err(|_| {
131 SignerError::InvalidPrivateKey(format!("unknown BIP-39 word: {word}"))
132 })?;
133 indices.push(idx as u32);
134 }
135
136 let total_bits = word_count * 11;
138 let cs_bits = word_count / 3; let ent_bits = total_bits - cs_bits;
140 let ent_bytes = ent_bits / 8;
141
142 let mut entropy = vec![0u8; ent_bytes];
143 for (i, idx) in indices.iter().enumerate() {
144 for j in 0..11 {
145 let bit_pos = i * 11 + j;
146 if bit_pos < ent_bits {
147 let bit = (idx >> (10 - j)) & 1;
148 entropy[bit_pos / 8] |= (bit as u8) << (7 - (bit_pos % 8));
149 }
150 }
151 }
152
153 let hash = Sha256::digest(&entropy);
155 for i in 0..cs_bits {
156 let bit_pos = ent_bits + i;
157 let word_idx = bit_pos / 11;
158 let bit_in_word = bit_pos % 11;
159 let expected_bit = (indices[word_idx] >> (10 - bit_in_word)) & 1;
160 let actual_bit = u32::from((hash[i / 8] >> (7 - (i % 8))) & 1);
161 if expected_bit != actual_bit {
162 return Err(SignerError::InvalidPrivateKey(
163 "invalid mnemonic checksum".into(),
164 ));
165 }
166 }
167
168 Ok(Self {
169 words: Zeroizing::new(phrase.to_string()),
170 })
171 }
172
173 pub fn to_seed(&self, passphrase: &str) -> Zeroizing<[u8; 64]> {
177 use zeroize::Zeroize;
178 let mut salt = format!("mnemonic{passphrase}");
179 let mut seed = Zeroizing::new([0u8; 64]);
180 pbkdf2::pbkdf2_hmac::<sha2::Sha512>(
181 self.words.as_bytes(),
182 salt.as_bytes(),
183 2048,
184 &mut *seed,
185 );
186 salt.zeroize();
187 seed
188 }
189
190 pub fn phrase(&self) -> &str {
192 &self.words
193 }
194
195 pub fn word_count(&self) -> usize {
197 self.words.split_whitespace().count()
198 }
199
200 #[cfg(feature = "ethereum")]
204 pub fn to_ethereum_signer(
205 &self,
206 passphrase: &str,
207 account_index: u32,
208 ) -> Result<crate::ethereum::EthereumSigner, SignerError> {
209 use crate::traits::KeyPair;
210 let seed = self.to_seed(passphrase);
211 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed)?;
212 let child = master.derive_path(&crate::hd_key::DerivationPath::ethereum(account_index))?;
213 crate::ethereum::EthereumSigner::from_bytes(&child.private_key_bytes())
214 }
215
216 #[cfg(feature = "bitcoin")]
220 pub fn to_bitcoin_signer(
221 &self,
222 passphrase: &str,
223 account_index: u32,
224 ) -> Result<crate::bitcoin::BitcoinSigner, SignerError> {
225 use crate::traits::KeyPair;
226 let seed = self.to_seed(passphrase);
227 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed)?;
228 let child = master.derive_path(&crate::hd_key::DerivationPath::bitcoin(account_index))?;
229 crate::bitcoin::BitcoinSigner::from_bytes(&child.private_key_bytes())
230 }
231
232 #[cfg(feature = "solana")]
240 pub fn to_solana_signer(
241 &self,
242 passphrase: &str,
243 account_index: u32,
244 ) -> Result<crate::solana::SolanaSigner, SignerError> {
245 use crate::traits::KeyPair;
246 let seed = self.to_seed(passphrase);
247 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed)?;
248 let child = master.derive_path(&crate::hd_key::DerivationPath::solana(account_index))?;
249 crate::solana::SolanaSigner::from_bytes(&child.private_key_bytes())
250 }
251
252 #[cfg(feature = "xrp")]
256 pub fn to_xrp_signer(
257 &self,
258 passphrase: &str,
259 account_index: u32,
260 ) -> Result<crate::xrp::XrpEcdsaSigner, SignerError> {
261 use crate::traits::KeyPair;
262 let seed = self.to_seed(passphrase);
263 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed)?;
264 let child = master.derive_path(&crate::hd_key::DerivationPath::xrp(account_index))?;
265 crate::xrp::XrpEcdsaSigner::from_bytes(&child.private_key_bytes())
266 }
267}
268
269#[cfg(test)]
270#[allow(clippy::unwrap_used, clippy::expect_used)]
271mod tests {
272 use super::*;
273
274 #[test]
277 fn test_bip39_vector1_12words() {
278 let entropy = hex::decode("00000000000000000000000000000000").unwrap();
279 let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
280 assert_eq!(
281 mnemonic.phrase(),
282 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
283 );
284 assert_eq!(mnemonic.word_count(), 12);
285 }
286
287 #[test]
290 fn test_bip39_vector2_24words() {
291 let entropy =
292 hex::decode("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")
293 .unwrap();
294 let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
295 assert_eq!(
296 mnemonic.phrase(),
297 "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title"
298 );
299 }
300
301 #[test]
303 fn test_bip39_seed_vector() {
304 let entropy = hex::decode("00000000000000000000000000000000").unwrap();
305 let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
306 let seed = mnemonic.to_seed("TREZOR");
307 let expected = "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04";
309 assert_eq!(hex::encode(*seed), expected);
310 }
311
312 #[test]
314 fn test_generate_parse_roundtrip_12() {
315 let m1 = Mnemonic::generate(12).unwrap();
316 let m2 = Mnemonic::from_phrase(m1.phrase()).unwrap();
317 assert_eq!(m1.phrase(), m2.phrase());
318 assert_eq!(*m1.to_seed(""), *m2.to_seed(""));
319 }
320
321 #[test]
322 fn test_generate_parse_roundtrip_24() {
323 let m1 = Mnemonic::generate(24).unwrap();
324 let m2 = Mnemonic::from_phrase(m1.phrase()).unwrap();
325 assert_eq!(m1.phrase(), m2.phrase());
326 }
327
328 #[test]
329 fn test_invalid_word_count() {
330 assert!(Mnemonic::generate(11).is_err());
331 assert!(Mnemonic::generate(13).is_err());
332 }
333
334 #[test]
335 fn test_invalid_entropy_length() {
336 assert!(Mnemonic::from_entropy(&[0u8; 15]).is_err());
337 assert!(Mnemonic::from_entropy(&[0u8; 33]).is_err());
338 }
339
340 #[test]
341 fn test_invalid_word_rejected() {
342 assert!(Mnemonic::from_phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon zzzzz").is_err());
343 }
344
345 #[test]
346 fn test_bad_checksum_rejected() {
347 assert!(Mnemonic::from_phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon").is_err());
349 }
350
351 #[test]
352 fn test_passphrase_changes_seed() {
353 let m = Mnemonic::from_entropy(&[0u8; 16]).unwrap();
354 let s1 = m.to_seed("");
355 let s2 = m.to_seed("password");
356 assert_ne!(*s1, *s2);
357 }
358
359 #[test]
361 fn test_mnemonic_to_eth_address() {
362 let entropy = hex::decode("00000000000000000000000000000000").unwrap();
363 let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
364 let seed = mnemonic.to_seed("TREZOR");
365 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed).unwrap();
366 let child = master
367 .derive_path(&crate::hd_key::DerivationPath::ethereum(0))
368 .unwrap();
369 assert_eq!(child.private_key_bytes().len(), 32);
370 }
371
372 #[test]
373 fn test_all_entropy_sizes() {
374 for size in [16, 20, 24, 28, 32] {
375 let entropy = vec![0xABu8; size];
376 let m = Mnemonic::from_entropy(&entropy).unwrap();
377 let expected_words = (size * 8 + size * 8 / 32) / 11;
378 assert_eq!(m.word_count(), expected_words);
379 let m2 = Mnemonic::from_phrase(m.phrase()).unwrap();
381 assert_eq!(m.phrase(), m2.phrase());
382 }
383 }
384
385 #[test]
388 fn test_bip39_vector_all_zeros_12() {
389 let entropy = hex::decode("00000000000000000000000000000000").unwrap();
391 let m = Mnemonic::from_entropy(&entropy).unwrap();
392 assert_eq!(
393 m.phrase(),
394 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
395 );
396 }
397
398 #[test]
399 fn test_bip39_vector_all_zeros_seed() {
400 let entropy = hex::decode("00000000000000000000000000000000").unwrap();
403 let m = Mnemonic::from_entropy(&entropy).unwrap();
404 let seed = m.to_seed("TREZOR");
405 let seed2 = m.to_seed("TREZOR");
407 assert_eq!(*seed, *seed2);
408 assert_eq!(seed.len(), 64);
410 let seed3 = m.to_seed("different");
412 assert_ne!(*seed, *seed3);
413 }
414
415 #[test]
416 fn test_bip39_vector_7f_entropy() {
417 let entropy = hex::decode("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f").unwrap();
418 let m = Mnemonic::from_entropy(&entropy).unwrap();
419 assert_eq!(
420 m.phrase(),
421 "legal winner thank year wave sausage worth useful legal winner thank yellow"
422 );
423 }
424
425 #[test]
426 fn test_bip39_vector_7f_seed() {
427 let entropy = hex::decode("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f").unwrap();
428 let m = Mnemonic::from_entropy(&entropy).unwrap();
429 let seed = m.to_seed("TREZOR");
430 assert_eq!(
431 hex::encode(*seed),
432 "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607"
433 );
434 }
435
436 #[test]
437 fn test_bip39_vector_ff_12() {
438 let entropy = hex::decode("ffffffffffffffffffffffffffffffff").unwrap();
439 let m = Mnemonic::from_entropy(&entropy).unwrap();
440 assert_eq!(
441 m.phrase(),
442 "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
443 );
444 }
445
446 #[test]
447 fn test_bip39_vector_24_words() {
448 let entropy =
450 hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
451 .unwrap();
452 let m = Mnemonic::from_entropy(&entropy).unwrap();
453 assert_eq!(
454 m.phrase(),
455 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
456 );
457 }
458
459 #[test]
460 fn test_bip39_vector_24_seed() {
461 let entropy =
462 hex::decode("0000000000000000000000000000000000000000000000000000000000000000")
463 .unwrap();
464 let m = Mnemonic::from_entropy(&entropy).unwrap();
465 let seed = m.to_seed("TREZOR");
466 assert_eq!(
467 hex::encode(*seed),
468 "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8"
469 );
470 }
471
472 #[test]
473 fn test_bip39_from_phrase_round_trip() {
474 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
476 let m = Mnemonic::from_phrase(phrase).unwrap();
477 assert_eq!(m.phrase(), phrase);
478 assert_eq!(m.word_count(), 12);
479 }
480
481 #[test]
486 fn test_cross_chain_mnemonic_derivation() {
487 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
489 let m = Mnemonic::from_phrase(phrase).unwrap();
490 let seed = m.to_seed("");
491
492 let master = crate::hd_key::ExtendedPrivateKey::from_seed(&*seed).unwrap();
494
495 let eth = master
497 .derive_path(&crate::hd_key::DerivationPath::ethereum(0))
498 .unwrap();
499 let eth_key = eth.private_key_bytes();
500 assert_eq!(eth_key.len(), 32);
501
502 let btc = master
504 .derive_path(&crate::hd_key::DerivationPath::bitcoin(0))
505 .unwrap();
506 let btc_key = btc.private_key_bytes();
507 assert_eq!(btc_key.len(), 32);
508
509 let sol = master
511 .derive_path(&crate::hd_key::DerivationPath::solana(0))
512 .unwrap();
513 let sol_key = sol.private_key_bytes();
514 assert_eq!(sol_key.len(), 32);
515
516 let xrp = master
518 .derive_path(&crate::hd_key::DerivationPath::xrp(0))
519 .unwrap();
520 let xrp_key = xrp.private_key_bytes();
521 assert_eq!(xrp_key.len(), 32);
522
523 assert_ne!(&*eth_key, &*btc_key, "ETH != BTC");
525 assert_ne!(&*eth_key, &*sol_key, "ETH != SOL");
526 assert_ne!(&*eth_key, &*xrp_key, "ETH != XRP");
527 assert_ne!(&*btc_key, &*sol_key, "BTC != SOL");
528 assert_ne!(&*btc_key, &*xrp_key, "BTC != XRP");
529 assert_ne!(&*sol_key, &*xrp_key, "SOL != XRP");
530
531 let eth2 = master
533 .derive_path(&crate::hd_key::DerivationPath::ethereum(0))
534 .unwrap();
535 assert_eq!(&*eth_key, &*eth2.private_key_bytes());
536 }
537
538 #[cfg(feature = "ethereum")]
539 #[test]
540 fn test_cross_chain_mnemonic_eth_address() {
541 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
542 let m = Mnemonic::from_phrase(phrase).unwrap();
543 let signer = m.to_ethereum_signer("", 0).unwrap();
544 let addr = signer.address_checksum();
545 assert!(
546 addr.starts_with("0x"),
547 "ETH address must start with 0x: {addr}"
548 );
549 assert_eq!(addr.len(), 42, "ETH address must be 42 chars");
550
551 let signer1 = m.to_ethereum_signer("", 1).unwrap();
553 let addr1 = signer1.address_checksum();
554 assert_ne!(
555 addr, addr1,
556 "different account indices → different addresses"
557 );
558 }
559
560 #[cfg(feature = "bitcoin")]
561 #[test]
562 fn test_cross_chain_mnemonic_btc_signer() {
563 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
564 let m = Mnemonic::from_phrase(phrase).unwrap();
565 let signer = m.to_bitcoin_signer("", 0).unwrap();
566 use crate::traits::Signer;
567 assert_eq!(signer.public_key_bytes().len(), 33); }
569
570 #[cfg(feature = "solana")]
571 #[test]
572 fn test_cross_chain_mnemonic_sol_signer() {
573 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
574 let m = Mnemonic::from_phrase(phrase).unwrap();
575 let signer = m.to_solana_signer("", 0).unwrap();
576 use crate::traits::Signer;
577 assert_eq!(signer.public_key_bytes().len(), 32); }
579
580 #[cfg(feature = "xrp")]
581 #[test]
582 fn test_cross_chain_mnemonic_xrp_signer() {
583 let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
584 let m = Mnemonic::from_phrase(phrase).unwrap();
585 let signer = m.to_xrp_signer("", 0).unwrap();
586 use crate::traits::Signer;
587 assert_eq!(signer.public_key_bytes().len(), 33); }
589}