1#![forbid(unsafe_code)]
4
5use aelf_crypto::Wallet;
6use aes::{Aes128, Aes192, Aes256};
7use cbc::{Decryptor as CbcDecryptor, Encryptor as CbcEncryptor};
8use cipher::block_padding::Pkcs7;
9use cipher::{BlockDecryptMut, BlockEncryptMut, InvalidLength, KeyIvInit, StreamCipher};
10use core::cmp;
11use ctr::Ctr128BE;
12use pbkdf2::pbkdf2_hmac;
13use rand::{rng, RngCore};
14use salsa20::{
15 cipher::{typenum::U4, StreamCipherCore},
16 SalsaCore,
17};
18use scrypt::{scrypt, Params};
19use serde::{Deserialize, Serialize};
20use sha3::{Digest, Keccak256};
21use std::fmt;
22use thiserror::Error;
23use zeroize::{Zeroize, Zeroizing};
24
25const DEFAULT_DKLEN: usize = 32;
26const DEFAULT_N: u32 = 8192;
27const DEFAULT_R: u32 = 8;
28const DEFAULT_P: u32 = 1;
29const DEFAULT_CIPHER: &str = "aes-128-ctr";
30
31#[derive(Debug, Error)]
33pub enum KeystoreError {
34 #[error("invalid scrypt params")]
35 InvalidScryptParams,
36 #[error("unsupported cipher: {0}")]
37 UnsupportedCipher(String),
38 #[error("cipher key or iv length mismatch")]
39 InvalidCipherLength(#[from] InvalidLength),
40 #[error("invalid password")]
41 InvalidPassword,
42 #[error("cipher padding error")]
43 CipherPadding,
44 #[error("hex decode error: {0}")]
45 Hex(#[from] hex::FromHexError),
46 #[error("json error: {0}")]
47 Json(#[from] serde_json::Error),
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct Keystore {
53 pub version: u32,
54 #[serde(rename = "type", default)]
55 pub kind: String,
56 #[serde(rename = "nickName", default)]
57 pub nick_name: String,
58 #[serde(default)]
59 pub id: Option<String>,
60 #[serde(default)]
61 pub address: String,
62 pub crypto: KeystoreCrypto,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67pub struct KeystoreCrypto {
68 pub cipher: String,
69 pub ciphertext: String,
70 pub cipherparams: CipherParams,
71 #[serde(rename = "mnemonicEncrypted", default)]
72 pub mnemonic_encrypted: String,
73 pub kdf: String,
74 pub kdfparams: KdfParams,
75 pub mac: String,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
80pub struct CipherParams {
81 pub iv: String,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86pub struct KdfParams {
87 pub r: u32,
88 pub n: u32,
89 pub p: u32,
90 #[serde(alias = "dkLen")]
91 pub dklen: usize,
92 pub salt: String,
93}
94
95#[derive(Clone, PartialEq, Eq)]
99pub struct UnlockedKeystore {
100 pub nick_name: String,
101 pub address: String,
102 pub mnemonic: String,
103 pub private_key: String,
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct KeystoreEncryptOptions {
109 pub cipher: String,
110 pub dklen: usize,
111 pub n: u32,
112 pub r: u32,
113 pub p: u32,
114 pub salt: Option<Vec<u8>>,
115 pub iv: Option<Vec<u8>>,
116 pub nick_name: Option<String>,
117 pub address: Option<String>,
118}
119
120impl Default for KeystoreEncryptOptions {
121 fn default() -> Self {
122 Self {
123 cipher: DEFAULT_CIPHER.to_owned(),
124 dklen: DEFAULT_DKLEN,
125 n: DEFAULT_N,
126 r: DEFAULT_R,
127 p: DEFAULT_P,
128 salt: None,
129 iv: None,
130 nick_name: None,
131 address: None,
132 }
133 }
134}
135
136impl Keystore {
137 pub fn encrypt_js(wallet: &Wallet, password: &str) -> Result<Self, KeystoreError> {
139 Self::encrypt_js_with_options(wallet, password, KeystoreEncryptOptions::default())
140 }
141
142 pub fn encrypt_js_with_options(
144 wallet: &Wallet,
145 password: &str,
146 options: KeystoreEncryptOptions,
147 ) -> Result<Self, KeystoreError> {
148 let spec = CipherSpec::parse(&options.cipher)?;
149 ensure_derived_key_len(options.dklen, spec)?;
150 let salt = options.salt.unwrap_or_else(|| random_bytes(32));
151 let iv = options.iv.unwrap_or_else(|| random_bytes(spec.iv_len()));
152 let derived_key = Zeroizing::new(derive_key(
153 password,
154 &salt,
155 options.n,
156 options.r,
157 options.p,
158 options.dklen,
159 )?);
160 let private_key = Zeroizing::new(hex::decode(wallet.private_key())?);
161 let private_key_ciphertext =
162 encrypt(spec, &derived_key[..spec.key_len()], &iv, &private_key)?;
163 let mnemonic_ciphertext = encrypt(
164 spec,
165 &derived_key[..spec.key_len()],
166 &iv,
167 wallet.mnemonic().as_bytes(),
168 )?;
169
170 let mut raw_mac = Zeroizing::new(derived_key[16..].to_vec());
171 raw_mac.extend_from_slice(&private_key_ciphertext);
172
173 Ok(Self {
174 version: 1,
175 kind: "aelf".to_owned(),
176 nick_name: options.nick_name.unwrap_or_default(),
177 id: None,
178 address: options
179 .address
180 .unwrap_or_else(|| wallet.address().to_owned()),
181 crypto: KeystoreCrypto {
182 cipher: spec.as_str().to_owned(),
183 ciphertext: hex::encode(private_key_ciphertext),
184 cipherparams: CipherParams {
185 iv: hex::encode(iv),
186 },
187 mnemonic_encrypted: hex::encode(mnemonic_ciphertext),
188 kdf: "scrypt".to_owned(),
189 kdfparams: KdfParams {
190 r: options.r,
191 n: options.n,
192 p: options.p,
193 dklen: options.dklen,
194 salt: hex::encode(salt),
195 },
196 mac: keccak256_hex(&raw_mac),
197 },
198 })
199 }
200
201 pub fn unlock_js(&self, password: &str) -> Result<UnlockedKeystore, KeystoreError> {
203 let spec = CipherSpec::parse(&self.crypto.cipher)?;
204 ensure_derived_key_len(self.crypto.kdfparams.dklen, spec)?;
205 let salt = hex::decode(&self.crypto.kdfparams.salt)?;
206 let iv = hex::decode(&self.crypto.cipherparams.iv)?;
207 let ciphertext = hex::decode(&self.crypto.ciphertext)?;
208 let mnemonic_ciphertext = hex::decode(&self.crypto.mnemonic_encrypted)?;
209 let derived_key = Zeroizing::new(derive_key(
210 password,
211 &salt,
212 self.crypto.kdfparams.n,
213 self.crypto.kdfparams.r,
214 self.crypto.kdfparams.p,
215 self.crypto.kdfparams.dklen,
216 )?);
217
218 let mut raw_mac = Zeroizing::new(derived_key[16..].to_vec());
219 raw_mac.extend_from_slice(&ciphertext);
220 if keccak256_hex(&raw_mac) != self.crypto.mac {
221 return Err(KeystoreError::InvalidPassword);
222 }
223
224 let private_key = Zeroizing::new(decrypt(
225 spec,
226 &derived_key[..spec.key_len()],
227 &iv,
228 &ciphertext,
229 )?);
230 let mnemonic = Zeroizing::new(decrypt(
231 spec,
232 &derived_key[..spec.key_len()],
233 &iv,
234 &mnemonic_ciphertext,
235 )?);
236
237 Ok(UnlockedKeystore {
238 nick_name: self.nick_name.clone(),
239 address: self.address.clone(),
240 mnemonic: String::from_utf8_lossy(&mnemonic).into_owned(),
241 private_key: hex::encode(&*private_key),
242 })
243 }
244
245 pub fn check_password(&self, password: &str) -> bool {
247 self.unlock_js(password).is_ok()
248 }
249}
250
251impl fmt::Debug for UnlockedKeystore {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 f.debug_struct("UnlockedKeystore")
254 .field("nick_name", &self.nick_name)
255 .field("address", &self.address)
256 .field("mnemonic", &"<redacted>")
257 .field("private_key", &"<redacted>")
258 .finish()
259 }
260}
261
262impl Drop for UnlockedKeystore {
263 fn drop(&mut self) {
264 self.mnemonic.zeroize();
265 self.private_key.zeroize();
266 }
267}
268
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
270enum CipherSpec {
271 Aes128Ctr,
272 Aes192Ctr,
273 Aes256Ctr,
274 Aes128Cbc,
275 Aes192Cbc,
276 Aes256Cbc,
277}
278
279impl CipherSpec {
280 fn parse(value: &str) -> Result<Self, KeystoreError> {
281 match value.to_ascii_lowercase().as_str() {
282 "aes-128-ctr" => Ok(Self::Aes128Ctr),
283 "aes-192-ctr" => Ok(Self::Aes192Ctr),
284 "aes-256-ctr" => Ok(Self::Aes256Ctr),
285 "aes-128-cbc" | "aes128" => Ok(Self::Aes128Cbc),
286 "aes-192-cbc" | "aes192" => Ok(Self::Aes192Cbc),
287 "aes-256-cbc" | "aes256" => Ok(Self::Aes256Cbc),
288 other => Err(KeystoreError::UnsupportedCipher(other.to_owned())),
289 }
290 }
291
292 fn as_str(self) -> &'static str {
293 match self {
294 Self::Aes128Ctr => "aes-128-ctr",
295 Self::Aes192Ctr => "aes-192-ctr",
296 Self::Aes256Ctr => "aes-256-ctr",
297 Self::Aes128Cbc => "aes-128-cbc",
298 Self::Aes192Cbc => "aes-192-cbc",
299 Self::Aes256Cbc => "aes-256-cbc",
300 }
301 }
302
303 fn key_len(self) -> usize {
304 match self {
305 Self::Aes128Ctr | Self::Aes128Cbc => 16,
306 Self::Aes192Ctr | Self::Aes192Cbc => 24,
307 Self::Aes256Ctr | Self::Aes256Cbc => 32,
308 }
309 }
310
311 fn iv_len(self) -> usize {
312 16
313 }
314}
315
316fn derive_key(
317 password: &str,
318 salt: &[u8],
319 n: u32,
320 r: u32,
321 p: u32,
322 dklen: usize,
323) -> Result<Vec<u8>, KeystoreError> {
324 if !n.is_power_of_two() || n == 0 || r == 0 || p == 0 || dklen == 0 {
325 return Err(KeystoreError::InvalidScryptParams);
326 }
327 let log_n = n.ilog2() as u8;
328 let params = match Params::new(log_n, r, p, dklen) {
329 Ok(params) => params,
330 Err(_) => return derive_key_compat(password, salt, n, r, p, dklen),
331 };
332 let mut derived_key = vec![0_u8; dklen];
333 scrypt(password.as_bytes(), salt, ¶ms, &mut derived_key)
334 .map_err(|_| KeystoreError::InvalidScryptParams)?;
335 Ok(derived_key)
336}
337
338fn derive_key_compat(
340 password: &str,
341 salt: &[u8],
342 n: u32,
343 r: u32,
344 p: u32,
345 dklen: usize,
346) -> Result<Vec<u8>, KeystoreError> {
347 let params = CompatScryptParams::new(n, r, p, dklen)?;
348 compat_scrypt(password.as_bytes(), salt, ¶ms)
349}
350
351fn ensure_derived_key_len(dklen: usize, spec: CipherSpec) -> Result<(), KeystoreError> {
352 let minimum = cmp::max(16, spec.key_len());
353 if dklen < minimum {
354 return Err(KeystoreError::InvalidScryptParams);
355 }
356 Ok(())
357}
358
359fn encrypt(
360 spec: CipherSpec,
361 key: &[u8],
362 iv: &[u8],
363 plaintext: &[u8],
364) -> Result<Vec<u8>, KeystoreError> {
365 match spec {
366 CipherSpec::Aes128Ctr => encrypt_aes128_ctr(key, iv, plaintext),
367 CipherSpec::Aes192Ctr => encrypt_aes192_ctr(key, iv, plaintext),
368 CipherSpec::Aes256Ctr => encrypt_aes256_ctr(key, iv, plaintext),
369 CipherSpec::Aes128Cbc => encrypt_aes128_cbc(key, iv, plaintext),
370 CipherSpec::Aes192Cbc => encrypt_aes192_cbc(key, iv, plaintext),
371 CipherSpec::Aes256Cbc => encrypt_aes256_cbc(key, iv, plaintext),
372 }
373}
374
375fn decrypt(
376 spec: CipherSpec,
377 key: &[u8],
378 iv: &[u8],
379 ciphertext: &[u8],
380) -> Result<Vec<u8>, KeystoreError> {
381 match spec {
382 CipherSpec::Aes128Ctr => decrypt_aes128_ctr(key, iv, ciphertext),
383 CipherSpec::Aes192Ctr => decrypt_aes192_ctr(key, iv, ciphertext),
384 CipherSpec::Aes256Ctr => decrypt_aes256_ctr(key, iv, ciphertext),
385 CipherSpec::Aes128Cbc => decrypt_aes128_cbc(key, iv, ciphertext),
386 CipherSpec::Aes192Cbc => decrypt_aes192_cbc(key, iv, ciphertext),
387 CipherSpec::Aes256Cbc => decrypt_aes256_cbc(key, iv, ciphertext),
388 }
389}
390
391fn encrypt_aes128_ctr(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
392 encrypt_ctr::<Ctr128BE<Aes128>>(key, iv, plaintext)
393}
394
395fn decrypt_aes128_ctr(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
396 decrypt_ctr::<Ctr128BE<Aes128>>(key, iv, ciphertext)
397}
398
399fn encrypt_aes192_ctr(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
400 encrypt_ctr::<Ctr128BE<Aes192>>(key, iv, plaintext)
401}
402
403fn decrypt_aes192_ctr(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
404 decrypt_ctr::<Ctr128BE<Aes192>>(key, iv, ciphertext)
405}
406
407fn encrypt_aes256_ctr(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
408 encrypt_ctr::<Ctr128BE<Aes256>>(key, iv, plaintext)
409}
410
411fn decrypt_aes256_ctr(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
412 decrypt_ctr::<Ctr128BE<Aes256>>(key, iv, ciphertext)
413}
414
415fn encrypt_aes128_cbc(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
416 encrypt_cbc::<Aes128>(key, iv, plaintext)
417}
418
419fn decrypt_aes128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
420 decrypt_cbc::<Aes128>(key, iv, ciphertext)
421}
422
423fn encrypt_aes192_cbc(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
424 encrypt_cbc::<Aes192>(key, iv, plaintext)
425}
426
427fn decrypt_aes192_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
428 decrypt_cbc::<Aes192>(key, iv, ciphertext)
429}
430
431fn encrypt_aes256_cbc(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
432 encrypt_cbc::<Aes256>(key, iv, plaintext)
433}
434
435fn decrypt_aes256_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError> {
436 decrypt_cbc::<Aes256>(key, iv, ciphertext)
437}
438
439fn encrypt_ctr<C>(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError>
440where
441 C: KeyIvInit + StreamCipher,
442{
443 let mut cipher = C::new_from_slices(key, iv)?;
444 let mut output = plaintext.to_vec();
445 cipher.apply_keystream(&mut output);
446 Ok(output)
447}
448
449fn decrypt_ctr<C>(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError>
450where
451 C: KeyIvInit + StreamCipher,
452{
453 encrypt_ctr::<C>(key, iv, ciphertext)
454}
455
456fn encrypt_cbc<C>(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, KeystoreError>
457where
458 C: cipher::BlockCipher + cipher::BlockEncryptMut + cipher::KeyInit,
459{
460 let mut buffer = vec![0_u8; plaintext.len() + 16];
461 buffer[..plaintext.len()].copy_from_slice(plaintext);
462 let encrypted = CbcEncryptor::<C>::new_from_slices(key, iv)?
463 .encrypt_padded_mut::<Pkcs7>(&mut buffer, plaintext.len())
464 .map_err(|_| KeystoreError::CipherPadding)?;
465 Ok(encrypted.to_vec())
466}
467
468fn decrypt_cbc<C>(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, KeystoreError>
469where
470 C: cipher::BlockCipher + cipher::BlockDecryptMut + cipher::KeyInit,
471{
472 let mut buffer = ciphertext.to_vec();
473 let decrypted = CbcDecryptor::<C>::new_from_slices(key, iv)?
474 .decrypt_padded_mut::<Pkcs7>(&mut buffer)
475 .map_err(|_| KeystoreError::InvalidPassword)?;
476 Ok(decrypted.to_vec())
477}
478
479fn random_bytes(len: usize) -> Vec<u8> {
480 let mut bytes = vec![0_u8; len];
481 rng().fill_bytes(&mut bytes);
482 bytes
483}
484
485fn keccak256_hex(bytes: &[u8]) -> String {
486 hex::encode(Keccak256::digest(bytes))
487}
488
489#[derive(Clone, Copy, Debug)]
490struct CompatScryptParams {
491 n: usize,
492 r: usize,
493 p: usize,
494 dklen: usize,
495}
496
497impl CompatScryptParams {
498 fn new(n: u32, r: u32, p: u32, dklen: usize) -> Result<Self, KeystoreError> {
499 if !n.is_power_of_two() || n == 0 || r == 0 || p == 0 || dklen == 0 {
500 return Err(KeystoreError::InvalidScryptParams);
501 }
502 if dklen / 32 > 0xffff_ffff {
503 return Err(KeystoreError::InvalidScryptParams);
504 }
505
506 let n = n as usize;
507 let r = r as usize;
508 let p = p as usize;
509
510 let r128 = r
511 .checked_mul(128)
512 .ok_or(KeystoreError::InvalidScryptParams)?;
513 let pr128 = p
514 .checked_mul(r128)
515 .ok_or(KeystoreError::InvalidScryptParams)?;
516 let nr128 = n
517 .checked_mul(r128)
518 .ok_or(KeystoreError::InvalidScryptParams)?;
519
520 let _ = pr128
522 .checked_add(nr128)
523 .ok_or(KeystoreError::InvalidScryptParams)?;
524
525 Ok(Self { n, r, p, dklen })
526 }
527}
528
529fn compat_scrypt(
530 password: &[u8],
531 salt: &[u8],
532 params: &CompatScryptParams,
533) -> Result<Vec<u8>, KeystoreError> {
534 let r128 = params
535 .r
536 .checked_mul(128)
537 .ok_or(KeystoreError::InvalidScryptParams)?;
538 let pr128 = params
539 .p
540 .checked_mul(r128)
541 .ok_or(KeystoreError::InvalidScryptParams)?;
542 let nr128 = params
543 .n
544 .checked_mul(r128)
545 .ok_or(KeystoreError::InvalidScryptParams)?;
546
547 let mut b = Zeroizing::new(vec![0_u8; pr128]);
548 pbkdf2_hmac::<sha2::Sha256>(password, salt, 1, &mut b);
549
550 let mut v = Zeroizing::new(vec![0_u8; nr128]);
551 let mut t = Zeroizing::new(vec![0_u8; r128]);
552
553 for chunk in b.chunks_mut(r128) {
554 compat_scrypt_ro_mix(chunk, &mut v, &mut t, params.n);
555 }
556
557 let mut derived_key = vec![0_u8; params.dklen];
558 pbkdf2_hmac::<sha2::Sha256>(password, &b, 1, &mut derived_key);
559 Ok(derived_key)
560}
561
562fn compat_scrypt_ro_mix(b: &mut [u8], v: &mut [u8], t: &mut [u8], n: usize) {
563 let len = b.len();
564
565 for chunk in v.chunks_mut(len) {
566 chunk.copy_from_slice(b);
567 compat_scrypt_block_mix(chunk, b);
568 }
569
570 for _ in 0..n {
571 let j = compat_integerify(b, n);
572 compat_xor(b, &v[j * len..(j + 1) * len], t);
573 compat_scrypt_block_mix(t, b);
574 }
575}
576
577fn compat_integerify(x: &[u8], n: usize) -> usize {
578 let mask = n - 1;
579 let t = u32::from_le_bytes(
580 x[x.len() - 64..x.len() - 60]
581 .try_into()
582 .expect("integerify slice"),
583 );
584 (t as usize) & mask
585}
586
587fn compat_scrypt_block_mix(input: &[u8], output: &mut [u8]) {
588 type Salsa20_8 = SalsaCore<U4>;
589
590 let mut x = [0_u8; 64];
591 x.copy_from_slice(&input[input.len() - 64..]);
592
593 let mut t = [0_u8; 64];
594
595 for (i, chunk) in input.chunks(64).enumerate() {
596 compat_xor(&x, chunk, &mut t);
597
598 let mut state = [0_u32; 16];
599 for (chunk, word) in t.chunks_exact(4).zip(state.iter_mut()) {
600 *word = u32::from_le_bytes(chunk.try_into().expect("salsa chunk"));
601 }
602
603 Salsa20_8::from_raw_state(state).write_keystream_block((&mut x).into());
604
605 let pos = if i % 2 == 0 {
606 (i / 2) * 64
607 } else {
608 (i / 2) * 64 + input.len() / 2
609 };
610
611 output[pos..pos + 64].copy_from_slice(&x);
612 }
613}
614
615fn compat_xor(x: &[u8], y: &[u8], output: &mut [u8]) {
616 for ((out, &lhs), &rhs) in output.iter_mut().zip(x.iter()).zip(y.iter()) {
617 *out = lhs ^ rhs;
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::{Keystore, KeystoreEncryptOptions};
624 use aelf_crypto::Wallet;
625
626 const CSHARP_KEYSTORE_FIXTURE: &str = include_str!(concat!(
627 env!("CARGO_MANIFEST_DIR"),
628 "/../../tests/fixtures/csharp-keystore-v3.json"
629 ));
630 const TEST_MNEMONIC: &str =
631 "orange learn result add snack curtain double state expose bless also clarify";
632 const TEST_PRIVATE_KEY: &str =
633 "ff96c3463af0b8629f170f078f97ac0147490b92e1784e3bff93f7ee9d1abcb6";
634
635 #[test]
636 fn roundtrip_default_keystore() {
637 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
638 let keystore = Keystore::encrypt_js(&wallet, "123123").expect("keystore");
639 let unlocked = keystore.unlock_js("123123").expect("unlock");
640 assert_eq!(unlocked.private_key, wallet.private_key());
641 assert_eq!(unlocked.mnemonic, wallet.mnemonic());
642 assert_eq!(unlocked.address, wallet.address());
643 }
644
645 #[test]
646 fn supports_dk_len_alias_on_import() {
647 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
648 let keystore = Keystore::encrypt_js_with_options(
649 &wallet,
650 "123123",
651 KeystoreEncryptOptions {
652 salt: Some(vec![0x11; 32]),
653 iv: Some(vec![0x22; 16]),
654 ..KeystoreEncryptOptions::default()
655 },
656 )
657 .expect("keystore");
658 let mut json = serde_json::to_value(&keystore).expect("json");
659 let dklen = json["crypto"]["kdfparams"]["dklen"]
660 .as_u64()
661 .expect("dklen");
662 json["crypto"]["kdfparams"]
663 .as_object_mut()
664 .expect("kdfparams")
665 .insert("dkLen".to_owned(), serde_json::json!(dklen));
666 json["crypto"]["kdfparams"]
667 .as_object_mut()
668 .expect("kdfparams")
669 .remove("dklen");
670 let imported: Keystore = serde_json::from_value(json).expect("import");
671 let unlocked = imported.unlock_js("123123").expect("unlock");
672 assert_eq!(unlocked.private_key, wallet.private_key());
673 }
674
675 #[test]
676 fn wrong_password_returns_false() {
677 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
678 let keystore = Keystore::encrypt_js(&wallet, "123123").expect("keystore");
679 assert!(!keystore.check_password("wrong-password"));
680 }
681
682 #[test]
683 fn rejects_short_dklen_before_slice() {
684 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
685 let error = Keystore::encrypt_js_with_options(
686 &wallet,
687 "123123",
688 KeystoreEncryptOptions {
689 dklen: 8,
690 ..KeystoreEncryptOptions::default()
691 },
692 )
693 .expect_err("short dklen should fail");
694 assert!(matches!(error, super::KeystoreError::InvalidScryptParams));
695 }
696
697 #[test]
698 fn imports_csharp_v3_keystore_fixture() {
699 let keystore: Keystore = serde_json::from_str(CSHARP_KEYSTORE_FIXTURE).expect("fixture");
700 let unlocked = keystore.unlock_js("abcde").expect("unlock");
701 assert_eq!(unlocked.private_key, TEST_PRIVATE_KEY);
702 assert_eq!(unlocked.mnemonic, "");
703 assert_eq!(
704 unlocked.address,
705 "VQFq9atg4fMtFLhqpVh48ZnhX8FXMGBHW8MDANPpCSHcZisU6"
706 );
707 }
708
709 #[test]
710 fn debug_redacts_unlocked_keystore_secrets() {
711 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
712 let keystore = Keystore::encrypt_js(&wallet, "123123").expect("keystore");
713 let unlocked = keystore.unlock_js("123123").expect("unlock");
714 let debug = format!("{unlocked:?}");
715 assert!(!debug.contains(TEST_MNEMONIC));
716 assert!(!debug.contains(wallet.private_key()));
717 assert!(debug.contains(wallet.address()));
718 }
719
720 #[test]
721 fn compat_scrypt_matches_standard_scrypt_for_supported_params() {
722 let salt = [0x11_u8; 32];
723 let standard = super::derive_key("123123", &salt, 8192, 8, 1, 32).expect("standard");
724 let compat = super::derive_key_compat("123123", &salt, 8192, 8, 1, 32).expect("compat");
725 assert_eq!(compat, standard);
726 }
727}