Skip to main content

aelf_keystore/
lib.rs

1//! JS-compatible keystore encryption and decryption for AElf wallets.
2
3#![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/// Errors returned while encoding or decoding AElf keystores.
32#[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/// Serializable keystore compatible with `aelf-web3.js`.
51#[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/// Cryptographic payload stored in an AElf keystore file.
66#[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/// IV or nonce parameters for the selected cipher.
79#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
80pub struct CipherParams {
81    pub iv: String,
82}
83
84/// Scrypt parameters used to derive the encryption key.
85#[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/// Decrypted keystore contents.
96///
97/// Sensitive fields are zeroized on drop and redacted from `Debug` output.
98#[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/// Options for JS-compatible keystore encryption.
107#[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    /// Encrypts a wallet with the default JS-compatible keystore parameters.
138    pub fn encrypt_js(wallet: &Wallet, password: &str) -> Result<Self, KeystoreError> {
139        Self::encrypt_js_with_options(wallet, password, KeystoreEncryptOptions::default())
140    }
141
142    /// Encrypts a wallet with explicit keystore options.
143    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    /// Decrypts the keystore and returns the recovered wallet data.
202    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    /// Checks whether the provided password can unlock the keystore.
246    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, &params, &mut derived_key)
334        .map_err(|_| KeystoreError::InvalidScryptParams)?;
335    Ok(derived_key)
336}
337
338// Legacy Nethereum/C# keystores can use parameter sets rejected by RustCrypto's stricter guard.
339fn 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, &params)
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        // Keep the compatibility branch bounded and allocation-safe.
521        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}