use super::{Bip39Error, Language};
use bitcoin::bip32::Xpriv;
use sha2::{Digest, Sha256};
use xbits::{FromBits, XBits};
type Result<T> = std::result::Result<T, Bip39Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mnemonic {
words: Vec<String>,
language: Language,
}
#[allow(unused)]
impl Mnemonic {
pub const VALID_SIZES: &[usize] = &[12, 15, 18, 21, 24];
pub fn new(entropy: &[u8], language: Language) -> Result<Self> {
if !matches!(entropy.len(), 16 | 20 | 24 | 28 | 32) {
return Err(Bip39Error::InvalidSize);
}
let size = entropy.len() / 4 * 3;
let check_mask = 0xff << (8 - size / 3);
let checksum = Sha256::digest(entropy)[0] & check_mask;
let indices: Vec<usize> = [entropy.to_vec(), vec![checksum]]
.concat()
.bits()
.chunks(11)
.take(size)
.collect();
let words = indices
.iter()
.map(|&i| language.word_at(i).unwrap_or_default().to_string())
.collect();
Ok(Mnemonic { words, language })
}
pub fn to_master(&self, salt: &str) -> Result<Xpriv> {
let mnemonic = self.to_string();
let salt = format!("mnemonic{salt}");
let mut seed: [u8; 64] = [0; 64];
pbkdf2::pbkdf2_hmac::<sha2::Sha512>(
mnemonic.as_bytes(),
salt.as_bytes(),
u32::pow(2, 11),
&mut seed,
);
Ok(Xpriv::new_master(crate::NETWORK, &seed)?)
}
#[inline]
pub fn language(&self) -> Language {
self.language
}
#[inline]
pub fn size(&self) -> usize {
self.words.len()
}
#[inline]
pub fn words(&self) -> impl Iterator<Item = &String> {
self.words.iter()
}
#[inline]
pub fn indices(&self) -> impl Iterator<Item = usize> {
self.words
.iter()
.map(|w| self.language.index_of(w).unwrap())
}
#[inline]
pub fn entropy(&self) -> Vec<u8> {
let mut entropy: Vec<u8> = Vec::from_bits_chunk(self.indices(), 11);
entropy.pop(); entropy
}
fn detect_language<T>(words: impl Iterator<Item = T>) -> Vec<Language>
where
T: AsRef<str>,
{
words
.map(|w| Language::detect(w.as_ref()))
.reduce(|mut acc, v| {
acc.retain(|x| v.contains(x));
acc
})
.unwrap_or_default()
}
fn verify_checksum(indices: &[usize]) -> Result<()> {
if !Self::VALID_SIZES.contains(&indices.len()) {
return Err(Bip39Error::InvalidSize);
}
let mut entropy = Vec::from_bits_chunk(indices.iter().copied(), 11);
let tail = entropy.pop().unwrap();
let check_mask = 0xff << (8 - indices.len() / 3);
let checksum = Sha256::digest(&entropy)[0] & check_mask;
if checksum != tail {
return Err(Bip39Error::InvalidChecksum);
}
Ok(())
}
}
impl std::str::FromStr for Mnemonic {
type Err = Bip39Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let words: Vec<&str> = s.split_whitespace().collect();
if !Self::VALID_SIZES.contains(&words.len()) {
return Err(Bip39Error::InvalidSize);
}
let mut languages = Mnemonic::detect_language(words.iter());
languages.retain(|&language| {
if let Ok(indices) = language.indices(words.iter()) {
Mnemonic::verify_checksum(&indices).is_ok()
} else {
false
}
});
match languages.len() {
0 => Err(Bip39Error::InvalidChecksum),
1 => Ok(Mnemonic {
words: words.into_iter().map(String::from).collect(),
language: languages.pop().unwrap(),
}),
2.. => {
use Language::*;
if languages == [ChineseSimplified, ChineseTraditional]
|| languages == [ChineseTraditional, ChineseSimplified]
{
Ok(Mnemonic {
words: words.into_iter().map(String::from).collect(),
language: ChineseSimplified,
})
} else {
Err(Bip39Error::AmbiguousLanguages(languages))
}
}
}
}
}
impl std::fmt::Display for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.words.join(" "))
}
}
trait Indices {
fn indices<T>(&self, words: impl Iterator<Item = T>) -> Result<Vec<usize>>
where
T: AsRef<str>;
}
impl Indices for Language {
fn indices<T>(&self, words: impl Iterator<Item = T>) -> Result<Vec<usize>>
where
T: AsRef<str>,
{
words
.map(|w| self.index_of(w.as_ref()))
.collect::<Option<Vec<_>>>()
.ok_or(Bip39Error::InvalidLanguage)
}
}
#[cfg(test)]
mod mnemonic_tests {
use super::*;
#[test]
fn test_mnemonic_master() -> Result<()> {
#[cfg(not(feature = "testnet"))]
const TEST_DATA: &[[&str; 3]] = &[[
"theme rain hollow final expire proud detect wife hotel taxi witness strategy park head forest",
"ππππ",
"xprv9s21ZrQH143K2k5PPw697AeKWWdeQueM2JCKu8bsmF7M7dDmPGHecHJJNGeujWTJ97Fy9PfobsgZfxhcpWaYyAauFMxcy4fo3x7JNnbYQyD",
]];
#[cfg(feature = "testnet")]
const TEST_DATA: &[[&str; 3]] = &[[
"theme rain hollow final expire proud detect wife hotel taxi witness strategy park head forest",
"ππππ",
"tprv8ZgxMBicQKsPdZJv4VweGpGJpe3reRgMMr7SmZ2LFDbpuDxrNddQ82fkHSpZjsqcWYnk9VHZmEGN8pFMwivVnDrVn1AvdRPqy3ripW55kfq",
]];
for data in TEST_DATA {
let mnemonic: Mnemonic = data[0].parse()?;
assert_eq!(mnemonic.to_master(data[1])?.to_string(), data[2]);
}
Ok(())
}
#[cfg(not(feature = "testnet"))]
#[test]
fn test_nfc_salt() -> Result<()> {
use unicode_normalization::UnicodeNormalization;
let mnemonic = "caution want scheme basic teach bulb shadow pioneer blue add expand guess";
let salt1 = "muΜsica"; let salt2 = "mΓΊsica"; assert_eq!(salt1.nfc().collect::<String>(), salt2);
assert_ne!(salt1.chars().count(), salt2.chars().count());
let master1 = mnemonic.parse::<Mnemonic>()?.to_master(salt1)?;
let master2 = mnemonic.parse::<Mnemonic>()?.to_master(salt2)?;
assert_ne!(master1, master2);
use crate::BIP49;
let wallet1 = master1.bip49_wallet(0, 0, false).unwrap();
let wallet2 = master2.bip49_wallet(0, 0, false).unwrap();
assert_ne!(wallet1, wallet2);
println!("wallet1: {wallet1:?}");
println!("wallet2: {wallet2:?}");
let electrum_address = "3NgaBMn1fQ9wrAVAhhnVKaTVP5gFo2Wedn";
assert_eq!(electrum_address, wallet1.0);
Ok(())
}
}