Skip to main content

multiversx_sdk/
wallet.rs

1use core::str;
2use std::{
3    fs::{self},
4    io::{self, Write},
5    path::Path,
6};
7
8use aes::{Aes128, cipher::KeyIvInit};
9use anyhow::Result;
10use bip39::Mnemonic;
11use ctr::{Ctr128BE, cipher::StreamCipher};
12use hmac::{Hmac, KeyInit, Mac};
13use multiversx_chain_core::{std::Bech32Address, types::Address};
14use pbkdf2::pbkdf2;
15use scrypt::{Params, scrypt};
16use serde_json::json;
17use sha2::{Digest, Sha256, Sha512};
18use sha3::Keccak256;
19use zeroize::Zeroize;
20
21use crate::{
22    crypto::{
23        private_key::{PRIVATE_KEY_LENGTH, PrivateKey},
24        public_key::PublicKey,
25    },
26    data::{keystore::*, transaction::Transaction},
27    utils::*,
28};
29
30const EGLD_COIN_TYPE: u32 = 508;
31const HARDENED: u32 = 0x80000000;
32const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr";
33const KDF_SCRYPT: &str = "scrypt";
34
35type HmacSha512 = Hmac<Sha512>;
36type HmacSha256 = Hmac<Sha256>;
37
38#[derive(Copy, Clone, Debug)]
39pub struct Wallet {
40    priv_key: PrivateKey,
41}
42
43impl Wallet {
44    fn seed_from_mnemonic(mnemonic: Mnemonic, password: &str) -> [u8; 64] {
45        let mut salt = String::with_capacity(8 + password.len());
46        salt.push_str("mnemonic");
47        salt.push_str(password);
48
49        let mut seed = [0u8; 64];
50
51        let _ = pbkdf2::<Hmac<Sha512>>(
52            mnemonic.to_string().as_bytes(),
53            salt.as_bytes(),
54            2048,
55            &mut seed,
56        );
57
58        salt.zeroize();
59
60        seed
61    }
62
63    pub fn get_private_key_from_mnemonic(
64        mnemonic: Mnemonic,
65        account: u32,
66        address_index: u32,
67    ) -> PrivateKey {
68        let seed = Self::seed_from_mnemonic(mnemonic, "");
69
70        let serialized_key_len = 32;
71        let hardened_child_padding: u8 = 0;
72
73        let mut digest =
74            HmacSha512::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size");
75        digest.update(&seed);
76        let intermediary: Vec<u8> = digest.finalize().into_bytes().into_iter().collect();
77        let mut key = intermediary[..serialized_key_len].to_vec();
78        let mut chain_code = intermediary[serialized_key_len..].to_vec();
79
80        for child_idx in [
81            44 | HARDENED,
82            EGLD_COIN_TYPE | HARDENED,
83            account | HARDENED, // account
84            HARDENED,
85            address_index | HARDENED, // addressIndex
86        ] {
87            let mut buff = [vec![hardened_child_padding], key.clone()].concat();
88            buff.push((child_idx >> 24) as u8);
89            buff.push((child_idx >> 16) as u8);
90            buff.push((child_idx >> 8) as u8);
91            buff.push(child_idx as u8);
92
93            digest =
94                HmacSha512::new_from_slice(&chain_code).expect("HMAC can take key of any size");
95            digest.update(&buff);
96            let intermediary: Vec<u8> = digest.finalize().into_bytes().into_iter().collect();
97            key = intermediary[..serialized_key_len].to_vec();
98            chain_code = intermediary[serialized_key_len..].to_vec();
99        }
100
101        PrivateKey::from_bytes(key.as_slice()).unwrap()
102    }
103
104    pub fn get_wallet_keys_mnemonic(mnemonic_str: String) -> (String, String) {
105        let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', "")).unwrap();
106        let private_key = Self::get_private_key_from_mnemonic(mnemonic, 0u32, 0u32);
107        let public_key = PublicKey::from(&private_key);
108
109        let public_key_str: &str = &public_key.to_string();
110        let private_key_str: &str = &private_key.to_string();
111
112        (private_key_str.to_string(), public_key_str.to_string())
113    }
114
115    pub fn from_private_key(priv_key: &str) -> Result<Self> {
116        let priv_key = PrivateKey::from_hex_str(priv_key)?;
117        Ok(Self { priv_key })
118    }
119
120    pub fn from_pem_file<P>(file_path: P) -> Result<Self>
121    where
122        P: AsRef<Path>,
123    {
124        let contents = std::fs::read_to_string(file_path)?;
125        Self::from_pem_file_contents(contents)
126    }
127
128    pub fn from_pem_file_contents(contents: String) -> Result<Self> {
129        let x = pem::parse(contents)?;
130        let x = x.contents()[..PRIVATE_KEY_LENGTH].to_vec();
131        let priv_key_str = std::str::from_utf8(x.as_slice())?;
132        let pri_key = PrivateKey::from_hex_str(priv_key_str)?;
133        Ok(Self { priv_key: pri_key })
134    }
135
136    pub fn get_shard(&self) -> u8 {
137        let address = self.to_address();
138        let address_bytes = address.as_bytes();
139        address_bytes[address_bytes.len() - 1] % 3
140    }
141
142    pub fn get_pem_decoded_content<P: AsRef<Path>>(file: P) -> Vec<u8> {
143        let pem_content = fs::read_to_string(file).unwrap();
144        let lines: Vec<&str> = pem_content.split('\n').collect();
145        let pem_encoded_keys = format!("{}{}{}", lines[1], lines[2], lines[3]);
146        base64_decode(pem_encoded_keys)
147    }
148
149    pub fn get_wallet_keys_pem<P: AsRef<Path>>(file: P) -> (String, String) {
150        let pem_decoded_keys = Self::get_pem_decoded_content(file);
151        let (private_key, public_key) = pem_decoded_keys.split_at(pem_decoded_keys.len() / 2);
152        let private_key_str = String::from_utf8(private_key.to_vec()).unwrap();
153        let public_key_str = String::from_utf8(public_key.to_vec()).unwrap();
154
155        (private_key_str, public_key_str)
156    }
157
158    pub fn from_keystore_secret<P: AsRef<Path>>(
159        file_path: P,
160        insert_password: InsertPassword,
161    ) -> Result<Self> {
162        let decryption_params = match insert_password {
163            InsertPassword::Plaintext(password) => {
164                Self::validate_keystore_password(&file_path, password.to_string()).unwrap_or_else(
165                    |e| {
166                        panic!("Error: {:?}", e);
167                    },
168                )
169            }
170            InsertPassword::StandardInput => {
171                Self::validate_keystore_password(&file_path, Self::get_keystore_password())
172                    .unwrap_or_else(|e| {
173                        panic!("Error: {:?}", e);
174                    })
175            }
176        };
177        let priv_key = PrivateKey::from_hex_str(
178            hex::encode(Self::decrypt_secret_key(decryption_params)).as_str(),
179        )?;
180        Ok(Self { priv_key })
181    }
182
183    pub fn get_private_key_from_keystore_secret<P: AsRef<Path>>(
184        file_path: P,
185        password: &str,
186    ) -> Result<PrivateKey> {
187        let decyption_params = Self::validate_keystore_password(file_path, password.to_string())
188            .unwrap_or_else(|e| {
189                panic!("Error: {:?}", e);
190            });
191        let priv_key = PrivateKey::from_hex_str(
192            hex::encode(Self::decrypt_secret_key(decyption_params)).as_str(),
193        )?;
194        Ok(priv_key)
195    }
196
197    #[deprecated(
198        since = "0.54.0",
199        note = "Renamed to `to_address`, type changed to multiversx_chain_core::types::Address"
200    )]
201    pub fn address(&self) -> Bech32Address {
202        self.to_address().to_bech32_default()
203    }
204
205    pub fn to_address(&self) -> Address {
206        PublicKey::from(&self.priv_key).to_address()
207    }
208
209    pub fn sign_tx(&self, unsign_tx: &Transaction) -> [u8; 64] {
210        let mut unsign_tx = unsign_tx.clone();
211        unsign_tx.signature = None;
212
213        let mut tx_bytes = json!(unsign_tx).to_string().as_bytes().to_vec();
214
215        let should_sign_on_tx_hash = unsign_tx.version >= 2 && unsign_tx.options & 1 > 0;
216        if should_sign_on_tx_hash {
217            let mut h = Keccak256::new();
218            h.update(tx_bytes);
219            tx_bytes = h.finalize().to_vec();
220        }
221
222        self.priv_key.sign(tx_bytes)
223    }
224
225    pub fn sign_bytes(&self, data: Vec<u8>) -> [u8; 64] {
226        self.priv_key.sign(data)
227    }
228
229    pub fn get_keystore_password() -> String {
230        print!("Insert password: ");
231        io::stdout().flush().unwrap();
232        rpassword::read_password().unwrap()
233    }
234
235    pub fn validate_keystore_password<P: AsRef<Path>>(
236        path: P,
237        password: String,
238    ) -> Result<DecryptionParams, WalletError> {
239        let json_body = fs::read_to_string(path).unwrap();
240        let keystore: Keystore = serde_json::from_str(&json_body).unwrap();
241        let ciphertext = hex::decode(&keystore.crypto.ciphertext).unwrap();
242
243        let cipher = &keystore.crypto.cipher;
244        if cipher != CIPHER_ALGORITHM_AES_128_CTR {
245            return Err(WalletError::InvalidCipher);
246        }
247
248        let iv = hex::decode(&keystore.crypto.cipherparams.iv).unwrap();
249        let salt = hex::decode(&keystore.crypto.kdfparams.salt).unwrap();
250        let json_mac = hex::decode(&keystore.crypto.mac).unwrap();
251
252        let kdf = &keystore.crypto.kdf;
253        if kdf != KDF_SCRYPT {
254            return Err(WalletError::InvalidKdf);
255        }
256        let n = keystore.crypto.kdfparams.n as f64;
257        let r = keystore.crypto.kdfparams.r as u64;
258        let p = keystore.crypto.kdfparams.p as u64;
259        let _dklen = keystore.crypto.kdfparams.dklen as usize;
260
261        let params = Params::new(n.log2() as u8, r as u32, p as u32).unwrap();
262
263        let mut derived_key = vec![0u8; 32];
264        scrypt(password.as_bytes(), &salt, &params, &mut derived_key).unwrap();
265
266        let derived_key_first_half = derived_key[0..16].to_vec();
267        let derived_key_second_half = derived_key[16..32].to_vec();
268
269        let mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap();
270        input_mac.update(&ciphertext);
271        let computed_mac = input_mac.finalize().into_bytes();
272
273        if computed_mac.to_vec() == json_mac {
274            println!("Password is correct");
275            Ok(DecryptionParams {
276                derived_key_first_half,
277                iv,
278                data: ciphertext,
279            })
280        } else {
281            println!("Password is incorrect");
282            Err(WalletError::InvalidPassword)
283        }
284    }
285
286    pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> Vec<u8> {
287        let key: &[u8; 16] = decryption_params
288            .derived_key_first_half
289            .as_slice()
290            .try_into()
291            .unwrap();
292        let iv: &[u8; 16] = decryption_params.iv.as_slice().try_into().unwrap();
293        let mut cipher = Ctr128BE::<Aes128>::new(key.into(), iv.into());
294        let mut decrypted = decryption_params.data.to_vec();
295        cipher.apply_keystream(&mut decrypted);
296
297        decrypted
298    }
299
300    /// Not available in dapps, since it uses randomness to generate the keystore.
301    ///
302    /// Only available in the sc-meta standalone CLI.
303    #[cfg(feature = "wallet-full")]
304    pub fn encrypt_keystore(
305        data: &[u8],
306        hrp: &str,
307        address: &Address,
308        public_key: &str,
309        password: &str,
310    ) -> String {
311        use rand::Rng;
312
313        let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P).unwrap();
314        let mut rand_salt: [u8; 32] = [0u8; 32];
315        rand::rng().fill_bytes(&mut rand_salt);
316        let salt_hex = hex::encode(rand_salt);
317
318        let mut rand_iv: [u8; 16] = [0u8; 16];
319        rand::rng().fill_bytes(&mut rand_iv);
320        let iv_hex = hex::encode(rand_iv);
321
322        let mut derived_key = vec![0u8; 32];
323        scrypt(password.as_bytes(), &rand_salt, &params, &mut derived_key).unwrap();
324
325        let derived_key_first_half = derived_key[0..16].to_vec();
326        let derived_key_second_half = derived_key[16..32].to_vec();
327
328        let decryption_params = DecryptionParams {
329            derived_key_first_half,
330            iv: rand_iv.to_vec(),
331            data: data.to_vec(),
332        };
333
334        let ciphertext = Self::decrypt_secret_key(decryption_params);
335
336        let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap();
337        h.update(&ciphertext);
338        let mac = h.finalize().into_bytes();
339        let keystore = Keystore {
340            crypto: Crypto {
341                cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(),
342                cipherparams: CryptoParams { iv: iv_hex },
343                ciphertext: hex::encode(&ciphertext),
344                kdf: KDF_SCRYPT.to_string(),
345                kdfparams: KdfParams {
346                    salt: salt_hex,
347                    n: KDF_N,
348                    r: KDF_R,
349                    p: KDF_P,
350                    dklen: KDF_DKLEN as u32,
351                },
352                mac: hex::encode(mac),
353            },
354            id: uuid::Uuid::new_v4().to_string(),
355            version: KEYSTORE_VERSION,
356            kind: "secretKey".to_string(),
357            address: public_key.to_string(),
358            bech32: address.to_bech32(hrp).bech32,
359        };
360
361        let mut keystore_json: String = serde_json::to_string_pretty(&keystore).unwrap();
362        keystore_json.push('\n');
363        keystore_json
364    }
365
366    pub fn generate_pem_content(
367        hrp: &str,
368        address: &Address,
369        private_key: &str,
370        public_key: &str,
371    ) -> String {
372        let concat_keys = format!("{}{}", private_key, public_key);
373        let concat_keys_b64 = base64_encode(concat_keys);
374
375        // Split the base64 string into 64-character lines
376        let formatted_key = concat_keys_b64
377            .as_bytes()
378            .chunks(64)
379            .map(|chunk| std::str::from_utf8(chunk).unwrap())
380            .collect::<Vec<&str>>()
381            .join("\n");
382
383        let address_bech32 = Bech32Address::encode_address(hrp, address.clone());
384        let pem_content = format!(
385            "-----BEGIN PRIVATE KEY for {address_bech32}-----\n{formatted_key}\n-----END PRIVATE KEY for {address_bech32}-----\n"
386        );
387
388        pem_content
389    }
390}