diff --git a/Cargo.lock b/Cargo.lock index f0558fc8b1..fecbf652fe 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2151,6 +2151,7 @@ dependencies = [ "sha2 0.10.8", "sha3", "tokio", + "uuid", "zeroize", ] @@ -3724,6 +3725,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "vault" version = "0.0.0" diff --git a/framework/meta/src/cmd/wallet.rs b/framework/meta/src/cmd/wallet.rs index 7fcf28899a..ba8a9f6132 100644 --- a/framework/meta/src/cmd/wallet.rs +++ b/framework/meta/src/cmd/wallet.rs @@ -1,16 +1,16 @@ use core::str; -use multiversx_sc::types; -use std::{ - fs::{self, File}, - io::{self, Read, Write}, -}; use crate::cli::{WalletAction, WalletArgs, WalletBech32Args, WalletConvertArgs, WalletNewArgs}; -use bip39::Mnemonic; +use multiversx_sc::types::{self}; use multiversx_sc_snippets::sdk::{ crypto::public_key::PublicKey, data::address::Address, utils::base64_encode, wallet::Wallet, }; use multiversx_sc_snippets::{hex, imports::Bech32Address}; +use std::{ + fs::{self, File}, + io::{self, Read, Write}, +}; + pub fn wallet(args: &WalletArgs) { let command = &args.command; match command { @@ -27,45 +27,92 @@ fn convert(convert_args: &WalletConvertArgs) { let out_format = &convert_args.to; let mut mnemonic_str = String::new(); + let private_key_str: String; + let public_key_str: String; match (in_format.as_str(), out_format.as_str()) { ("mnemonic", "pem") => match infile { Some(file) => { mnemonic_str = fs::read_to_string(file).unwrap(); - mnemonic_str = mnemonic_str.replace('\n', ""); + (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic_str); + write_resulted_pem(&public_key_str, &private_key_str, outfile); }, None => { println!("Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done."); - _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap() + _ = io::stdin().read_to_string(&mut mnemonic_str).unwrap(); + (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic_str); + write_resulted_pem(&public_key_str, &private_key_str, outfile); + }, + }, + ("keystore-secret", "pem") => match infile { + Some(file) => { + let private_key = Wallet::get_private_key_from_keystore_secret( + file, + &Wallet::get_keystore_password(), + ) + .unwrap(); + private_key_str = private_key.to_string(); + let public_key = PublicKey::from(&private_key); + public_key_str = public_key.to_string(); + write_resulted_pem(&public_key_str, &private_key_str, outfile); + }, + None => { + panic!("Input file is required for keystore-secret format"); + }, + }, + ("pem", "keystore-secret") => match infile { + Some(file) => { + let pem_decoded_keys = Wallet::get_pem_decoded_content(file); + (private_key_str, public_key_str) = Wallet::get_wallet_keys_pem(file); + + let address = get_wallet_address(&private_key_str); + let hex_decoded_keys = hex::decode(pem_decoded_keys).unwrap(); + + let json_result = Wallet::encrypt_keystore( + hex_decoded_keys.as_slice(), + &address, + &public_key_str, + &Wallet::get_keystore_password(), + ); + write_resulted_keystore(json_result, outfile); + }, + None => { + panic!("Input file is required for pem format"); }, }, _ => { println!("Unsupported conversion"); }, } +} - let mnemonic = Mnemonic::parse(mnemonic_str).unwrap(); - - let (private_key_str, public_key_str) = get_wallet_keys(mnemonic); - let address = get_wallet_address(private_key_str.as_str()); - +fn write_resulted_pem(public_key: &str, private_key: &str, outfile: Option<&String>) { + let address = get_wallet_address(private_key); match outfile { Some(outfile) => { - generate_pem( - &address, - private_key_str.as_str(), - public_key_str.as_str(), - outfile, - ); + let pem_content = generate_pem_content(&address, private_key, public_key); + let mut file = File::create(outfile).unwrap(); + file.write_all(pem_content.as_bytes()).unwrap(); }, None => { - let pem_content = - generate_pem_content(&address, private_key_str.as_str(), public_key_str.as_str()); + let pem_content = generate_pem_content(&address, private_key, public_key); print!("{}", pem_content); }, } } +fn write_resulted_keystore(json_result: String, outfile: Option<&String>) { + match outfile { + Some(outfile) => { + let mut file = File::create(outfile).unwrap(); + file.write_all(json_result.as_bytes()).unwrap(); + }, + None => { + println!("{}", json_result); + }, + } +} + fn bech32_conversion(bech32_args: &WalletBech32Args) { let encode_address = bech32_args.hex_address.as_ref(); let decode_address = bech32_args.bech32_address.as_ref(); @@ -90,57 +137,45 @@ fn bech32_conversion(bech32_args: &WalletBech32Args) { } } -fn get_wallet_keys(mnemonic: Mnemonic) -> (String, String) { - let private_key = Wallet::get_private_key_from_mnemonic(mnemonic, 0u32, 0u32); - let public_key = PublicKey::from(&private_key); - - let public_key_str: &str = &public_key.to_string(); - let private_key_str: &str = &private_key.to_string(); - - (private_key_str.to_string(), public_key_str.to_string()) -} - fn get_wallet_address(private_key: &str) -> Address { let wallet = Wallet::from_private_key(private_key).unwrap(); wallet.address() } fn new(new_args: &WalletNewArgs) { - let format = new_args.wallet_format.as_ref(); - let outfile = new_args.outfile.as_ref(); + let format = new_args.wallet_format.as_deref(); + let outfile = new_args.outfile.as_ref(); // Handle outfile as Option<&str> if it's an Option let mnemonic = Wallet::generate_mnemonic(); println!("Mnemonic: {}", mnemonic); - let (private_key_str, public_key_str) = get_wallet_keys(mnemonic); + let (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic.to_string()); let address = get_wallet_address(private_key_str.as_str()); println!("Wallet address: {}", address); - if let Some(f) = format { - match (f.as_str(), outfile) { - ("pem", Some(file)) => { - generate_pem( - &address, - private_key_str.as_str(), - public_key_str.as_str(), - file, - ); - }, - ("pem", None) => { - println!("Output file is required for PEM format"); - }, - _ => {}, - } + match format { + Some("pem") => { + write_resulted_pem(public_key_str.as_str(), private_key_str.as_str(), outfile); + }, + Some("keystore-secret") => { + let concatenated_keys = format!("{}{}", private_key_str, public_key_str); + let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); + let json_result = Wallet::encrypt_keystore( + hex_decoded_keys.as_slice(), + &address, + &public_key_str, + &Wallet::get_keystore_password(), + ); + write_resulted_keystore(json_result, outfile); + }, + Some(_) => { + println!("Unsupported format"); + }, + None => {}, } } -fn generate_pem(address: &Address, private_key: &str, public_key: &str, outfile: &String) { - let pem_content = generate_pem_content(address, private_key, public_key); - let mut file = File::create(outfile).unwrap(); - file.write_all(pem_content.as_bytes()).unwrap() -} - -fn generate_pem_content(address: &Address, private_key: &str, public_key: &str) -> String { +pub fn generate_pem_content(address: &Address, private_key: &str, public_key: &str) -> String { let concat_keys = format!("{}{}", private_key, public_key); let concat_keys_b64 = base64_encode(concat_keys); diff --git a/framework/meta/tests/wallet_test.rs b/framework/meta/tests/wallet_test.rs new file mode 100644 index 0000000000..ce1627ff64 --- /dev/null +++ b/framework/meta/tests/wallet_test.rs @@ -0,0 +1,91 @@ +use std::fs::{self, File}; +use std::io::Write; + +use multiversx_sc_meta::cmd::wallet::generate_pem_content; +use multiversx_sc_snippets::sdk::{crypto::public_key::PublicKey, data::address::Address}; +use multiversx_sc_snippets::{hex, imports::Wallet}; + +const ALICE_PEM_PATH: &str = "../snippets/src/test_wallets/alice.pem"; +const ALICE_KEYSTORE_PATH_TEST: &str = "alice.json"; +const ALICE_PEM_PATH_TEST: &str = "alice_test.pem"; +const KEYSTORE_PASSWORD: &str = "abcd"; + +fn create_keystore_from_pem(file: &str) { + let pem_decoded_keys = Wallet::get_pem_decoded_content(file); + let (private_key_str, public_key_str) = Wallet::get_wallet_keys_pem(file); + + let address = Wallet::from_private_key(&private_key_str) + .unwrap() + .address(); + let hex_decoded_keys = hex::decode(pem_decoded_keys).unwrap(); + + let json_result = Wallet::encrypt_keystore( + hex_decoded_keys.as_slice(), + &address, + &public_key_str, + KEYSTORE_PASSWORD, + ); + + write_to_file(&json_result, ALICE_KEYSTORE_PATH_TEST); +} + +fn write_to_file(content: &str, file: &str) { + let mut file = File::create(file).unwrap(); + file.write_all(content.as_bytes()).unwrap(); +} + +fn create_keystore_file_from_scratch() -> Address { + let mnemonic = Wallet::generate_mnemonic(); + let (private_key_str, public_key_str) = Wallet::get_wallet_keys_mnemonic(mnemonic.to_string()); + let wallet = Wallet::from_private_key(&private_key_str).unwrap(); + let address = wallet.address(); + + let concatenated_keys = format!("{}{}", private_key_str, public_key_str); + let hex_decoded_keys = hex::decode(concatenated_keys).unwrap(); + let json_result = Wallet::encrypt_keystore( + hex_decoded_keys.as_slice(), + &address, + &public_key_str, + KEYSTORE_PASSWORD, + ); + write_to_file(&json_result, ALICE_KEYSTORE_PATH_TEST); + address +} + +#[test] +fn test_wallet_convert_pem_to_keystore() { + create_keystore_from_pem(ALICE_PEM_PATH); + let (private_key_pem, _public_key_pem) = Wallet::get_wallet_keys_pem(ALICE_PEM_PATH); + assert_eq!( + Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST, KEYSTORE_PASSWORD) + .unwrap() + .to_string(), + private_key_pem + ); + fs::remove_file(ALICE_KEYSTORE_PATH_TEST).unwrap(); +} + +#[test] +fn test_wallet_convert_keystore_to_pem() { + let address = create_keystore_file_from_scratch(); + + let private_key = + Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST, KEYSTORE_PASSWORD) + .unwrap(); + let private_key_str = private_key.to_string(); + let public_key = PublicKey::from(&private_key); + let public_key_str = public_key.to_string(); + + let pem_content = generate_pem_content(&address, &private_key_str, &public_key_str); + write_to_file(&pem_content, ALICE_PEM_PATH_TEST); + + assert_eq!( + Wallet::get_private_key_from_keystore_secret(ALICE_KEYSTORE_PATH_TEST, KEYSTORE_PASSWORD) + .unwrap() + .to_string(), + Wallet::get_wallet_keys_pem(ALICE_PEM_PATH_TEST).0 + ); + + fs::remove_file(ALICE_PEM_PATH_TEST).unwrap(); + fs::remove_file(ALICE_KEYSTORE_PATH_TEST).unwrap(); +} diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 32e170618f..e84f8cd4cb 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -37,4 +37,5 @@ pem = "3.0.2" log = "0.4.17" scrypt = "0.11" aes = "0.8" -ctr = "0.9.2" \ No newline at end of file +ctr = "0.9.2" +uuid = {version = "1.10.0", features = ["v4"]} \ No newline at end of file diff --git a/sdk/core/src/data/keystore.rs b/sdk/core/src/data/keystore.rs index 2564d70a2e..1b3c656ef1 100644 --- a/sdk/core/src/data/keystore.rs +++ b/sdk/core/src/data/keystore.rs @@ -1,5 +1,11 @@ use serde::{Deserialize, Serialize}; +pub const KDF_N: u32 = 4096; +pub const KDF_R: u32 = 8; +pub const KDF_P: u32 = 1; +pub const KDF_DKLEN: usize = 32; +pub const KEYSTORE_VERSION: u32 = 4; + #[derive(Debug)] pub enum WalletError { InvalidPassword, @@ -44,5 +50,5 @@ pub struct Keystore { pub struct DecryptionParams { pub derived_key_first_half: Vec, pub iv: Vec, - pub ciphertext: Vec, + pub data: Vec, } diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index b2856b74da..bbf2c41a2c 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,5 +1,6 @@ extern crate rand; +use core::str; use std::{ fs::{self}, io::{self, Read}, @@ -11,6 +12,7 @@ use bip39::{Language, Mnemonic}; use ctr::{cipher::StreamCipher, Ctr128BE}; use hmac::{Hmac, Mac}; use pbkdf2::pbkdf2; +use rand::RngCore; use scrypt::{scrypt, Params}; use serde_json::json; use sha2::{Digest, Sha256, Sha512}; @@ -22,13 +24,12 @@ use crate::{ private_key::{PrivateKey, PRIVATE_KEY_LENGTH}, public_key::PublicKey, }, - data::{ - address::Address, - keystore::{DecryptionParams, Keystore, WalletError}, - transaction::Transaction, - }, + data::{address::Address, keystore::*, transaction::Transaction}, + utils::base64_decode, }; +use uuid::Uuid; + const EGLD_COIN_TYPE: u32 = 508; const HARDENED: u32 = 0x80000000; const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; @@ -79,6 +80,7 @@ impl Wallet { let mut digest = HmacSha512::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); + HmacSha512::new_from_slice(b"ed25519 seed").expect("HMAC can take key of any size"); digest.update(&seed); let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); let mut key = intermediary[..serialized_key_len].to_vec(); @@ -99,6 +101,7 @@ impl Wallet { digest = HmacSha512::new_from_slice(&chain_code).expect("HMAC can take key of any size"); + HmacSha512::new_from_slice(&chain_code).expect("HMAC can take key of any size"); digest.update(&buff); let intermediary: Vec = digest.finalize().into_bytes().into_iter().collect(); key = intermediary[..serialized_key_len].to_vec(); @@ -108,9 +111,20 @@ impl Wallet { PrivateKey::from_bytes(key.as_slice()).unwrap() } + pub fn get_wallet_keys_mnemonic(mnemonic_str: String) -> (String, String) { + let mnemonic = Mnemonic::parse(mnemonic_str.replace('\n', "")).unwrap(); + let private_key = Self::get_private_key_from_mnemonic(mnemonic, 0u32, 0u32); + let public_key = PublicKey::from(&private_key); + + let public_key_str: &str = &public_key.to_string(); + let private_key_str: &str = &private_key.to_string(); + + (private_key_str.to_string(), public_key_str.to_string()) + } + pub fn from_private_key(priv_key: &str) -> Result { - let pri_key = PrivateKey::from_hex_str(priv_key)?; - Ok(Self { priv_key: pri_key }) + let priv_key = PrivateKey::from_hex_str(priv_key)?; + Ok(Self { priv_key }) } pub fn from_pem_file(file_path: &str) -> Result { @@ -126,25 +140,45 @@ impl Wallet { Ok(Self { priv_key: pri_key }) } + pub fn get_pem_decoded_content(file: &str) -> Vec { + let pem_content = fs::read_to_string(file).unwrap(); + let lines: Vec<&str> = pem_content.split('\n').collect(); + let pem_encoded_keys = format!("{}{}{}", lines[1], lines[2], lines[3]); + base64_decode(pem_encoded_keys) + } + + pub fn get_wallet_keys_pem(file: &str) -> (String, String) { + let pem_decoded_keys = Self::get_pem_decoded_content(file); + let (private_key, public_key) = pem_decoded_keys.split_at(pem_decoded_keys.len() / 2); + let private_key_str = String::from_utf8(private_key.to_vec()).unwrap(); + let public_key_str = String::from_utf8(public_key.to_vec()).unwrap(); + + (private_key_str, public_key_str) + } + pub fn from_keystore_secret(file_path: &str) -> Result { let decyption_params = Self::validate_keystore_password(file_path, Self::get_keystore_password()) .unwrap_or_else(|e| { panic!("Error: {:?}", e); }); - let priv_key = - PrivateKey::from_hex_str(Self::decrypt_secret_key(decyption_params).as_str())?; + let priv_key = PrivateKey::from_hex_str( + hex::encode(Self::decrypt_secret_key(decyption_params)).as_str(), + )?; Ok(Self { priv_key }) } - pub fn get_private_key_from_keystore_secret(file_path: &str) -> Result { - let decyption_params = - Self::validate_keystore_password(file_path, Self::get_keystore_password()) - .unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }); - let priv_key = - PrivateKey::from_hex_str(Self::decrypt_secret_key(decyption_params).as_str())?; + pub fn get_private_key_from_keystore_secret( + file_path: &str, + password: &str, + ) -> Result { + let decyption_params = Self::validate_keystore_password(file_path, password.to_string()) + .unwrap_or_else(|e| { + panic!("Error: {:?}", e); + }); + let priv_key = PrivateKey::from_hex_str( + hex::encode(Self::decrypt_secret_key(decyption_params)).as_str(), + )?; Ok(priv_key) } @@ -169,7 +203,7 @@ impl Wallet { self.priv_key.sign(tx_bytes) } - fn get_keystore_password() -> String { + pub fn get_keystore_password() -> String { println!( "Insert password. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); @@ -222,7 +256,7 @@ impl Wallet { Ok(DecryptionParams { derived_key_first_half, iv, - ciphertext, + data: ciphertext, }) } else { println!("Password is incorrect"); @@ -230,14 +264,73 @@ impl Wallet { } } - pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> String { + pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> Vec { let mut cipher = Ctr128BE::::new( decryption_params.derived_key_first_half.as_slice().into(), decryption_params.iv.as_slice().into(), ); - let mut decrypted = decryption_params.ciphertext.to_vec(); + let mut decrypted = decryption_params.data.to_vec(); cipher.apply_keystream(&mut decrypted); - hex::encode(decrypted).to_string() + decrypted + } + + pub fn encrypt_keystore( + data: &[u8], + address: &Address, + public_key: &str, + password: &str, + ) -> String { + let params = Params::new((KDF_N as f64).log2() as u8, KDF_R, KDF_P, KDF_DKLEN).unwrap(); + let mut rand_salt: [u8; 32] = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut rand_salt); + let salt_hex = hex::encode(rand_salt); + + let mut rand_iv: [u8; 16] = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut rand_iv); + let iv_hex = hex::encode(rand_iv); + + let mut derived_key = vec![0u8; 32]; + scrypt(password.as_bytes(), &rand_salt, ¶ms, &mut derived_key).unwrap(); + + let derived_key_first_half = derived_key[0..16].to_vec(); + let derived_key_second_half = derived_key[16..32].to_vec(); + + let decryption_params = DecryptionParams { + derived_key_first_half, + iv: rand_iv.to_vec(), + data: data.to_vec(), + }; + + let ciphertext = Self::decrypt_secret_key(decryption_params); + + let mut h = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + h.update(&ciphertext); + let mac = h.finalize().into_bytes(); + let keystore = Keystore { + crypto: Crypto { + cipher: CIPHER_ALGORITHM_AES_128_CTR.to_string(), + cipherparams: CryptoParams { iv: iv_hex }, + ciphertext: hex::encode(&ciphertext), + kdf: KDF_SCRYPT.to_string(), + kdfparams: KdfParams { + salt: salt_hex, + n: KDF_N, + r: KDF_R, + p: KDF_P, + dklen: KDF_DKLEN as u32, + }, + mac: hex::encode(mac), + }, + id: Uuid::new_v4().to_string(), + version: KEYSTORE_VERSION, + kind: "secretKey".to_string(), + address: public_key.to_string(), + bech32: address.to_string(), + }; + + let mut keystore_json: String = serde_json::to_string_pretty(&keystore).unwrap(); + keystore_json.push('\n'); + keystore_json } }