crypto_wallet_gen/mnemonics/
scrypt.rs1use 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#[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 Params::new(12, 1, 1).expect("Invalid hardcoded scrypt params")
65}
66
67#[cfg(not(test))]
68fn scrypt_params() -> Params {
69 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 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 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 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 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 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 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 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 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 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 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}