use bip39::{Language, Mnemonic};
use zeroize::Zeroizing;
use crate::error::{ProtocolError, Result};
pub fn seed_to_phrase(seed: &[u8; 32]) -> String {
Mnemonic::from_entropy(seed)
.expect("a 32-byte seed is a valid 256-bit BIP39 entropy length")
.to_string()
}
pub fn phrase_to_seed(phrase: &str) -> Result<Zeroizing<[u8; 32]>> {
let normalized = phrase.trim().to_lowercase();
let mnemonic = Mnemonic::parse_in(Language::English, normalized)
.map_err(|e| ProtocolError::Identity(format!("invalid seed phrase: {e}")))?;
let (raw, len) = mnemonic.to_entropy_array();
let raw = Zeroizing::new(raw);
if len != 32 {
return Err(ProtocolError::Identity(format!(
"seed phrase decodes to {len} bytes; expected a 24-word (32-byte) phrase"
)));
}
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(&raw[..32]);
Ok(seed)
}
#[cfg(test)]
mod tests {
use super::*;
fn zero_seed_phrase() -> String {
let mut words = vec!["abandon"; 23];
words.push("art");
words.join(" ")
}
#[test]
fn zero_seed_matches_bip39_vector() {
let phrase = seed_to_phrase(&[0u8; 32]);
assert_eq!(phrase, zero_seed_phrase());
assert_eq!(phrase.split_whitespace().count(), 24);
assert_eq!(*phrase_to_seed(&zero_seed_phrase()).unwrap(), [0u8; 32]);
}
#[test]
fn round_trips_random_seeds() {
for _ in 0..32 {
let seed: [u8; 32] = rand::random();
let phrase = seed_to_phrase(&seed);
assert_eq!(phrase.split_whitespace().count(), 24);
assert_eq!(*phrase_to_seed(&phrase).unwrap(), seed);
}
}
#[test]
fn is_case_insensitive_and_trims_whitespace() {
let seed = [7u8; 32];
let phrase = seed_to_phrase(&seed);
let messy = format!(" \t {} \n ", phrase.to_uppercase().replace(' ', " "));
assert_eq!(*phrase_to_seed(&messy).unwrap(), seed);
}
#[test]
fn rejects_bad_checksum() {
let bad_checksum = vec!["abandon"; 24].join(" ");
assert!(phrase_to_seed(&bad_checksum).is_err());
}
#[test]
fn rejects_off_wordlist_word() {
let mut words = vec!["abandon"; 23];
words.push("notabip39word");
assert!(phrase_to_seed(&words.join(" ")).is_err());
}
#[test]
fn decode_returns_zeroizing_seed() {
let seed = [9u8; 32];
let phrase = seed_to_phrase(&seed);
let decoded: Zeroizing<[u8; 32]> = phrase_to_seed(&phrase).unwrap();
assert_eq!(*decoded, seed);
}
#[test]
fn rejects_wrong_word_count() {
let short = vec!["abandon"; 23].join(" ");
assert!(phrase_to_seed(&short).is_err());
}
}