use crate::error::{AptosError, AptosResult};
#[derive(Clone)]
pub struct Mnemonic {
phrase: String,
}
impl Mnemonic {
pub fn generate(word_count: usize) -> AptosResult<Self> {
let entropy_bytes = match word_count {
12 => 16, 15 => 20, 18 => 24, 21 => 28, 24 => 32, _ => {
return Err(AptosError::InvalidMnemonic(format!(
"invalid word count: {word_count}, must be 12, 15, 18, 21, or 24"
)));
}
};
let mut entropy = vec![0u8; entropy_bytes];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut entropy);
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
.map_err(|e| AptosError::InvalidMnemonic(e.to_string()));
zeroize::Zeroize::zeroize(&mut entropy);
let mnemonic = mnemonic?;
Ok(Self {
phrase: mnemonic.to_string(),
})
}
pub fn from_phrase(phrase: &str) -> AptosResult<Self> {
let _mnemonic = bip39::Mnemonic::parse_normalized(phrase)
.map_err(|e| AptosError::InvalidMnemonic(e.to_string()))?;
Ok(Self {
phrase: phrase.to_string(),
})
}
pub fn phrase(&self) -> &str {
&self.phrase
}
pub fn to_seed(&self) -> AptosResult<[u8; 64]> {
self.to_seed_with_passphrase("")
}
pub fn to_seed_with_passphrase(&self, passphrase: &str) -> AptosResult<[u8; 64]> {
let mnemonic = bip39::Mnemonic::parse_normalized(&self.phrase).map_err(|e| {
AptosError::InvalidMnemonic(format!("internal error: mnemonic re-parse failed: {e}"))
})?;
Ok(mnemonic.to_seed(passphrase))
}
#[cfg(feature = "ed25519")]
pub fn derive_ed25519_key(&self, index: u32) -> AptosResult<crate::crypto::Ed25519PrivateKey> {
let mut seed = self.to_seed()?;
let result = derive_ed25519_from_seed(&seed, index);
zeroize::Zeroize::zeroize(&mut seed);
let mut key = result?;
let private_key = crate::crypto::Ed25519PrivateKey::from_bytes(&key);
zeroize::Zeroize::zeroize(&mut key);
private_key
}
}
#[cfg(feature = "ed25519")]
fn derive_ed25519_from_seed(seed: &[u8], index: u32) -> AptosResult<[u8; 32]> {
use hmac::{Hmac, Mac};
use sha2::Sha512;
type HmacSha512 = Hmac<Sha512>;
let mut mac = HmacSha512::new_from_slice(b"ed25519 seed")
.map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
mac.update(seed);
let result = mac.finalize().into_bytes();
let mut key = [0u8; 32];
let mut chain_code = [0u8; 32];
key.copy_from_slice(&result[..32]);
chain_code.copy_from_slice(&result[32..]);
let path = [
44 | 0x8000_0000, 637 | 0x8000_0000, 0x8000_0000, 0x8000_0000, index | 0x8000_0000, ];
for child_index in path {
let mut data = vec![0u8];
data.extend_from_slice(&key);
data.extend_from_slice(&child_index.to_be_bytes());
let mut mac = HmacSha512::new_from_slice(&chain_code)
.map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
mac.update(&data);
let result = mac.finalize().into_bytes();
key.copy_from_slice(&result[..32]);
chain_code.copy_from_slice(&result[32..]);
zeroize::Zeroize::zeroize(&mut data);
}
zeroize::Zeroize::zeroize(&mut chain_code);
Ok(key)
}
impl std::fmt::Debug for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Mnemonic([REDACTED])")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_mnemonic() {
let mnemonic = Mnemonic::generate(12).unwrap();
assert_eq!(mnemonic.phrase().split_whitespace().count(), 12);
let mnemonic = Mnemonic::generate(24).unwrap();
assert_eq!(mnemonic.phrase().split_whitespace().count(), 24);
}
#[test]
fn test_invalid_word_count() {
assert!(Mnemonic::generate(13).is_err());
}
#[test]
fn test_parse_mnemonic() {
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_invalid_mnemonic() {
assert!(Mnemonic::from_phrase("invalid mnemonic phrase").is_err());
}
#[test]
#[cfg(feature = "ed25519")]
fn test_derive_ed25519_key() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mnemonic = Mnemonic::from_phrase(phrase).unwrap();
let key1 = mnemonic.derive_ed25519_key(0).unwrap();
let key2 = mnemonic.derive_ed25519_key(0).unwrap();
assert_eq!(key1.to_bytes(), key2.to_bytes());
let key3 = mnemonic.derive_ed25519_key(1).unwrap();
assert_ne!(key1.to_bytes(), key3.to_bytes());
}
}