tonlib_core/wallet/
mnemonic.rs

1use std::collections::HashMap;
2use std::{cmp, fmt};
3
4use hmac::{Hmac, Mac};
5use lazy_static::lazy_static;
6use nacl::sign::generate_keypair;
7use pbkdf2::password_hash::Output;
8use pbkdf2::{pbkdf2_hmac, Params};
9use sha2::Sha512;
10
11use crate::wallet::error::MnemonicError;
12
13const WORDLIST_EN: &str = include_str!("../../resources/mnemonic/wordlist.EN");
14const PBKDF_ITERATIONS: u32 = 100000;
15
16lazy_static! {
17    pub static ref WORDLIST_EN_SET: HashMap<&'static str, usize> = {
18        let words: HashMap<&'static str, usize> = WORDLIST_EN
19            .split('\n')
20            .filter(|w| !w.is_empty())
21            .enumerate()
22            .map(|(i, w)| (w.trim(), i))
23            .collect();
24        words
25    };
26}
27
28/// A Rust port of https://github.com/tonwhales/ton-crypto/blob/master/src/mnemonic/mnemonic.ts
29pub struct Mnemonic {
30    words: Vec<String>,
31    password: Option<String>,
32}
33
34#[derive(PartialEq, Eq, Clone, Hash)]
35pub struct KeyPair {
36    pub public_key: Vec<u8>,
37    pub secret_key: Vec<u8>,
38}
39
40impl fmt::Debug for KeyPair {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        f.debug_struct("KeyPair")
43            .field("public_key", &self.public_key)
44            .field("secret_key", &"***REDACTED***")
45            .finish()
46    }
47}
48
49impl Mnemonic {
50    pub fn new(words: Vec<&str>, password: &Option<String>) -> Result<Mnemonic, MnemonicError> {
51        let normalized_words: Vec<String> = words.iter().map(|w| w.trim().to_lowercase()).collect();
52
53        // Check words
54        if normalized_words.len() != 24 {
55            return Err(MnemonicError::UnexpectedWordCount(normalized_words.len()));
56        }
57        for word in &normalized_words {
58            if !WORDLIST_EN_SET.contains_key(word.as_str()) {
59                return Err(MnemonicError::InvalidWord(word.clone()));
60            }
61        }
62
63        // Check password validity
64        match password {
65            Some(s) if !s.is_empty() => {
66                let passless_entropy = to_entropy(&normalized_words, &None)?;
67                let seed = pbkdf2_sha512(passless_entropy, "TON fast seed version", 1, 64)?;
68                if seed[0] != 1 {
69                    return Err(MnemonicError::InvalidFirstByte(seed[0]));
70                }
71                // Make that this also is not a valid passwordless mnemonic
72                let entropy = to_entropy(&normalized_words, password)?;
73                let seed = pbkdf2_sha512(
74                    entropy,
75                    "TON seed version",
76                    cmp::max(1, PBKDF_ITERATIONS / 256),
77                    64,
78                )?;
79                if seed[0] == 0 {
80                    return Err(MnemonicError::InvalidFirstByte(seed[0]));
81                }
82            }
83            _ => {
84                let entropy = to_entropy(&normalized_words, &None)?;
85                let seed = pbkdf2_sha512(
86                    entropy,
87                    "TON seed version",
88                    cmp::max(1, PBKDF_ITERATIONS / 256),
89                    64,
90                )?;
91                if seed[0] != 0 {
92                    return Err(MnemonicError::InvalidPasswordlessMenmonicFirstByte(seed[0]));
93                }
94            }
95        }
96
97        let mnemonic = Mnemonic {
98            words: normalized_words,
99            password: password.clone(),
100        };
101        Ok(mnemonic)
102    }
103
104    pub fn from_str(s: &str, password: &Option<String>) -> Result<Mnemonic, MnemonicError> {
105        let words: Vec<&str> = s
106            .split(' ')
107            .map(|w| w.trim())
108            .filter(|w| !w.is_empty())
109            .collect();
110        Mnemonic::new(words, password)
111    }
112
113    pub fn to_key_pair(&self) -> Result<KeyPair, MnemonicError> {
114        let entropy = to_entropy(&self.words, &self.password)?;
115        let seed = pbkdf2_sha512(entropy, "TON default seed", PBKDF_ITERATIONS, 64)?;
116        let key_pair = generate_keypair(&seed.as_slice()[0..32]);
117        Ok(KeyPair {
118            public_key: key_pair.pkey.to_vec(),
119            secret_key: key_pair.skey.to_vec(),
120        })
121    }
122}
123
124fn to_entropy(words: &[String], password: &Option<String>) -> Result<Vec<u8>, MnemonicError> {
125    let mut mac = Hmac::<Sha512>::new_from_slice(words.join(" ").as_bytes())?;
126    if let Some(s) = password {
127        mac.update(s.as_bytes());
128    }
129    let result = mac.finalize();
130    let code_bytes = result.into_bytes().to_vec();
131    Ok(code_bytes)
132}
133
134fn pbkdf2_sha512(
135    key: Vec<u8>,
136    salt: &str,
137    rounds: u32,
138    output_length: usize,
139) -> Result<Vec<u8>, MnemonicError> {
140    let params = Params {
141        rounds,
142        output_length,
143    };
144
145    let output = Output::init_with(params.output_length, |out| {
146        pbkdf2_hmac::<Sha512>(key.as_slice(), salt.as_bytes(), params.rounds, out);
147        Ok(())
148    })
149    .map_err(MnemonicError::PasswordHashError)?;
150    Ok(output.as_bytes().to_vec())
151}
152
153///Based on https://github.com/tonwhales/ton-crypto/blob/master/src/mnemonic/mnemonic.spec.ts
154#[cfg(test)]
155mod tests {
156    use super::{Mnemonic, MnemonicError};
157
158    #[test]
159    fn mnemonic_parse_works() -> Result<(), MnemonicError> {
160        let words = "dose ice enrich trigger test dove century still betray gas diet dune use other base gym mad law immense village world example praise game";
161        let mnemonic = Mnemonic::from_str(words, &None);
162        assert!(mnemonic.is_ok());
163
164        let words = " dose ice enrich trigger test dove \
165        century still betray gas diet       dune use other base gym mad law \
166        immense village world example praise game ";
167        let mnemonic = Mnemonic::from_str(words, &None);
168        assert!(mnemonic.is_ok());
169        Ok(())
170    }
171
172    #[test]
173    fn mnemonic_validate_works() -> Result<(), MnemonicError> {
174        let mnemonic = Mnemonic::new(
175            vec![
176                "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
177                "gas", "diet", "dune",
178            ],
179            &None,
180        );
181        assert!(mnemonic.is_err());
182        let mnemonic = Mnemonic::new(vec!["a"], &None);
183        assert!(mnemonic.is_err());
184        Ok(())
185    }
186
187    #[test]
188    fn mnemonic_to_private_key_works() -> Result<(), MnemonicError> {
189        let mnemonic = Mnemonic::new(
190            vec![
191                "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
192                "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
193                "village", "world", "example", "praise", "game",
194            ],
195            &None,
196        )?;
197        let expected = "119dcf2840a3d56521d260b2f125eedc0d4f3795b9e627269a4b5a6dca8257bdc04ad1885c127fe863abb00752fa844e6439bb04f264d70de7cea580b32637ab";
198
199        let kp = mnemonic.to_key_pair()?;
200        println!("{:?} {:?}", kp.public_key, kp.secret_key);
201
202        let res = hex::encode(kp.secret_key);
203
204        assert_eq!(res, expected);
205
206        Ok(())
207    }
208}