//! BIP-39 mnemonic support for Synor wallet key generation. //! //! Provides secure mnemonic phrase generation and seed derivation. // The `inner` field warning is a false positive from the ZeroizeOnDrop derive #![allow(unused_assignments)] use bip39::{Language, Mnemonic as Bip39Mnemonic, MnemonicType, Seed}; use thiserror::Error; use zeroize::ZeroizeOnDrop; /// A BIP-39 mnemonic phrase for key derivation. #[derive(Clone, ZeroizeOnDrop)] pub struct Mnemonic { #[zeroize(skip)] inner: Bip39Mnemonic, } impl Mnemonic { /// Generates a new random mnemonic with the specified word count. /// /// Supported word counts: 12, 15, 18, 21, 24 pub fn generate(word_count: usize) -> Result { let mnemonic_type = match word_count { 12 => MnemonicType::Words12, 15 => MnemonicType::Words15, 18 => MnemonicType::Words18, 21 => MnemonicType::Words21, 24 => MnemonicType::Words24, _ => return Err(MnemonicError::InvalidWordCount(word_count)), }; let inner = Bip39Mnemonic::new(mnemonic_type, Language::English); Ok(Mnemonic { inner }) } /// Creates a mnemonic from a phrase string. pub fn from_phrase(phrase: &str) -> Result { let inner = Bip39Mnemonic::from_phrase(phrase, Language::English) .map_err(|e| MnemonicError::InvalidPhrase(format!("{}", e)))?; Ok(Mnemonic { inner }) } /// Returns the mnemonic phrase as a string. pub fn phrase(&self) -> &str { self.inner.phrase() } /// Returns the mnemonic words as a vector. pub fn words(&self) -> Vec<&str> { self.inner.phrase().split_whitespace().collect() } /// Derives a 64-byte seed from the mnemonic with an optional passphrase. pub fn to_seed(&self, passphrase: &str) -> [u8; 64] { let seed = Seed::new(&self.inner, passphrase); let mut result = [0u8; 64]; result.copy_from_slice(seed.as_bytes()); result } /// Returns the entropy bytes. pub fn entropy(&self) -> &[u8] { self.inner.entropy() } /// Creates a mnemonic from entropy bytes. pub fn from_entropy(entropy: &[u8]) -> Result { let inner = Bip39Mnemonic::from_entropy(entropy, Language::English) .map_err(|e| MnemonicError::InvalidEntropy(format!("{}", e)))?; Ok(Mnemonic { inner }) } /// Validates that a phrase is a valid BIP-39 mnemonic. pub fn validate(phrase: &str) -> bool { Bip39Mnemonic::validate(phrase, Language::English).is_ok() } /// Returns the word count. pub fn word_count(&self) -> usize { self.inner.phrase().split_whitespace().count() } } impl std::fmt::Debug for Mnemonic { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Mnemonic") .field("word_count", &self.word_count()) .finish_non_exhaustive() } } impl std::fmt::Display for Mnemonic { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Only show first and last word for security let words: Vec<&str> = self.words(); if words.len() >= 2 { write!( f, "{} ... {} ({} words)", words[0], words[words.len() - 1], words.len() ) } else { write!(f, "[invalid mnemonic]") } } } /// Errors related to mnemonics. #[derive(Debug, Error)] pub enum MnemonicError { #[error("Invalid word count: {0}. Must be 12, 15, 18, 21, or 24")] InvalidWordCount(usize), #[error("Invalid mnemonic phrase: {0}")] InvalidPhrase(String), #[error("Invalid entropy: {0}")] InvalidEntropy(String), #[error("Checksum validation failed")] ChecksumFailed, } /// Suggests completions for a partial word from the BIP-39 wordlist. pub fn suggest_word(partial: &str) -> Vec<&'static str> { let wl = Language::English.wordlist(); wl.get_words_by_prefix(partial) .iter() .take(10) .copied() .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_mnemonic() { let mnemonic = Mnemonic::generate(12).unwrap(); assert_eq!(mnemonic.word_count(), 12); let mnemonic = Mnemonic::generate(24).unwrap(); assert_eq!(mnemonic.word_count(), 24); } #[test] fn test_invalid_word_count() { assert!(Mnemonic::generate(11).is_err()); assert!(Mnemonic::generate(25).is_err()); } #[test] fn test_from_phrase() { let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; let mnemonic = Mnemonic::from_phrase(phrase).unwrap(); assert_eq!(mnemonic.phrase(), phrase); } #[test] fn test_seed_derivation() { let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; let mnemonic = Mnemonic::from_phrase(phrase).unwrap(); let seed1 = mnemonic.to_seed(""); let seed2 = mnemonic.to_seed("password"); // Same mnemonic with different passphrases should produce different seeds assert_ne!(seed1, seed2); // Same mnemonic and passphrase should produce same seed let seed3 = mnemonic.to_seed(""); assert_eq!(seed1, seed3); } #[test] fn test_validate_phrase() { assert!(Mnemonic::validate( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" )); assert!(!Mnemonic::validate("invalid phrase here")); } #[test] fn test_suggest_word() { let suggestions = suggest_word("aban"); assert!(suggestions.contains(&"abandon")); } #[test] fn test_entropy_roundtrip() { let mnemonic = Mnemonic::generate(12).unwrap(); let entropy = mnemonic.entropy().to_vec(); let recovered = Mnemonic::from_entropy(&entropy).unwrap(); assert_eq!(mnemonic.phrase(), recovered.phrase()); } }