From 43a99d8c92286b2e3cfd04fafecf8131b2297005 Mon Sep 17 00:00:00 2001 From: Andrei Vasilescu Date: Wed, 14 Aug 2024 20:19:29 +0300 Subject: [PATCH 1/2] keystore signing done --- Cargo.lock | 100 +++++++++++++++++++++++++++---- sdk/core/Cargo.toml | 3 + sdk/core/src/data/keystore.rs | 35 +++++++++++ sdk/core/src/data/mod.rs | 1 + sdk/core/src/wallet.rs | 110 ++++++++++++++++++++++++++++++++-- 5 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 sdk/core/src/data/keystore.rs diff --git a/Cargo.lock b/Cargo.lock index 08f2acc6ae..f0558fc8b1 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -197,15 +208,15 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64" -version = "0.22.1" +name = "base64ct" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-features" @@ -435,6 +446,16 @@ dependencies = [ "multiversx-sc-meta-lib", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.11" @@ -670,6 +691,15 @@ dependencies = [ "multiversx-sc-meta-lib", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1527,6 +1557,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "interact" version = "0.0.0" @@ -1994,7 +2033,7 @@ dependencies = [ name = "multiversx-sc-meta" version = "0.52.3" dependencies = [ - "base64 0.13.1", + "bip39", "clap", "colored", "common-path", @@ -2003,7 +2042,6 @@ dependencies = [ "multiversx-sc", "multiversx-sc-meta-lib", "multiversx-sc-snippets", - "multiversx-sdk", "pathdiff", "reqwest", "ruplacer", @@ -2045,7 +2083,7 @@ dependencies = [ name = "multiversx-sc-scenario" version = "0.52.3" dependencies = [ - "base64 0.22.1", + "base64", "bech32", "colored", "hex", @@ -2069,7 +2107,7 @@ dependencies = [ name = "multiversx-sc-snippets" version = "0.52.3" dependencies = [ - "base64 0.22.1", + "base64", "env_logger", "futures", "hex", @@ -2092,10 +2130,12 @@ dependencies = [ name = "multiversx-sdk" version = "0.5.0" dependencies = [ + "aes", "anyhow", - "base64 0.22.1", + "base64", "bech32", "bip39", + "ctr", "hex", "hmac", "itertools", @@ -2104,6 +2144,7 @@ dependencies = [ "pem", "rand 0.8.5", "reqwest", + "scrypt", "serde", "serde_json", "serde_repr", @@ -2390,6 +2431,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -2423,6 +2475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", + "hmac", ] [[package]] @@ -2431,7 +2484,7 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] @@ -2763,7 +2816,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", @@ -2933,7 +2986,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.1", + "base64", "rustls-pki-types", ] @@ -2960,6 +3013,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3000,6 +3062,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2 0.10.8", +] + [[package]] name = "second-contract" version = "0.0.0" diff --git a/sdk/core/Cargo.toml b/sdk/core/Cargo.toml index 463b4ce130..32e170618f 100644 --- a/sdk/core/Cargo.toml +++ b/sdk/core/Cargo.toml @@ -35,3 +35,6 @@ bech32 = "0.9" itertools = "0.13.0" pem = "3.0.2" log = "0.4.17" +scrypt = "0.11" +aes = "0.8" +ctr = "0.9.2" \ No newline at end of file diff --git a/sdk/core/src/data/keystore.rs b/sdk/core/src/data/keystore.rs new file mode 100644 index 0000000000..fd3a618ce7 --- /dev/null +++ b/sdk/core/src/data/keystore.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct CryptoParams { + pub iv: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KdfParams { + pub dklen: u32, + pub salt: String, + pub n: u32, + pub r: u32, + pub p: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Crypto { + pub ciphertext: String, + pub cipherparams: CryptoParams, + pub cipher: String, + pub kdf: String, + pub kdfparams: KdfParams, + pub mac: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Keystore { + pub version: u32, + pub kind: String, + pub id: String, + pub address: String, + pub bech32: String, + pub crypto: Crypto, +} \ No newline at end of file diff --git a/sdk/core/src/data/mod.rs b/sdk/core/src/data/mod.rs index 797ec6e1ce..9a270211df 100644 --- a/sdk/core/src/data/mod.rs +++ b/sdk/core/src/data/mod.rs @@ -3,6 +3,7 @@ pub mod account_storage; pub mod address; pub mod esdt; pub mod hyperblock; +pub mod keystore; pub mod network_config; pub mod network_economics; pub mod network_status; diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index ab1b0a2f37..0519c0413d 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -1,11 +1,19 @@ extern crate rand; +use std::{ + fs::{self}, + io::{self, Read}, +}; + +use aes::{cipher::KeyIvInit, Aes128}; use anyhow::Result; use bip39::{Language, Mnemonic}; +use ctr::{cipher::StreamCipher, Ctr128BE}; use hmac::{Hmac, Mac}; use pbkdf2::pbkdf2; +use scrypt::{scrypt, Params}; use serde_json::json; -use sha2::{Digest, Sha512}; +use sha2::{Digest, Sha256, Sha512}; use sha3::Keccak256; use zeroize::Zeroize; @@ -14,19 +22,36 @@ use crate::{ private_key::{PrivateKey, PRIVATE_KEY_LENGTH}, public_key::PublicKey, }, - data::{address::Address, transaction::Transaction}, + data::{address::Address, keystore::Keystore, transaction::Transaction}, }; const EGLD_COIN_TYPE: u32 = 508; const HARDENED: u32 = 0x80000000; +const CIPHER_ALGORITHM_AES_128_CTR: &str = "aes-128-ctr"; +const KDF_SCRYPT: &str = "scrypt"; -type HmacSha521 = Hmac; +type HmacSha512 = Hmac; +type HmacSha256 = Hmac; #[derive(Copy, Clone, Debug)] pub struct Wallet { priv_key: PrivateKey, } +#[derive(Clone, Debug)] +pub struct DecryptionParams { + pub derived_key_first_half: Vec, + pub iv: Vec, + pub ciphertext: Vec, +} + +#[derive(Debug)] +pub enum WalletError { + InvalidPassword, + InvalidKdf, + InvalidCipher, +} + impl Wallet { // GenerateMnemonic will generate a new mnemonic value using the bip39 implementation pub fn generate_mnemonic() -> Mnemonic { @@ -63,7 +88,7 @@ impl Wallet { let hardened_child_padding: u8 = 0; let mut digest = - HmacSha521::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(); @@ -83,7 +108,7 @@ impl Wallet { buff.push(child_idx as u8); digest = - HmacSha521::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(); @@ -111,6 +136,15 @@ impl Wallet { Ok(Self { priv_key: pri_key }) } + pub fn from_keystore_secret(file_path: &str) -> Result { + let decyption_params = Self::validate_keystore_password(file_path).unwrap_or_else(|e| { + panic!("Error: {:?}", e); + }); + let priv_key = + PrivateKey::from_hex_str(Self::decrypt_secret_key(decyption_params).as_str())?; + Ok(Self { priv_key }) + } + pub fn address(&self) -> Address { let public_key = PublicKey::from(&self.priv_key); Address::from(&public_key) @@ -131,4 +165,70 @@ impl Wallet { self.priv_key.sign(tx_bytes) } + + pub fn validate_keystore_password(path: &str) -> Result { + println!( + "Insert password. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." + ); + let mut password = String::new(); + io::stdin().read_to_string(&mut password).unwrap(); + password = password.trim().to_string(); + + let json_body = fs::read_to_string(path).unwrap(); + let keystore: Keystore = serde_json::from_str(&json_body).unwrap(); + let ciphertext = hex::decode(&keystore.crypto.ciphertext).unwrap(); + + let cipher = &keystore.crypto.cipher; + if cipher != CIPHER_ALGORITHM_AES_128_CTR { + return Err(WalletError::InvalidCipher); + } + + let iv = hex::decode(&keystore.crypto.cipherparams.iv).unwrap(); + let salt = hex::decode(&keystore.crypto.kdfparams.salt).unwrap(); + let json_mac = hex::decode(&keystore.crypto.mac).unwrap(); + + let kdf = &keystore.crypto.kdf; + if kdf != KDF_SCRYPT { + return Err(WalletError::InvalidKdf); + } + let n = keystore.crypto.kdfparams.n as f64; + let r = keystore.crypto.kdfparams.r as u64; + let p = keystore.crypto.kdfparams.p as u64; + let dklen = keystore.crypto.kdfparams.dklen as usize; + + let params = Params::new(n.log2() as u8, r as u32, p as u32, dklen).unwrap(); + + let mut derived_key = vec![0u8; 32]; + scrypt(password.as_bytes(), &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 mut input_mac = HmacSha256::new_from_slice(&derived_key_second_half).unwrap(); + input_mac.update(&ciphertext); + let computed_mac = input_mac.finalize().into_bytes(); + + if computed_mac.as_slice() == json_mac.as_slice() { + println!("Password is correct"); + Ok(DecryptionParams { + derived_key_first_half, + iv, + ciphertext, + }) + } else { + println!("Password is incorrect"); + Err(WalletError::InvalidPassword) + } + } + + pub fn decrypt_secret_key(decryption_params: DecryptionParams) -> String { + 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(); + cipher.apply_keystream(&mut decrypted); + + hex::encode(decrypted).to_string() + } } From 0490e8122b385d6b4e6ff8d5b3ed0c60cd711b0f Mon Sep 17 00:00:00 2001 From: Andrei Vasilescu Date: Tue, 20 Aug 2024 10:40:24 +0300 Subject: [PATCH 2/2] password prompt separated --- sdk/core/src/data/keystore.rs | 15 ++++++++++- sdk/core/src/wallet.rs | 47 +++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/sdk/core/src/data/keystore.rs b/sdk/core/src/data/keystore.rs index fd3a618ce7..2564d70a2e 100644 --- a/sdk/core/src/data/keystore.rs +++ b/sdk/core/src/data/keystore.rs @@ -1,5 +1,11 @@ use serde::{Deserialize, Serialize}; +#[derive(Debug)] +pub enum WalletError { + InvalidPassword, + InvalidKdf, + InvalidCipher, +} #[derive(Debug, Serialize, Deserialize)] pub struct CryptoParams { pub iv: String, @@ -32,4 +38,11 @@ pub struct Keystore { pub address: String, pub bech32: String, pub crypto: Crypto, -} \ No newline at end of file +} + +#[derive(Clone, Debug)] +pub struct DecryptionParams { + pub derived_key_first_half: Vec, + pub iv: Vec, + pub ciphertext: Vec, +} diff --git a/sdk/core/src/wallet.rs b/sdk/core/src/wallet.rs index 0519c0413d..b2856b74da 100644 --- a/sdk/core/src/wallet.rs +++ b/sdk/core/src/wallet.rs @@ -22,7 +22,11 @@ use crate::{ private_key::{PrivateKey, PRIVATE_KEY_LENGTH}, public_key::PublicKey, }, - data::{address::Address, keystore::Keystore, transaction::Transaction}, + data::{ + address::Address, + keystore::{DecryptionParams, Keystore, WalletError}, + transaction::Transaction, + }, }; const EGLD_COIN_TYPE: u32 = 508; @@ -38,20 +42,6 @@ pub struct Wallet { priv_key: PrivateKey, } -#[derive(Clone, Debug)] -pub struct DecryptionParams { - pub derived_key_first_half: Vec, - pub iv: Vec, - pub ciphertext: Vec, -} - -#[derive(Debug)] -pub enum WalletError { - InvalidPassword, - InvalidKdf, - InvalidCipher, -} - impl Wallet { // GenerateMnemonic will generate a new mnemonic value using the bip39 implementation pub fn generate_mnemonic() -> Mnemonic { @@ -137,14 +127,27 @@ impl Wallet { } pub fn from_keystore_secret(file_path: &str) -> Result { - let decyption_params = Self::validate_keystore_password(file_path).unwrap_or_else(|e| { - panic!("Error: {:?}", e); - }); + 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())?; 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())?; + Ok(priv_key) + } + pub fn address(&self) -> Address { let public_key = PublicKey::from(&self.priv_key); Address::from(&public_key) @@ -166,14 +169,20 @@ impl Wallet { self.priv_key.sign(tx_bytes) } - pub fn validate_keystore_password(path: &str) -> Result { + fn get_keystore_password() -> String { println!( "Insert password. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done." ); let mut password = String::new(); io::stdin().read_to_string(&mut password).unwrap(); password = password.trim().to_string(); + password + } + pub fn validate_keystore_password( + path: &str, + password: String, + ) -> Result { let json_body = fs::read_to_string(path).unwrap(); let keystore: Keystore = serde_json::from_str(&json_body).unwrap(); let ciphertext = hex::decode(&keystore.crypto.ciphertext).unwrap();