ethers_signers/wallet/
private_key.rs

1//! Specific helper functions for loading an offline K256 Private Key stored on disk
2use super::Wallet;
3
4use crate::wallet::mnemonic::MnemonicBuilderError;
5use coins_bip32::Bip32Error;
6use coins_bip39::MnemonicError;
7#[cfg(not(target_arch = "wasm32"))]
8use elliptic_curve::rand_core;
9#[cfg(not(target_arch = "wasm32"))]
10use eth_keystore::KeystoreError;
11use ethers_core::{
12    k256::ecdsa::{self, SigningKey},
13    rand::{CryptoRng, Rng},
14    utils::secret_key_to_address,
15};
16#[cfg(not(target_arch = "wasm32"))]
17use std::path::Path;
18use std::str::FromStr;
19use thiserror::Error;
20
21#[derive(Error, Debug)]
22/// Error thrown by the Wallet module
23pub enum WalletError {
24    /// Error propagated from the BIP-32 crate
25    #[error(transparent)]
26    Bip32Error(#[from] Bip32Error),
27    /// Error propagated from the BIP-39 crate
28    #[error(transparent)]
29    Bip39Error(#[from] MnemonicError),
30    /// Underlying eth keystore error
31    #[cfg(not(target_arch = "wasm32"))]
32    #[error(transparent)]
33    EthKeystoreError(#[from] KeystoreError),
34    /// Error propagated from k256's ECDSA module
35    #[error(transparent)]
36    EcdsaError(#[from] ecdsa::Error),
37    /// Error propagated from the hex crate.
38    #[error(transparent)]
39    HexError(#[from] hex::FromHexError),
40    /// Error propagated by IO operations
41    #[error(transparent)]
42    IoError(#[from] std::io::Error),
43    /// Error propagated from the mnemonic builder module.
44    #[error(transparent)]
45    MnemonicBuilderError(#[from] MnemonicBuilderError),
46    /// Error type from Eip712Error message
47    #[error("error encoding eip712 struct: {0:?}")]
48    Eip712Error(String),
49}
50
51impl Wallet<SigningKey> {
52    /// Creates a new random encrypted JSON with the provided password and stores it in the
53    /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the
54    /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`,
55    /// the keystore is stored as the stringified UUID.
56    #[cfg(not(target_arch = "wasm32"))]
57    pub fn new_keystore<P, R, S>(
58        dir: P,
59        rng: &mut R,
60        password: S,
61        name: Option<&str>,
62    ) -> Result<(Self, String), WalletError>
63    where
64        P: AsRef<Path>,
65        R: Rng + CryptoRng + rand_core::CryptoRng,
66        S: AsRef<[u8]>,
67    {
68        let (secret, uuid) = eth_keystore::new(dir, rng, password, name)?;
69        let signer = SigningKey::from_bytes(secret.as_slice().into())?;
70        let address = secret_key_to_address(&signer);
71        Ok((Self { signer, address, chain_id: 1 }, uuid))
72    }
73
74    /// Decrypts an encrypted JSON from the provided path to construct a Wallet instance
75    #[cfg(not(target_arch = "wasm32"))]
76    pub fn decrypt_keystore<P, S>(keypath: P, password: S) -> Result<Self, WalletError>
77    where
78        P: AsRef<Path>,
79        S: AsRef<[u8]>,
80    {
81        let secret = eth_keystore::decrypt_key(keypath, password)?;
82        let signer = SigningKey::from_bytes(secret.as_slice().into())?;
83        let address = secret_key_to_address(&signer);
84        Ok(Self { signer, address, chain_id: 1 })
85    }
86
87    /// Creates a new encrypted JSON with the provided private key and password and stores it in the
88    /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the
89    /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`,
90    /// the keystore is stored as the stringified UUID.
91    #[cfg(not(target_arch = "wasm32"))]
92    pub fn encrypt_keystore<P, R, B, S>(
93        keypath: P,
94        rng: &mut R,
95        pk: B,
96        password: S,
97        name: Option<&str>,
98    ) -> Result<(Self, String), WalletError>
99    where
100        P: AsRef<Path>,
101        R: Rng + CryptoRng,
102        B: AsRef<[u8]>,
103        S: AsRef<[u8]>,
104    {
105        let uuid = eth_keystore::encrypt_key(keypath, rng, &pk, password, name)?;
106        let signer = SigningKey::from_slice(pk.as_ref())?;
107        let address = secret_key_to_address(&signer);
108        Ok((Self { signer, address, chain_id: 1 }, uuid))
109    }
110
111    /// Creates a new random keypair seeded with the provided RNG
112    pub fn new<R: Rng + CryptoRng>(rng: &mut R) -> Self {
113        let signer = SigningKey::random(rng);
114        let address = secret_key_to_address(&signer);
115        Self { signer, address, chain_id: 1 }
116    }
117
118    /// Creates a new Wallet instance from a raw scalar value (big endian).
119    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WalletError> {
120        let signer = SigningKey::from_bytes(bytes.into())?;
121        let address = secret_key_to_address(&signer);
122        Ok(Self { signer, address, chain_id: 1 })
123    }
124}
125
126impl PartialEq for Wallet<SigningKey> {
127    fn eq(&self, other: &Self) -> bool {
128        self.signer.to_bytes().eq(&other.signer.to_bytes()) &&
129            self.address == other.address &&
130            self.chain_id == other.chain_id
131    }
132}
133
134impl From<SigningKey> for Wallet<SigningKey> {
135    fn from(signer: SigningKey) -> Self {
136        let address = secret_key_to_address(&signer);
137
138        Self { signer, address, chain_id: 1 }
139    }
140}
141
142use ethers_core::k256::SecretKey as K256SecretKey;
143
144impl From<K256SecretKey> for Wallet<SigningKey> {
145    fn from(key: K256SecretKey) -> Self {
146        let signer = key.into();
147        let address = secret_key_to_address(&signer);
148
149        Self { signer, address, chain_id: 1 }
150    }
151}
152
153impl FromStr for Wallet<SigningKey> {
154    type Err = WalletError;
155
156    fn from_str(src: &str) -> Result<Self, Self::Err> {
157        let src = hex::decode(src.strip_prefix("0X").unwrap_or(src))?;
158
159        if src.len() != 32 {
160            return Err(WalletError::HexError(hex::FromHexError::InvalidStringLength))
161        }
162
163        let sk = SigningKey::from_bytes(src.as_slice().into())?;
164        Ok(sk.into())
165    }
166}
167
168impl TryFrom<&str> for Wallet<SigningKey> {
169    type Error = WalletError;
170
171    fn try_from(value: &str) -> Result<Self, Self::Error> {
172        value.parse()
173    }
174}
175
176impl TryFrom<String> for Wallet<SigningKey> {
177    type Error = WalletError;
178
179    fn try_from(value: String) -> Result<Self, Self::Error> {
180        value.parse()
181    }
182}
183
184#[cfg(test)]
185#[cfg(not(target_arch = "wasm32"))]
186mod tests {
187    use super::*;
188    use crate::{LocalWallet, Signer};
189    use ethers_core::types::Address;
190    use tempfile::tempdir;
191
192    #[test]
193    fn parse_pk() {
194        let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b";
195        let _pk: Wallet<SigningKey> = s.parse().unwrap();
196    }
197
198    #[test]
199    fn parse_short_key() {
200        let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea3";
201        assert!(s.len() < 64);
202        let pk = s.parse::<LocalWallet>().unwrap_err();
203        match pk {
204            WalletError::HexError(hex::FromHexError::InvalidStringLength) => {}
205            _ => panic!("Unexpected error"),
206        }
207    }
208
209    async fn test_encrypted_json_keystore(key: Wallet<SigningKey>, uuid: &str, dir: &Path) {
210        // sign a message using the given key
211        let message = "Some data";
212        let signature = key.sign_message(message).await.unwrap();
213
214        // read from the encrypted JSON keystore and decrypt it, while validating that the
215        // signatures produced by both the keys should match
216        let path = Path::new(dir).join(uuid);
217        let key2 = Wallet::<SigningKey>::decrypt_keystore(path.clone(), "randpsswd").unwrap();
218
219        let signature2 = key2.sign_message(message).await.unwrap();
220        assert_eq!(signature, signature2);
221
222        std::fs::remove_file(&path).unwrap();
223    }
224
225    #[tokio::test]
226    async fn encrypted_json_keystore_new() {
227        // create and store an encrypted JSON keystore in this directory
228        let dir = tempdir().unwrap();
229        let mut rng = rand::thread_rng();
230        let (key, uuid) =
231            Wallet::<SigningKey>::new_keystore(&dir, &mut rng, "randpsswd", None).unwrap();
232
233        test_encrypted_json_keystore(key, &uuid, dir.path()).await;
234    }
235
236    #[tokio::test]
237    async fn encrypted_json_keystore_from_pk() {
238        // create and store an encrypted JSON keystore in this directory
239        let dir = tempdir().unwrap();
240        let mut rng = rand::thread_rng();
241
242        let private_key =
243            hex::decode("6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b")
244                .unwrap();
245
246        let (key, uuid) =
247            Wallet::<SigningKey>::encrypt_keystore(&dir, &mut rng, private_key, "randpsswd", None)
248                .unwrap();
249
250        test_encrypted_json_keystore(key, &uuid, dir.path()).await;
251    }
252
253    #[tokio::test]
254    async fn signs_msg() {
255        let message = "Some data";
256        let hash = ethers_core::utils::hash_message(message);
257        let key = Wallet::<SigningKey>::new(&mut rand::thread_rng());
258        let address = key.address;
259
260        // sign a message
261        let signature = key.sign_message(message).await.unwrap();
262
263        // ecrecover via the message will hash internally
264        let recovered = signature.recover(message).unwrap();
265
266        // if provided with a hash, it will skip hashing
267        let recovered2 = signature.recover(hash).unwrap();
268
269        // verifies the signature is produced by `address`
270        signature.verify(message, address).unwrap();
271
272        assert_eq!(recovered, address);
273        assert_eq!(recovered2, address);
274    }
275
276    #[tokio::test]
277    #[cfg(not(feature = "celo"))]
278    async fn signs_tx() {
279        use crate::TypedTransaction;
280        use ethers_core::types::{TransactionRequest, U64};
281        // retrieved test vector from:
282        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
283        let tx: TypedTransaction = TransactionRequest {
284            from: None,
285            to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
286            value: Some(1_000_000_000.into()),
287            gas: Some(2_000_000.into()),
288            nonce: Some(0.into()),
289            gas_price: Some(21_000_000_000u128.into()),
290            data: None,
291            chain_id: Some(U64::one()),
292        }
293        .into();
294        let wallet: Wallet<SigningKey> =
295            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
296        let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64());
297
298        let sig = wallet.sign_transaction(&tx).await.unwrap();
299        let sighash = tx.sighash();
300        sig.verify(sighash, wallet.address).unwrap();
301    }
302
303    #[tokio::test]
304    #[cfg(not(feature = "celo"))]
305    async fn signs_tx_empty_chain_id() {
306        use crate::TypedTransaction;
307        use ethers_core::types::TransactionRequest;
308        // retrieved test vector from:
309        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
310        let tx: TypedTransaction = TransactionRequest {
311            from: None,
312            to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
313            value: Some(1_000_000_000.into()),
314            gas: Some(2_000_000.into()),
315            nonce: Some(0.into()),
316            gas_price: Some(21_000_000_000u128.into()),
317            data: None,
318            chain_id: None,
319        }
320        .into();
321        let wallet: Wallet<SigningKey> =
322            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
323        let wallet = wallet.with_chain_id(1u64);
324
325        // this should populate the tx chain_id as the signer's chain_id (1) before signing
326        let sig = wallet.sign_transaction(&tx).await.unwrap();
327
328        // since we initialize with None we need to re-set the chain_id for the sighash to be
329        // correct
330        let mut tx = tx;
331        tx.set_chain_id(1);
332        let sighash = tx.sighash();
333        sig.verify(sighash, wallet.address).unwrap();
334    }
335
336    #[test]
337    #[cfg(not(feature = "celo"))]
338    fn signs_tx_empty_chain_id_sync() {
339        use crate::TypedTransaction;
340        use ethers_core::types::TransactionRequest;
341
342        let chain_id = 1337u64;
343        // retrieved test vector from:
344        // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
345        let tx: TypedTransaction = TransactionRequest {
346            from: None,
347            to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
348            value: Some(1_000_000_000u64.into()),
349            gas: Some(2_000_000u64.into()),
350            nonce: Some(0u64.into()),
351            gas_price: Some(21_000_000_000u128.into()),
352            data: None,
353            chain_id: None,
354        }
355        .into();
356        let wallet: Wallet<SigningKey> =
357            "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap();
358        let wallet = wallet.with_chain_id(chain_id);
359
360        // this should populate the tx chain_id as the signer's chain_id (1337) before signing and
361        // normalize the v
362        let sig = wallet.sign_transaction_sync(&tx).unwrap();
363
364        // ensure correct v given the chain - first extract recid
365        let recid = (sig.v - 35) % 2;
366        // eip155 check
367        assert_eq!(sig.v, chain_id * 2 + 35 + recid);
368
369        // since we initialize with None we need to re-set the chain_id for the sighash to be
370        // correct
371        let mut tx = tx;
372        tx.set_chain_id(chain_id);
373        let sighash = tx.sighash();
374        sig.verify(sighash, wallet.address).unwrap();
375    }
376
377    #[test]
378    fn key_to_address() {
379        let wallet: Wallet<SigningKey> =
380            "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
381        assert_eq!(
382            wallet.address,
383            Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed")
384        );
385
386        let wallet: Wallet<SigningKey> =
387            "0000000000000000000000000000000000000000000000000000000000000002".parse().unwrap();
388        assert_eq!(
389            wallet.address,
390            Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed")
391        );
392
393        let wallet: Wallet<SigningKey> =
394            "0000000000000000000000000000000000000000000000000000000000000003".parse().unwrap();
395        assert_eq!(
396            wallet.address,
397            Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed")
398        );
399    }
400
401    #[test]
402    fn key_from_bytes() {
403        let wallet: Wallet<SigningKey> =
404            "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
405
406        let key_as_bytes = wallet.signer.to_bytes();
407        let wallet_from_bytes = Wallet::from_bytes(&key_as_bytes).unwrap();
408
409        assert_eq!(wallet.address, wallet_from_bytes.address);
410        assert_eq!(wallet.chain_id, wallet_from_bytes.chain_id);
411        assert_eq!(wallet.signer, wallet_from_bytes.signer);
412    }
413
414    #[test]
415    fn key_from_str() {
416        let wallet: Wallet<SigningKey> =
417            "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
418
419        // Check FromStr and `0x`
420        let wallet_0x: Wallet<SigningKey> =
421            "0x0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
422        assert_eq!(wallet.address, wallet_0x.address);
423        assert_eq!(wallet.chain_id, wallet_0x.chain_id);
424        assert_eq!(wallet.signer, wallet_0x.signer);
425
426        // Check FromStr and `0X`
427        let wallet_0x_cap: Wallet<SigningKey> =
428            "0X0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
429        assert_eq!(wallet.address, wallet_0x_cap.address);
430        assert_eq!(wallet.chain_id, wallet_0x_cap.chain_id);
431        assert_eq!(wallet.signer, wallet_0x_cap.signer);
432
433        // Check TryFrom<&str>
434        let wallet_0x_tryfrom_str: Wallet<SigningKey> =
435            "0x0000000000000000000000000000000000000000000000000000000000000001"
436                .try_into()
437                .unwrap();
438        assert_eq!(wallet.address, wallet_0x_tryfrom_str.address);
439        assert_eq!(wallet.chain_id, wallet_0x_tryfrom_str.chain_id);
440        assert_eq!(wallet.signer, wallet_0x_tryfrom_str.signer);
441
442        // Check TryFrom<String>
443        let wallet_0x_tryfrom_string: Wallet<SigningKey> =
444            "0x0000000000000000000000000000000000000000000000000000000000000001"
445                .to_string()
446                .try_into()
447                .unwrap();
448        assert_eq!(wallet.address, wallet_0x_tryfrom_string.address);
449        assert_eq!(wallet.chain_id, wallet_0x_tryfrom_string.chain_id);
450        assert_eq!(wallet.signer, wallet_0x_tryfrom_string.signer);
451
452        // Must fail because of `0z`
453        "0z0000000000000000000000000000000000000000000000000000000000000001"
454            .parse::<Wallet<SigningKey>>()
455            .unwrap_err();
456    }
457}