use alloc::string::String;
use alloc::vec::Vec;
use crate::types::{SdkError, Result};
use crate::wordlist;
#[derive(Clone)]
pub struct Mnemonic {
phrase: String,
word_count: usize,
}
impl Mnemonic {
pub fn from_entropy_128(entropy: &[u8; 16]) -> Result<Self> {
Self::from_entropy(entropy)
}
pub fn from_entropy_256(entropy: &[u8; 32]) -> Result<Self> {
Self::from_entropy(entropy)
}
fn from_entropy(entropy: &[u8]) -> Result<Self> {
let ent_bits = entropy.len() * 8;
if ent_bits != 128 && ent_bits != 256 {
return Err(SdkError::Invalid);
}
let cs_bits = ent_bits / 32;
let total_bits = ent_bits + cs_bits;
let word_count = total_bits / 11;
let hash = hmac_sha256::Hash::hash(entropy);
let checksum_byte = hash[0];
let mut words = Vec::with_capacity(word_count);
for i in 0..word_count {
let bit_offset = i * 11;
let mut index: u16 = 0;
for bit in 0..11 {
let pos = bit_offset + bit;
let byte_val = if pos < ent_bits {
entropy[pos / 8]
} else {
checksum_byte
};
let bit_pos = 7 - (pos % 8);
if pos >= ent_bits {
let cs_pos = pos - ent_bits;
let cs_bit_pos = 7 - cs_pos;
if (checksum_byte >> cs_bit_pos) & 1 == 1 {
index |= 1 << (10 - bit);
}
} else if (byte_val >> bit_pos) & 1 == 1 {
index |= 1 << (10 - bit);
}
}
if (index as usize) >= 2048 {
return Err(SdkError::Invalid);
}
words.push(wordlist::get_word(index as usize).ok_or(SdkError::Invalid)?);
}
let phrase = words.join(" ");
Ok(Self { phrase, word_count })
}
pub fn from_phrase(phrase: &str) -> Result<Self> {
let words: Vec<&str> = phrase.split_whitespace().collect();
let word_count = words.len();
if word_count != 12 && word_count != 24 {
return Err(SdkError::Invalid);
}
let mut indices = Vec::with_capacity(word_count);
for word in &words {
let idx = wordlist::find_word(word)
.ok_or(SdkError::Invalid)?;
indices.push(idx as u16);
}
let total_bits = word_count * 11;
let cs_bits = word_count / 3; let ent_bits = total_bits - cs_bits;
let ent_bytes = ent_bits / 8;
let mut entropy = alloc::vec![0u8; ent_bytes];
let mut checksum_bits: u8 = 0;
for (i, &idx) in indices.iter().enumerate() {
for bit in 0..11 {
let pos = i * 11 + bit;
let bit_val = (idx >> (10 - bit)) & 1;
if pos < ent_bits {
if bit_val == 1 {
entropy[pos / 8] |= 1 << (7 - (pos % 8));
}
} else {
let cs_pos = pos - ent_bits;
if bit_val == 1 {
checksum_bits |= 1 << (7 - cs_pos);
}
}
}
}
let hash = hmac_sha256::Hash::hash(&entropy);
let expected_mask = 0xFFu8 << (8 - cs_bits);
if (hash[0] & expected_mask) != (checksum_bits & expected_mask) {
return Err(SdkError::Invalid);
}
let phrase = words.join(" ");
Ok(Self { phrase, word_count })
}
pub fn phrase(&self) -> &str {
&self.phrase
}
pub fn word_count(&self) -> usize {
self.word_count
}
pub fn derive_seed(&self, passphrase: &str) -> [u8; 64] {
let mut salt = Vec::with_capacity(8 + passphrase.len());
salt.extend_from_slice(b"mnemonic");
salt.extend_from_slice(passphrase.as_bytes());
pbkdf2_hmac_sha512(self.phrase.as_bytes(), &salt, 2048)
}
}
fn pbkdf2_hmac_sha512(password: &[u8], salt: &[u8], iterations: u32) -> [u8; 64] {
let mut salt_with_index = Vec::with_capacity(salt.len() + 4);
salt_with_index.extend_from_slice(salt);
salt_with_index.extend_from_slice(&1u32.to_be_bytes());
let mut u_prev = hmac_sha512::HMAC::mac(salt_with_index, password);
let mut result = u_prev;
for _ in 1..iterations {
let u_next = hmac_sha512::HMAC::mac(u_prev, password);
for (r, u) in result.iter_mut().zip(u_next.iter()) {
*r ^= u;
}
u_prev = u_next;
}
result
}
impl core::fmt::Debug for Mnemonic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Mnemonic({} words)", self.word_count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_entropy_128_produces_12_words() {
let entropy = [0u8; 16];
let m = Mnemonic::from_entropy_128(&entropy).unwrap();
assert_eq!(m.word_count(), 12);
assert_eq!(m.phrase(), "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about");
}
#[test]
fn from_entropy_256_produces_24_words() {
let entropy = [0u8; 32];
let m = Mnemonic::from_entropy_256(&entropy).unwrap();
assert_eq!(m.word_count(), 24);
assert_eq!(m.phrase(), "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art");
}
#[test]
fn from_phrase_valid_12_words() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let m = Mnemonic::from_phrase(phrase).unwrap();
assert_eq!(m.word_count(), 12);
assert_eq!(m.phrase(), phrase);
}
#[test]
fn from_phrase_invalid_checksum() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon";
assert!(Mnemonic::from_phrase(phrase).is_err());
}
#[test]
fn from_phrase_invalid_word() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon zzzzz";
assert!(Mnemonic::from_phrase(phrase).is_err());
}
#[test]
fn from_phrase_wrong_count() {
let phrase = "abandon abandon abandon";
assert!(Mnemonic::from_phrase(phrase).is_err());
}
#[test]
fn roundtrip_entropy_to_phrase() {
let entropy: [u8; 16] = [0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f];
let m = Mnemonic::from_entropy_128(&entropy).unwrap();
let m2 = Mnemonic::from_phrase(m.phrase()).unwrap();
assert_eq!(m.phrase(), m2.phrase());
}
#[test]
fn derive_seed_known_vector() {
let m = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
).unwrap();
let seed = m.derive_seed("");
assert_eq!(seed[0], 0x5e);
assert_eq!(seed[1], 0xb0);
assert_eq!(seed[2], 0x0b);
assert_eq!(seed[3], 0xbd);
}
#[test]
fn derive_seed_with_passphrase() {
let m = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
).unwrap();
let seed_no_pass = m.derive_seed("");
let seed_with_pass = m.derive_seed("mypassword");
assert_ne!(seed_no_pass, seed_with_pass);
}
#[test]
fn bip39_test_vector_128bit() {
let m = Mnemonic::from_phrase(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
).unwrap();
let seed = m.derive_seed("");
let expected: [u8; 64] = [
0x5e, 0xb0, 0x0b, 0xbd, 0xdc, 0xf0, 0x69, 0x08,
0x48, 0x89, 0xa8, 0xab, 0x91, 0x55, 0x56, 0x81,
0x65, 0xf5, 0xc4, 0x53, 0xcc, 0xb8, 0x5e, 0x70,
0x81, 0x1a, 0xae, 0xd6, 0xf6, 0xda, 0x5f, 0xc1,
0x9a, 0x5a, 0xc4, 0x0b, 0x38, 0x9c, 0xd3, 0x70,
0xd0, 0x86, 0x20, 0x6d, 0xec, 0x8a, 0xa6, 0xc4,
0x3d, 0xae, 0xa6, 0x69, 0x0f, 0x20, 0xad, 0x3d,
0x8d, 0x48, 0xb2, 0xd2, 0xce, 0x9e, 0x38, 0xe4,
];
assert_eq!(seed, expected);
}
}