crypto_wallet_gen/mnemonics/
scrypt.rs

1use anyhow::Result;
2use scrypt::{scrypt, Params};
3use unicode_normalization::UnicodeNormalization;
4
5use super::bip39::Bip39Mnemonic;
6use super::{Mnemonic, MnemonicFactory};
7use crate::bip32::HDPrivKey;
8use crate::seed::Seed;
9
10/// A mnemonic similar to BIP39, but using scrypt instead of PBKDF2 for the key derivation.
11#[derive(Debug)]
12pub struct ScryptMnemonic {
13    phrase: String,
14}
15
16impl MnemonicFactory for ScryptMnemonic {
17    fn generate() -> Result<Self> {
18        Ok(Self {
19            phrase: Bip39Mnemonic::generate()?.into_phrase(),
20        })
21    }
22
23    fn from_phrase(phrase: &str) -> Result<Self> {
24        Self::validate(phrase)?;
25        Ok(Self {
26            phrase: phrase.to_string(),
27        })
28    }
29
30    fn validate(phrase: &str) -> Result<()> {
31        Bip39Mnemonic::validate(phrase)
32    }
33}
34
35impl Mnemonic for ScryptMnemonic {
36    fn phrase(&self) -> &str {
37        &self.phrase
38    }
39
40    fn into_phrase(self) -> String {
41        self.phrase
42    }
43
44    fn to_private_key(&self, password: &str) -> Result<HDPrivKey> {
45        let salt = format!("mnemonic{}", password);
46        let normalized_salt = salt.nfkd().to_string();
47        let bytes = kdf(self.phrase.as_bytes(), normalized_salt.as_bytes())?;
48
49        HDPrivKey::new(Seed::from_bytes(bytes))
50    }
51}
52
53fn kdf(password: &[u8], salt: &[u8]) -> Result<Vec<u8>> {
54    const OUTPUT_BYTES: usize = 64;
55    let mut seed = vec![0u8; OUTPUT_BYTES];
56    scrypt(password, salt, &scrypt_params(), &mut seed)?;
57
58    Ok(seed)
59}
60
61#[cfg(test)]
62fn scrypt_params() -> Params {
63    // Tests need lower scrypt params or they won't be able to run on CI machines
64    Params::new(12, 1, 1).expect("Invalid hardcoded scrypt params")
65}
66
67#[cfg(not(test))]
68fn scrypt_params() -> Params {
69    // Using parameters that are higher than the ones proposed in BIP38
70    // (note log2(N) == 21 means N == 2097152)
71    Params::new(21, 8, 8).expect("Invalid hardcoded scrypt params")
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn expect_generated_key_is(expected_key: &str, phrase: &str, password: &str) {
79        assert_eq!(
80            expected_key,
81            ScryptMnemonic::from_phrase(phrase)
82                .unwrap()
83                .to_private_key(password)
84                .unwrap()
85                .to_base58()
86        );
87    }
88
89    #[test]
90    fn twelve_words_without_password() {
91        // Since there is no online sources for our scrypt approach, this was generated with
92        // our own algorithm and is more a regression test to make sure we don't accidentally
93        // change the algorithm.
94        expect_generated_key_is(
95            "xprv9s21ZrQH143K31h69CVTU374efVBSbx8PHnh27om2e7Nh4r8wjvnrb3iHrH4HWn1KVUM27YEf5UtaZt7AKvv7HBjhkmSdnoWYpVNSqQHXMK",
96            "lunch blanket cruise chair question good market allow blue celery little void",
97            "",
98        );
99    }
100
101    #[test]
102    fn twelve_words_with_password() {
103        // Since there is no online sources for our scrypt approach, this was generated with
104        // our own algorithm and is more a regression test to make sure we don't accidentally
105        // change the algorithm.
106        expect_generated_key_is(
107            "xprv9s21ZrQH143K3KrGus5NXedDmE1MHgRhy5Kpa1fsiRm3PeG6bE4oxgqRAuxFHqMPMcEKrALFKmVpMj6jAzbTaEncJSUqUCWFQdMh4njQN7X",
108            "lunch blanket cruise chair question good market allow blue celery little void",
109            "my password",
110        );
111    }
112
113    #[test]
114    fn fifteen_words_without_password() {
115        // Since there is no online sources for our scrypt approach, this was generated with
116        // our own algorithm and is more a regression test to make sure we don't accidentally
117        // change the algorithm.
118        expect_generated_key_is(
119            "xprv9s21ZrQH143K371jBvAZqkzZoXsLVNPWVtCPbyqKBtwEDY31vXqNkGuYqmJnxfPUkzSgQ4MC2BAFchkAYAirRek7BejSt59hfpnnTeGVNzS",
120            "mirror distance build unaware current concert link chapter resemble tuition main rent echo drum dolphin",
121            "");
122    }
123
124    #[test]
125    fn fifteen_words_with_password() {
126        // Since there is no online sources for our scrypt approach, this was generated with
127        // our own algorithm and is more a regression test to make sure we don't accidentally
128        // change the algorithm.
129        expect_generated_key_is(
130            "xprv9s21ZrQH143K2AqPnXvRcDw5ypxw5BpwxhuWnbeaiQwB5RueZsKZqB1TZGpBrtWiM3dGHr8BJtPMc4jTG7bDgsp2LXgQFgtDkiXxYmaArKj",
131            "mirror distance build unaware current concert link chapter resemble tuition main rent echo drum dolphin",
132            "my password");
133    }
134
135    #[test]
136    fn eighteen_words_without_password() {
137        // Since there is no online sources for our scrypt approach, this was generated with
138        // our own algorithm and is more a regression test to make sure we don't accidentally
139        // change the algorithm.
140        expect_generated_key_is(
141            "xprv9s21ZrQH143K3zCCiVgq3MAthXj1BLaD4CZa4UJXH3yttQWvXUGjMoR94eHeNLbgHpPJTQ5ayw73ng98QCXifABhnYenU73U1YvnaBt3fc7",
142            "blush section drift canoe reform friend rose cherry assume supreme home hub goat arena jazz absurd emotion hidden",
143            "");
144    }
145
146    #[test]
147    fn eighteen_words_with_password() {
148        // Since there is no online sources for our scrypt approach, this was generated with
149        // our own algorithm and is more a regression test to make sure we don't accidentally
150        // change the algorithm.
151        expect_generated_key_is(
152            "xprv9s21ZrQH143K4EWgn4SVWUrJKziE8n7qSbPC94wNRbupQXk6acDSAgv4kbBhXRqCTuspABiijrrzabcmKH14mMymF3t4uJk8MRhSogB9vjf",
153            "blush section drift canoe reform friend rose cherry assume supreme home hub goat arena jazz absurd emotion hidden",
154            "my password");
155    }
156
157    #[test]
158    fn twentyone_words_without_password() {
159        // Since there is no online sources for our scrypt approach, this was generated with
160        // our own algorithm and is more a regression test to make sure we don't accidentally
161        // change the algorithm.
162        expect_generated_key_is(
163            "xprv9s21ZrQH143K3RaUETg9duZwV5CtwsKwV2BRjy1e5CWCLt8YQHrFCTic42gAhfL91NidSJfpmie8YWMycpMRPrMLAC87hrDjvgreCRDbrBu",
164            "include disagree sentence junior gospel engage whip old boost scrap someone helmet list best afraid favorite gold antenna before peasant buffalo",
165            "");
166    }
167
168    #[test]
169    fn twentyone_words_with_password() {
170        // Since there is no online sources for our scrypt approach, this was generated with
171        // our own algorithm and is more a regression test to make sure we don't accidentally
172        // change the algorithm.
173        expect_generated_key_is(
174            "xprv9s21ZrQH143K46Wg1D47KYpxFsZWBsm9Xth7AJUgHwCAd2iKLowwbHK56JDBVtiyya2q4TScLAS8NvE81aZtN3GFbm3exeXjKdATmBAfz6e",
175            "include disagree sentence junior gospel engage whip old boost scrap someone helmet list best afraid favorite gold antenna before peasant buffalo",
176            "my password");
177    }
178
179    #[test]
180    fn twentyfour_words_without_password() {
181        // Since there is no online sources for our scrypt approach, this was generated with
182        // our own algorithm and is more a regression test to make sure we don't accidentally
183        // change the algorithm.
184        expect_generated_key_is(
185            "xprv9s21ZrQH143K3jgRiJbM3phUCscqjNpU7VSedfquJ9BeW2DdmMaksZvf3cjMFMfhPqgxNtMxhZgjQyzDSvQq8ASTQqcPN5pkiKCbf59rAt8",
186            "table car outdoor twist dutch auction monitor rude pumpkin very disease ability hope area metal brisk luggage tell ribbon profit various lake topic exist",
187            "");
188    }
189
190    #[test]
191    fn twentyfour_words_with_password() {
192        // Since there is no online sources for our scrypt approach, this was generated with
193        // our own algorithm and is more a regression test to make sure we don't accidentally
194        // change the algorithm.
195        expect_generated_key_is(
196            "xprv9s21ZrQH143K2wDqEuYRrXbVruhDgcVMe4fSqYMjny7shxkLUe2HLxSQNFvUKt3VA68v2q43UXSPAjMTdRV7DEN5bo4hCV8wvbbaHhDxNAK",
197            "table car outdoor twist dutch auction monitor rude pumpkin very disease ability hope area metal brisk luggage tell ribbon profit various lake topic exist",
198            "my password");
199    }
200
201    #[test]
202    fn generated_phrase_is_24_words() {
203        let phrase = ScryptMnemonic::generate().unwrap().into_phrase();
204        assert_eq!(23, phrase.chars().filter(|a| *a == ' ').count());
205    }
206
207    #[test]
208    fn generated_phrase_is_valid() {
209        ScryptMnemonic::validate(ScryptMnemonic::generate().unwrap().phrase()).unwrap();
210    }
211
212    #[test]
213    fn validate_valid_24word_phrase() {
214        ScryptMnemonic::validate("desert armed renew matrix congress order remove lab travel shallow there tool symbol three radio exhibit pledge alcohol quit host rare noble dose eager").unwrap();
215    }
216
217    #[test]
218    fn validate_valid_21word_phrase() {
219        ScryptMnemonic::validate("morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm hobby").unwrap();
220    }
221
222    #[test]
223    fn validate_valid_18word_phrase() {
224        ScryptMnemonic::validate("slice lift violin movie shield copy tail arrow idle lift knock fossil leave lawsuit tennis sight travel vivid").unwrap();
225    }
226
227    #[test]
228    fn validate_valid_15word_phrase() {
229        ScryptMnemonic::validate("call oval opinion exhibit limit write fine prepare sleep possible extend language split kidney desert").unwrap();
230    }
231
232    #[test]
233    fn validate_valid_12word_phrase() {
234        ScryptMnemonic::validate(
235            "tornado ginger error because arrange lake scale unfold palm theme frozen sick",
236        )
237        .unwrap();
238    }
239
240    #[test]
241    fn validate_invalid_20word_phrase() {
242        let err = ScryptMnemonic::validate(
243            "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm",
244        )
245        .unwrap_err();
246        assert!(err
247            .to_string()
248            .contains("invalid number of words in phrase"))
249    }
250
251    #[test]
252    fn validate_invalid_21word_phrase() {
253        let err = ScryptMnemonic::validate(
254            "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm prepare",
255        )
256        .unwrap_err();
257        assert!(err.to_string().contains("invalid checksum"))
258    }
259
260    #[test]
261    fn from_invalid_20word_phrase() {
262        let err = ScryptMnemonic::from_phrase(
263            "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm",
264        )
265        .unwrap_err();
266        assert!(err
267            .to_string()
268            .contains("invalid number of words in phrase"))
269    }
270
271    #[test]
272    fn from_invalid_21word_phrase() {
273        let err = ScryptMnemonic::from_phrase(
274            "morning mind present cloud boat phrase task uniform effort couple carpet wise steak eyebrow friend birth million photo tobacco firm prepare",
275        )
276        .unwrap_err();
277        assert!(err.to_string().contains("invalid checksum"))
278    }
279}