diff --git a/Cargo.toml b/Cargo.toml index 822a7271..67b18958 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ starknet-contract = { version = "0.16.0-rc.1", path = "./starknet-contract" } starknet-signers = { version = "0.14.0-rc.1", path = "./starknet-signers" } starknet-accounts = { version = "0.16.0-rc.1", path = "./starknet-accounts" } starknet-macros = { version = "0.2.5-rc.1", path = "./starknet-macros" } +secrecy = "0.10.3" + [dev-dependencies] serde_json = "1.0.74" diff --git a/examples/declare_cairo1_contract.rs b/examples/declare_cairo1_contract.rs index 615611f3..3d1f63c7 100644 --- a/examples/declare_cairo1_contract.rs +++ b/examples/declare_cairo1_contract.rs @@ -1,5 +1,5 @@ use std::sync::Arc; - +use secrecy::SecretString; use starknet::{ accounts::{Account, ExecutionEncoding, SingleOwnerAccount}, core::{ @@ -27,9 +27,9 @@ async fn main() { Url::parse("https://starknet-sepolia.public.blastapi.io/rpc/v0_9").unwrap(), )); - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("YOUR_PRIVATE_KEY_IN_HEX_HERE").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("YOUR_PRIVATE_KEY_IN_HEX_HERE".into()), + ).unwrap()); let address = Felt::from_hex("YOUR_ACCOUNT_CONTRACT_ADDRESS_IN_HEX_HERE").unwrap(); let account = SingleOwnerAccount::new( diff --git a/examples/deploy_argent_account.rs b/examples/deploy_argent_account.rs index c45b2e9b..a0eabd8f 100644 --- a/examples/deploy_argent_account.rs +++ b/examples/deploy_argent_account.rs @@ -1,3 +1,4 @@ +use secrecy::SecretString; use starknet::{ accounts::{AccountFactory, ArgentAccountFactory}, core::{chain_id, types::Felt}, @@ -21,9 +22,9 @@ async fn main() { Url::parse("https://starknet-sepolia.public.blastapi.io/rpc/v0_9").unwrap(), )); - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("YOUR_PRIVATE_KEY_IN_HEX_HERE").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("YOUR_PRIVATE_KEY_IN_HEX_HERE".into()), + ).unwrap()); let factory = ArgentAccountFactory::new(class_hash, chain_id::SEPOLIA, None, signer, provider) .await diff --git a/examples/deploy_contract.rs b/examples/deploy_contract.rs index 369abb5c..a1eee2fa 100644 --- a/examples/deploy_contract.rs +++ b/examples/deploy_contract.rs @@ -1,5 +1,5 @@ use std::sync::Arc; - +use secrecy::SecretString; use starknet::{ accounts::{ExecutionEncoding, SingleOwnerAccount}, contract::ContractFactory, @@ -27,9 +27,9 @@ async fn main() { Url::parse("https://starknet-sepolia.public.blastapi.io/rpc/v0_9").unwrap(), )); - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("YOUR_PRIVATE_KEY_IN_HEX_HERE").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("YOUR_PRIVATE_KEY_IN_HEX_HERE".into()), + ).unwrap()); let address = Felt::from_hex("YOUR_ACCOUNT_CONTRACT_ADDRESS_IN_HEX_HERE").unwrap(); let account = SingleOwnerAccount::new( provider, diff --git a/examples/mint_tokens.rs b/examples/mint_tokens.rs index 0b154281..1897e654 100644 --- a/examples/mint_tokens.rs +++ b/examples/mint_tokens.rs @@ -1,3 +1,4 @@ +use secrecy::SecretString; use starknet::{ accounts::{Account, ExecutionEncoding, SingleOwnerAccount}, core::{ @@ -18,9 +19,9 @@ async fn main() { Url::parse("https://starknet-sepolia.public.blastapi.io/rpc/v0_9").unwrap(), )); - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("YOUR_PRIVATE_KEY_IN_HEX_HERE").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("YOUR_PRIVATE_KEY_IN_HEX_HERE".into()), + ).unwrap()); let address = Felt::from_hex("YOUR_ACCOUNT_CONTRACT_ADDRESS_IN_HEX_HERE").unwrap(); let tst_token_address = Felt::from_hex("07394cbe418daa16e42b87ba67372d4ab4a5df0b05c6e554d158458ce245bc10").unwrap(); diff --git a/starknet-accounts/Cargo.toml b/starknet-accounts/Cargo.toml index 0455419d..231a76df 100644 --- a/starknet-accounts/Cargo.toml +++ b/starknet-accounts/Cargo.toml @@ -21,6 +21,7 @@ starknet-signers = { version = "0.14.0-rc.1", path = "../starknet-signers" } async-trait = "0.1.68" auto_impl = "1.0.1" thiserror = "1.0.40" +secrecy = "0.10.3" [dev-dependencies] serde = { version = "1.0.160", features = ["derive"] } diff --git a/starknet-accounts/tests/single_owner_account.rs b/starknet-accounts/tests/single_owner_account.rs index 83a130e3..f89b1c04 100644 --- a/starknet-accounts/tests/single_owner_account.rs +++ b/starknet-accounts/tests/single_owner_account.rs @@ -11,6 +11,7 @@ use starknet_providers::{ }; use starknet_signers::{LocalWallet, SigningKey}; use std::sync::Arc; +use secrecy::SecretString; /// Cairo short string encoding for `SN_SEPOLIA`. const CHAIN_ID: Felt = Felt::from_raw([ @@ -107,9 +108,9 @@ async fn can_declare_cairo1_contract_v3_with_jsonrpc() { } async fn can_get_nonce_inner(provider: P, address: &str) { - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let account = @@ -119,9 +120,9 @@ async fn can_get_nonce_inner(provider: P, address: &s } async fn can_estimate_invoke_v3_fee_inner(provider: P, address: &str) { - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let eth_token_address = Felt::from_hex("049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap(); @@ -146,9 +147,9 @@ async fn can_parse_fee_estimation_error_inner( provider: P, address: &str, ) { - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let eth_token_address = Felt::from_hex("049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap(); @@ -193,9 +194,9 @@ async fn can_execute_eth_transfer_invoke_v3_inner( // - change to use a local testing network similar to Ganacha for Ethereum; or // - poll the transaction hash until it's processed. - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let eth_token_address = Felt::from_hex("049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap(); @@ -224,9 +225,9 @@ async fn can_execute_eth_transfer_invoke_v3_with_manual_gas_inner(provider: compiled_class_hash: String, } - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let account = SingleOwnerAccount::new(provider, signer, address, CHAIN_ID, ExecutionEncoding::New); @@ -313,9 +314,9 @@ async fn can_declare_cairo1_contract_v3_inner( compiled_class_hash: String, } - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from(SigningKey::from_secret( + SecretString::new("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()), + ).unwrap()); let address = Felt::from_hex(address).unwrap(); let account = SingleOwnerAccount::new(provider, signer, address, CHAIN_ID, ExecutionEncoding::New); diff --git a/starknet-contract/Cargo.toml b/starknet-contract/Cargo.toml index 0c3759ed..465e0195 100644 --- a/starknet-contract/Cargo.toml +++ b/starknet-contract/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" serde_with = "3.9.0" thiserror = "1.0.40" +secrecy = "0.10.3" [dev-dependencies] rand = { version = "0.8.5", features=["std_rng"] } diff --git a/starknet-contract/tests/contract_deployment.rs b/starknet-contract/tests/contract_deployment.rs index 59226aef..676eea77 100644 --- a/starknet-contract/tests/contract_deployment.rs +++ b/starknet-contract/tests/contract_deployment.rs @@ -1,4 +1,5 @@ use rand::{rngs::StdRng, RngCore, SeedableRng}; +use secrecy::SecretString; use starknet_accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet_contract::ContractFactory; use starknet_core::types::{contract::legacy::LegacyContractClass, Felt}; @@ -19,9 +20,12 @@ async fn can_deploy_contract_to_alpha_sepolia_with_invoke_v3() { let rpc_url = std::env::var("STARKNET_RPC") .unwrap_or_else(|_| "https://pathfinder.rpc.sepolia.starknet.rs/rpc/v0_9".into()); let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(&rpc_url).unwrap())); - let signer = LocalWallet::from(SigningKey::from_secret_scalar( - Felt::from_hex("00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), - )); + let signer = LocalWallet::from( + SigningKey::from_secret(SecretString::new( + "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into(), + )) + .unwrap(), + ); let address = Felt::from_hex("0x034dd51aa591d174b60d1cb45e46dfcae47946fae1c5e62933bbf48effedde4d") .unwrap(); diff --git a/starknet-core/Cargo.toml b/starknet-core/Cargo.toml index f2b84e8b..41bf8f53 100644 --- a/starknet-core/Cargo.toml +++ b/starknet-core/Cargo.toml @@ -31,7 +31,7 @@ serde_json = { version = "1.0.96", default-features = false, features = ["alloc" serde_json_pythonic = { version = "0.1.2", default-features = false, features = ["alloc", "raw_value"] } serde_with = { version = "3.9.0", default-features = false, features = ["alloc", "macros"] } sha3 = { version = "0.10.7", default-features = false } -starknet-types-core = { version = "0.1.7", default-features = false, features = ["curve", "serde", "num-traits"] } +starknet-types-core = { version = "0.1.8", default-features = false, features = ["curve", "serde", "num-traits", "zeroize"] } [dev-dependencies] bincode = "1.3.3" diff --git a/starknet-crypto/Cargo.toml b/starknet-crypto/Cargo.toml index 67d501fe..39f5ae96 100644 --- a/starknet-crypto/Cargo.toml +++ b/starknet-crypto/Cargo.toml @@ -24,7 +24,7 @@ rfc6979 = { version = "0.4.0", default-features = false } sha2 = { version = "0.10.6", default-features = false } zeroize = { version = "1.6.0", default-features = false } hex = { version = "0.4.3", default-features = false, optional = true } -starknet-types-core = { version = "0.1.7", default-features = false, features = ["curve", "hash"] } +starknet-types-core = { version = "0.1.8", default-features = false, features = ["curve", "hash"] } [features] default = ["std", "signature-display"] @@ -39,7 +39,7 @@ hex = "0.4.3" hex-literal = "0.4.1" serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -starknet-types-core = { version = "0.1.7", default-features = false, features = ["alloc"] } +starknet-types-core = { version = "0.1.8", default-features = false, features = ["alloc"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.50" diff --git a/starknet-curve/Cargo.toml b/starknet-curve/Cargo.toml index 9df8b930..521ae397 100644 --- a/starknet-curve/Cargo.toml +++ b/starknet-curve/Cargo.toml @@ -13,7 +13,7 @@ Stark curve keywords = ["ethereum", "starknet", "web3", "no_std"] [dependencies] -starknet-types-core = { version = "0.1.7", default-features = false, features = ["curve"] } +starknet-types-core = { version = "0.1.8", default-features = false, features = ["curve"] } [lints] workspace = true diff --git a/starknet-signers/Cargo.toml b/starknet-signers/Cargo.toml index 21e736ae..a3075edf 100644 --- a/starknet-signers/Cargo.toml +++ b/starknet-signers/Cargo.toml @@ -23,6 +23,13 @@ rand = { version = "0.8.5", features = ["std_rng"] } coins-bip32 = { version = "0.11.1", optional = true } coins-ledger = { version = "0.12.0", default-features = false, optional = true } semver = { version = "1.0.23", optional = true } +zeroize = { version = "1.8.1", features = ["derive", "zeroize_derive"] } +hex = "0.4.3" +secrecy = "0.10.3" + +[dev-dependencies] +tempdir = "0.3.7" + [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] eth-keystore = { version = "0.5.0", default-features = false } diff --git a/starknet-signers/src/key_pair.rs b/starknet-signers/src/key_pair.rs index e89f52da..add93131 100644 --- a/starknet-signers/src/key_pair.rs +++ b/starknet-signers/src/key_pair.rs @@ -1,15 +1,19 @@ use crypto_bigint::{Encoding, NonZero, U256}; +use hex; use rand::{rngs::StdRng, Rng, SeedableRng}; +use secrecy::{ExposeSecret, SecretString}; +use starknet_core::types::FromStrError; use starknet_core::{ crypto::{ecdsa_sign, ecdsa_verify, EcdsaSignError, EcdsaVerifyError, Signature}, types::Felt, }; use starknet_crypto::get_public_key; +use zeroize::{Zeroize, ZeroizeOnDrop}; /// A ECDSA signing (private) key on the STARK curve. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)] pub struct SigningKey { - secret_scalar: Felt, + secret_scalar: Box, } /// A ECDSA verifying (public) key on the STARK curve. @@ -48,14 +52,22 @@ impl SigningKey { let secret_scalar = random_u256.rem(&PRIME); // It's safe to unwrap here as we're 100% sure it's not out of range - let secret_scalar = Felt::from_bytes_be_slice(&secret_scalar.to_be_bytes()); + let mut secret_scalar = Felt::from_bytes_be_slice(&secret_scalar.to_be_bytes()); + + let result = Self { + secret_scalar: Box::new(secret_scalar), + }; - Self { secret_scalar } + secret_scalar.zeroize(); + + result } /// Constructs [`SigningKey`] directly from a secret scalar. - pub const fn from_secret_scalar(secret_scalar: Felt) -> Self { - Self { secret_scalar } + pub fn from_secret(secret_value: SecretString) -> Result { + Ok(Self { + secret_scalar: Box::new(Felt::from_hex(secret_value.expose_secret())?), + }) } /// Loads the private key from a Web3 Secret Storage Definition keystore. @@ -65,8 +77,9 @@ impl SigningKey { P: AsRef, { let key = eth_keystore::decrypt_key(path, password).map_err(KeystoreError::Inner)?; - let secret_scalar = Felt::from_bytes_be_slice(&key); - Ok(Self::from_secret_scalar(secret_scalar)) + + Ok(Self::from_secret(SecretString::from(hex::encode(key))) + .map_err(|_| KeystoreError::InvalidScalar)?) } /// Encrypts and saves the private key to a Web3 Secret Storage Definition JSON file. @@ -100,8 +113,8 @@ impl SigningKey { } /// Gets the secret scalar in the signing key. - pub const fn secret_scalar(&self) -> Felt { - self.secret_scalar + pub const fn secret_scalar(&self) -> &Felt { + &self.secret_scalar } /// Derives the verifying (public) key that corresponds to the signing key. @@ -136,32 +149,34 @@ impl VerifyingKey { #[cfg(test)] mod tests { use super::*; + use tempdir::TempDir; #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] fn test_get_secret_scalar() { // Generated with `cairo-lang` - let private_key = - Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79") - .unwrap(); + let private_key = "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79"; - let signing_key = SigningKey::from_secret_scalar(private_key); + let signing_key = SigningKey::from_secret(SecretString::from(private_key)).unwrap(); - assert_eq!(signing_key.secret_scalar(), private_key); + assert_eq!( + *signing_key.secret_scalar(), + Felt::from_hex_unchecked(private_key) + ); } #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] fn test_get_verifying_key() { // Generated with `cairo-lang` - let private_key = - Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79") - .unwrap(); + let private_key = SecretString::new( + "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79".into(), + ); let expected_public_key = Felt::from_hex("02c5dbad71c92a45cc4b40573ae661f8147869a91d57b8d9b8f48c8af7f83159") .unwrap(); - let signing_key = SigningKey::from_secret_scalar(private_key); + let signing_key = SigningKey::from_secret(private_key).unwrap(); let verifying_key = signing_key.verifying_key(); assert_eq!(verifying_key.scalar(), expected_public_key); @@ -171,9 +186,9 @@ mod tests { #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] fn test_sign() { // Generated with `cairo-lang` - let private_key = - Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79") - .unwrap(); + let private_key = SecretString::new( + "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79".into(), + ); let hash = Felt::from_hex("06fea80189363a786037ed3e7ba546dad0ef7de49fccae0e31eb658b7dd4ea76") .unwrap(); @@ -184,7 +199,7 @@ mod tests { Felt::from_hex("04e44e759cea02c23568bb4d8a09929bbca8768ab68270d50c18d214166ccd9a") .unwrap(); - let signing_key = SigningKey::from_secret_scalar(private_key); + let signing_key = SigningKey::from_secret(private_key).unwrap(); let signature = signing_key.sign(&hash).unwrap(); assert_eq!(signature.r, expected_r); @@ -194,14 +209,14 @@ mod tests { #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] fn test_hash_out_of_range() { - let private_key = - Felt::from_hex("0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79") - .unwrap(); + let private_key = SecretString::new( + "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79".into(), + ); let hash = Felt::from_hex("0800000000000000000000000000000000000000000000000000000000000000") .unwrap(); - let signing_key = SigningKey::from_secret_scalar(private_key); + let signing_key = SigningKey::from_secret(private_key).unwrap(); match signing_key.sign(&hash) { Err(EcdsaSignError::MessageHashOutOfRange) => {} @@ -248,4 +263,43 @@ mod tests { assert!(!verifying_key.verify(&hash, &Signature { r, s }).unwrap()); } + + #[test] + fn test_zeroize_signing_key() { + let private_key = SecretString::new( + "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79".into(), + ); + let mut signing_key = SigningKey::from_secret(private_key).unwrap(); + signing_key.zeroize(); + + let ptr = signing_key.secret_scalar.as_ref() as *const Felt as *const u8; + let after_zeroize = unsafe { std::slice::from_raw_parts(ptr, size_of::()) }; + assert_eq!(after_zeroize, vec![0; 32]); + } + + #[test] + fn test_keystore() { + let signing_key = SigningKey::from_secret(SecretString::new( + "0139fe4d6f02e666e86a6f58e65060f115cd3c185bd9e98bd829636931458f79".into(), + )) + .unwrap(); + + let temp_dir = TempDir::new("temp_folder").unwrap(); + let mut keystore_path = temp_dir.into_path(); + + keystore_path.push("keystore"); + + let password = "1234"; + signing_key + .save_as_keystore(&keystore_path, password) + .unwrap(); + + let signing_key_from_keystore = + SigningKey::from_keystore(&keystore_path, password).unwrap(); + + assert_eq!( + signing_key.secret_scalar, + signing_key_from_keystore.secret_scalar + ); + } }