Skip to main content

aptos_sdk/account/
mnemonic.rs

1//! BIP-39 mnemonic phrase support for key derivation.
2//!
3//! This module requires the `mnemonic` feature flag.
4
5use crate::error::{AptosError, AptosResult};
6
7/// A BIP-39 mnemonic phrase for key derivation.
8///
9/// # Example
10///
11/// ```rust
12/// use aptos_sdk::account::Mnemonic;
13///
14/// // Generate a new mnemonic
15/// let mnemonic = Mnemonic::generate(24).unwrap();
16/// println!("Mnemonic: {}", mnemonic.phrase());
17///
18/// // Parse an existing mnemonic
19/// let mnemonic = Mnemonic::from_phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
20/// ```
21#[derive(Clone)]
22pub struct Mnemonic {
23    phrase: String,
24}
25
26impl Mnemonic {
27    /// Generates a new random mnemonic phrase.
28    ///
29    /// # Arguments
30    ///
31    /// * `word_count` - Number of words (12, 15, 18, 21, or 24)
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the word count is not one of 12, 15, 18, 21, or 24,
36    /// or if entropy generation fails.
37    pub fn generate(word_count: usize) -> AptosResult<Self> {
38        let entropy_bytes = match word_count {
39            12 => 16, // 128 bits
40            15 => 20, // 160 bits
41            18 => 24, // 192 bits
42            21 => 28, // 224 bits
43            24 => 32, // 256 bits
44            _ => {
45                return Err(AptosError::InvalidMnemonic(format!(
46                    "invalid word count: {word_count}, must be 12, 15, 18, 21, or 24"
47                )));
48            }
49        };
50
51        let mut entropy = vec![0u8; entropy_bytes];
52        rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut entropy);
53
54        let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
55            .map_err(|e| AptosError::InvalidMnemonic(e.to_string()));
56
57        // SECURITY: Zeroize entropy before it goes out of scope to prevent
58        // key material from lingering in memory
59        zeroize::Zeroize::zeroize(&mut entropy);
60
61        let mnemonic = mnemonic?;
62
63        Ok(Self {
64            phrase: mnemonic.to_string(),
65        })
66    }
67
68    /// Creates a mnemonic from an existing phrase.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the phrase is not a valid BIP-39 mnemonic.
73    pub fn from_phrase(phrase: &str) -> AptosResult<Self> {
74        // Validate the mnemonic
75        let _mnemonic = bip39::Mnemonic::parse_normalized(phrase)
76            .map_err(|e| AptosError::InvalidMnemonic(e.to_string()))?;
77
78        Ok(Self {
79            phrase: phrase.to_string(),
80        })
81    }
82
83    /// Returns the mnemonic phrase.
84    pub fn phrase(&self) -> &str {
85        &self.phrase
86    }
87
88    /// Derives the seed from this mnemonic.
89    ///
90    /// Uses an empty passphrase by default.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the mnemonic cannot be re-parsed (should not happen
95    /// since the phrase was validated during construction).
96    pub fn to_seed(&self) -> AptosResult<[u8; 64]> {
97        self.to_seed_with_passphrase("")
98    }
99
100    /// Derives the seed from this mnemonic with a passphrase.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the mnemonic phrase cannot be re-parsed. This should
105    /// never happen because the phrase is validated during construction, but
106    /// returning an error is safer than panicking.
107    pub fn to_seed_with_passphrase(&self, passphrase: &str) -> AptosResult<[u8; 64]> {
108        let mnemonic = bip39::Mnemonic::parse_normalized(&self.phrase).map_err(|e| {
109            AptosError::InvalidMnemonic(format!("internal error: mnemonic re-parse failed: {e}"))
110        })?;
111
112        Ok(mnemonic.to_seed(passphrase))
113    }
114
115    /// Derives an Ed25519 private key using the Aptos derivation path.
116    ///
117    /// Path: `m/44'/637'/0'/0'/index'`
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if key derivation fails or the derived key is invalid.
122    #[cfg(feature = "ed25519")]
123    pub fn derive_ed25519_key(&self, index: u32) -> AptosResult<crate::crypto::Ed25519PrivateKey> {
124        let mut seed = self.to_seed()?;
125        let result = derive_ed25519_from_seed(&seed, index);
126        // SECURITY: Zeroize seed after use
127        zeroize::Zeroize::zeroize(&mut seed);
128        let mut key = result?;
129        let private_key = crate::crypto::Ed25519PrivateKey::from_bytes(&key);
130        // SECURITY: Zeroize raw key bytes after creating the key object
131        zeroize::Zeroize::zeroize(&mut key);
132        private_key
133    }
134}
135
136/// Derives an Ed25519 key from a seed using the Aptos BIP-44 path.
137///
138/// This implements a simplified SLIP-0010 derivation for Ed25519.
139#[cfg(feature = "ed25519")]
140fn derive_ed25519_from_seed(seed: &[u8], index: u32) -> AptosResult<[u8; 32]> {
141    use hmac::{Hmac, Mac};
142    use sha2::Sha512;
143
144    type HmacSha512 = Hmac<Sha512>;
145
146    // SLIP-0010 master key derivation
147    let mut mac = HmacSha512::new_from_slice(b"ed25519 seed")
148        .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
149    mac.update(seed);
150    let result = mac.finalize().into_bytes();
151
152    let mut key = [0u8; 32];
153    let mut chain_code = [0u8; 32];
154    key.copy_from_slice(&result[..32]);
155    chain_code.copy_from_slice(&result[32..]);
156
157    // Aptos derivation path: m/44'/637'/0'/0'/index'
158    // All indices are hardened (with 0x80000000 offset)
159    let path = [
160        44 | 0x8000_0000,    // 44' (purpose)
161        637 | 0x8000_0000,   // 637' (Aptos coin type)
162        0x8000_0000,         // 0' (account)
163        0x8000_0000,         // 0' (change)
164        index | 0x8000_0000, // index' (address index)
165    ];
166
167    for child_index in path {
168        let mut data = vec![0u8];
169        data.extend_from_slice(&key);
170        data.extend_from_slice(&child_index.to_be_bytes());
171
172        let mut mac = HmacSha512::new_from_slice(&chain_code)
173            .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
174        mac.update(&data);
175        let result = mac.finalize().into_bytes();
176
177        key.copy_from_slice(&result[..32]);
178        chain_code.copy_from_slice(&result[32..]);
179
180        // SECURITY: Zeroize intermediate derivation data
181        zeroize::Zeroize::zeroize(&mut data);
182    }
183
184    // SECURITY: Zeroize chain_code since we only return the key
185    zeroize::Zeroize::zeroize(&mut chain_code);
186
187    Ok(key)
188}
189
190impl std::fmt::Debug for Mnemonic {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "Mnemonic([REDACTED])")
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_generate_mnemonic() {
202        let mnemonic = Mnemonic::generate(12).unwrap();
203        assert_eq!(mnemonic.phrase().split_whitespace().count(), 12);
204
205        let mnemonic = Mnemonic::generate(24).unwrap();
206        assert_eq!(mnemonic.phrase().split_whitespace().count(), 24);
207    }
208
209    #[test]
210    fn test_invalid_word_count() {
211        assert!(Mnemonic::generate(13).is_err());
212    }
213
214    #[test]
215    fn test_parse_mnemonic() {
216        let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
217        let mnemonic = Mnemonic::from_phrase(phrase).unwrap();
218        assert_eq!(mnemonic.phrase(), phrase);
219    }
220
221    #[test]
222    fn test_invalid_mnemonic() {
223        assert!(Mnemonic::from_phrase("invalid mnemonic phrase").is_err());
224    }
225
226    #[test]
227    #[cfg(feature = "ed25519")]
228    fn test_derive_ed25519_key() {
229        let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
230        let mnemonic = Mnemonic::from_phrase(phrase).unwrap();
231
232        let key1 = mnemonic.derive_ed25519_key(0).unwrap();
233        let key2 = mnemonic.derive_ed25519_key(0).unwrap();
234        assert_eq!(key1.to_bytes(), key2.to_bytes());
235
236        let key3 = mnemonic.derive_ed25519_key(1).unwrap();
237        assert_ne!(key1.to_bytes(), key3.to_bytes());
238    }
239}