alloy_signer_local/
private_key.rs

1//! [`k256`] signer implementation.
2
3use super::{LocalSigner, LocalSignerError};
4use alloy_primitives::{hex, B256};
5use alloy_signer::utils::secret_key_to_address;
6use k256::{
7    ecdsa::{self, SigningKey},
8    FieldBytes, NonZeroScalar, SecretKey as K256SecretKey,
9};
10use rand::{CryptoRng, Rng};
11use std::str::FromStr;
12
13#[cfg(feature = "keystore")]
14use std::path::Path;
15
16impl LocalSigner<SigningKey> {
17    /// Creates a new [`LocalSigner`] instance from a [`SigningKey`].
18    ///
19    /// This can also be used to create a [`LocalSigner`] from a [`SecretKey`](K256SecretKey).
20    /// See also the `From` implementations.
21    #[doc(alias = "from_private_key")]
22    #[doc(alias = "new_private_key")]
23    #[doc(alias = "new_pk")]
24    #[inline]
25    pub fn from_signing_key(credential: SigningKey) -> Self {
26        let address = secret_key_to_address(&credential);
27        Self::new_with_credential(credential, address, None)
28    }
29
30    /// Creates a new [`LocalSigner`] instance from a raw scalar serialized as a [`B256`] byte
31    /// array.
32    ///
33    /// This is identical to [`from_field_bytes`](Self::from_field_bytes).
34    #[inline]
35    pub fn from_bytes(bytes: &B256) -> Result<Self, ecdsa::Error> {
36        Self::from_field_bytes((&bytes.0).into())
37    }
38
39    /// Creates a new [`LocalSigner`] instance from a raw scalar serialized as a [`FieldBytes`] byte
40    /// array.
41    #[inline]
42    pub fn from_field_bytes(bytes: &FieldBytes) -> Result<Self, ecdsa::Error> {
43        SigningKey::from_bytes(bytes).map(Self::from_signing_key)
44    }
45
46    /// Creates a new [`LocalSigner`] instance from a raw scalar serialized as a byte slice.
47    ///
48    /// Byte slices shorter than the field size (32 bytes) are handled by zero padding the input.
49    #[inline]
50    pub fn from_slice(bytes: &[u8]) -> Result<Self, ecdsa::Error> {
51        SigningKey::from_slice(bytes).map(Self::from_signing_key)
52    }
53
54    /// Creates a new random keypair seeded with [`rand::thread_rng()`].
55    #[inline]
56    pub fn random() -> Self {
57        Self::random_with(&mut rand::thread_rng())
58    }
59
60    /// Creates a new random keypair seeded with the provided RNG.
61    #[inline]
62    pub fn random_with<R: Rng + CryptoRng>(rng: &mut R) -> Self {
63        Self::from_signing_key(SigningKey::random(rng))
64    }
65
66    /// Borrow the secret [`NonZeroScalar`] value for this key.
67    ///
68    /// # ⚠️ Warning
69    ///
70    /// This value is key material.
71    ///
72    /// Please treat it with the care it deserves!
73    #[inline]
74    pub fn as_nonzero_scalar(&self) -> &NonZeroScalar {
75        self.credential.as_nonzero_scalar()
76    }
77
78    /// Serialize this [`LocalSigner`]'s [`SigningKey`] as a [`B256`] byte array.
79    #[inline]
80    pub fn to_bytes(&self) -> B256 {
81        B256::new(<[u8; 32]>::from(self.to_field_bytes()))
82    }
83
84    /// Serialize this [`LocalSigner`]'s [`SigningKey`] as a [`FieldBytes`] byte array.
85    #[inline]
86    pub fn to_field_bytes(&self) -> FieldBytes {
87        self.credential.to_bytes()
88    }
89}
90
91#[cfg(feature = "keystore")]
92impl LocalSigner<SigningKey> {
93    /// Creates a new random encrypted JSON with the provided password and stores it in the
94    /// provided directory. Returns a tuple (LocalSigner, String) of the signer instance for the
95    /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`,
96    /// the keystore is stored as the stringified UUID.
97    #[inline]
98    pub fn new_keystore<P, R, S>(
99        dir: P,
100        rng: &mut R,
101        password: S,
102        name: Option<&str>,
103    ) -> Result<(Self, String), LocalSignerError>
104    where
105        P: AsRef<Path>,
106        R: Rng + CryptoRng,
107        S: AsRef<[u8]>,
108    {
109        let (secret, uuid) = eth_keystore::new(dir, rng, password, name)?;
110        Ok((Self::from_slice(&secret)?, uuid))
111    }
112
113    /// Decrypts an encrypted JSON from the provided path to construct a [`LocalSigner`] instance
114    #[inline]
115    pub fn decrypt_keystore<P, S>(keypath: P, password: S) -> Result<Self, LocalSignerError>
116    where
117        P: AsRef<Path>,
118        S: AsRef<[u8]>,
119    {
120        let secret = eth_keystore::decrypt_key(keypath, password)?;
121        Ok(Self::from_slice(&secret)?)
122    }
123
124    /// Creates a new encrypted JSON with the provided private key and password and stores it in the
125    /// provided directory. Returns a tuple (LocalSigner, String) of the signer instance for the
126    /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`,
127    /// the keystore is stored as the stringified UUID.
128    #[inline]
129    pub fn encrypt_keystore<P, R, B, S>(
130        keypath: P,
131        rng: &mut R,
132        pk: B,
133        password: S,
134        name: Option<&str>,
135    ) -> Result<(Self, String), LocalSignerError>
136    where
137        P: AsRef<Path>,
138        R: Rng + CryptoRng,
139        B: AsRef<[u8]>,
140        S: AsRef<[u8]>,
141    {
142        let pk = pk.as_ref();
143        let uuid = eth_keystore::encrypt_key(keypath, rng, pk, password, name)?;
144        Ok((Self::from_slice(pk)?, uuid))
145    }
146}
147
148impl PartialEq for LocalSigner<SigningKey> {
149    fn eq(&self, other: &Self) -> bool {
150        self.credential.to_bytes().eq(&other.credential.to_bytes())
151            && self.address == other.address
152            && self.chain_id == other.chain_id
153    }
154}
155
156impl From<SigningKey> for LocalSigner<SigningKey> {
157    fn from(value: SigningKey) -> Self {
158        Self::from_signing_key(value)
159    }
160}
161
162impl From<K256SecretKey> for LocalSigner<SigningKey> {
163    fn from(value: K256SecretKey) -> Self {
164        Self::from_signing_key(value.into())
165    }
166}
167
168impl FromStr for LocalSigner<SigningKey> {
169    type Err = LocalSignerError;
170
171    fn from_str(src: &str) -> Result<Self, Self::Err> {
172        let array = hex::decode_to_array::<_, 32>(src)?;
173        Ok(Self::from_slice(&array)?)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::{PrivateKeySigner, SignerSync};
181    use alloy_primitives::{address, b256};
182
183    #[cfg(feature = "keystore")]
184    use tempfile::tempdir;
185
186    #[test]
187    fn parse_pk() {
188        let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b";
189        let _pk: PrivateKeySigner = s.parse().unwrap();
190    }
191
192    #[test]
193    fn parse_short_key() {
194        let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea3";
195        assert!(s.len() < 64);
196        let pk = s.parse::<PrivateKeySigner>().unwrap_err();
197        match pk {
198            LocalSignerError::HexError(hex::FromHexError::InvalidStringLength) => {}
199            _ => panic!("Unexpected error"),
200        }
201    }
202
203    #[cfg(feature = "keystore")]
204    fn test_encrypted_json_keystore(key: LocalSigner<SigningKey>, uuid: &str, dir: &Path) {
205        // sign a message using the given key
206        let message = "Some data";
207        let signature = key.sign_message_sync(message.as_bytes()).unwrap();
208
209        // read from the encrypted JSON keystore and decrypt it, while validating that the
210        // signatures produced by both the keys should match
211        let path = Path::new(dir).join(uuid);
212        let key2 = LocalSigner::<SigningKey>::decrypt_keystore(path.clone(), "randpsswd").unwrap();
213
214        let signature2 = key2.sign_message_sync(message.as_bytes()).unwrap();
215        assert_eq!(signature, signature2);
216
217        std::fs::remove_file(&path).unwrap();
218    }
219
220    #[test]
221    #[cfg(feature = "keystore")]
222    fn encrypted_json_keystore_new() {
223        // create and store an encrypted JSON keystore in this directory
224        let dir = tempdir().unwrap();
225        let mut rng = rand::thread_rng();
226        let (key, uuid) =
227            LocalSigner::<SigningKey>::new_keystore(&dir, &mut rng, "randpsswd", None).unwrap();
228
229        test_encrypted_json_keystore(key, &uuid, dir.path());
230    }
231
232    #[test]
233    #[cfg(feature = "keystore")]
234    fn encrypted_json_keystore_from_pk() {
235        // create and store an encrypted JSON keystore in this directory
236        let dir = tempdir().unwrap();
237        let mut rng = rand::thread_rng();
238
239        let private_key =
240            hex::decode("6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b")
241                .unwrap();
242
243        let (key, uuid) = LocalSigner::<SigningKey>::encrypt_keystore(
244            &dir,
245            &mut rng,
246            private_key,
247            "randpsswd",
248            None,
249        )
250        .unwrap();
251
252        test_encrypted_json_keystore(key, &uuid, dir.path());
253    }
254
255    #[test]
256    #[cfg(feature = "keystore-geth-compat")]
257    fn test_encrypted_json_keystore_with_address() {
258        // create and store an encrypted JSON keystore in this directory
259
260        use std::fs::File;
261
262        use eth_keystore::EthKeystore;
263        let dir = tempdir().unwrap();
264        let mut rng = rand::thread_rng();
265        let (key, uuid) =
266            LocalSigner::<SigningKey>::new_keystore(&dir, &mut rng, "randpsswd", None).unwrap();
267
268        let path = Path::new(dir.path()).join(uuid.clone());
269        let file = File::open(path).unwrap();
270        let keystore = serde_json::from_reader::<_, EthKeystore>(file).unwrap();
271
272        assert!(!keystore.address.is_zero());
273
274        test_encrypted_json_keystore(key, &uuid, dir.path());
275    }
276
277    #[test]
278    fn signs_msg() {
279        let message = "Some data";
280        let hash = alloy_primitives::utils::eip191_hash_message(message);
281        let key = LocalSigner::<SigningKey>::random_with(&mut rand::thread_rng());
282        let address = key.address;
283
284        // sign a message
285        let signature = key.sign_message_sync(message.as_bytes()).unwrap();
286
287        // ecrecover via the message will hash internally
288        let recovered = signature.recover_address_from_msg(message).unwrap();
289        assert_eq!(recovered, address);
290
291        // if provided with a hash, it will skip hashing
292        let recovered2 = signature.recover_address_from_prehash(&hash).unwrap();
293        assert_eq!(recovered2, address);
294    }
295
296    #[test]
297    #[cfg(feature = "eip712")]
298    fn typed_data() {
299        use alloy_dyn_abi::eip712::TypedData;
300        use alloy_primitives::{keccak256, Address, I256, U256};
301        use alloy_sol_types::{eip712_domain, sol, SolStruct};
302        use serde::Serialize;
303
304        sol! {
305            #[derive(Debug, Serialize)]
306            struct FooBar {
307                int256 foo;
308                uint256 bar;
309                bytes fizz;
310                bytes32 buzz;
311                string far;
312                address out;
313            }
314        }
315
316        let domain = eip712_domain! {
317            name: "Eip712Test",
318            version: "1",
319            chain_id: 1,
320            verifying_contract: address!("0000000000000000000000000000000000000001"),
321            salt: keccak256("eip712-test-75F0CCte"),
322        };
323        let foo_bar = FooBar {
324            foo: I256::try_from(10u64).unwrap(),
325            bar: U256::from(20u64),
326            fizz: b"fizz".to_vec().into(),
327            buzz: keccak256("buzz"),
328            far: "space".into(),
329            out: Address::ZERO,
330        };
331        let signer = LocalSigner::random();
332        let hash = foo_bar.eip712_signing_hash(&domain);
333        let sig = signer.sign_typed_data_sync(&foo_bar, &domain).unwrap();
334        assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), signer.address());
335        assert_eq!(signer.sign_hash_sync(&hash).unwrap(), sig);
336        let foo_bar_dynamic = TypedData::from_struct(&foo_bar, Some(domain));
337        let dynamic_hash = foo_bar_dynamic.eip712_signing_hash().unwrap();
338        let sig_dynamic = signer.sign_dynamic_typed_data_sync(&foo_bar_dynamic).unwrap();
339        assert_eq!(
340            sig_dynamic.recover_address_from_prehash(&dynamic_hash).unwrap(),
341            signer.address()
342        );
343        assert_eq!(signer.sign_hash_sync(&dynamic_hash).unwrap(), sig_dynamic);
344    }
345
346    #[test]
347    fn key_to_address() {
348        let signer: LocalSigner<SigningKey> =
349            "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
350        assert_eq!(signer.address, address!("7E5F4552091A69125d5DfCb7b8C2659029395Bdf"));
351
352        let signer: LocalSigner<SigningKey> =
353            "0000000000000000000000000000000000000000000000000000000000000002".parse().unwrap();
354        assert_eq!(signer.address, address!("2B5AD5c4795c026514f8317c7a215E218DcCD6cF"));
355
356        let signer: LocalSigner<SigningKey> =
357            "0000000000000000000000000000000000000000000000000000000000000003".parse().unwrap();
358        assert_eq!(signer.address, address!("6813Eb9362372EEF6200f3b1dbC3f819671cBA69"));
359    }
360
361    #[test]
362    fn conversions() {
363        let key = b256!("0000000000000000000000000000000000000000000000000000000000000001");
364
365        let signer_b256: LocalSigner<SigningKey> = LocalSigner::from_bytes(&key).unwrap();
366        assert_eq!(signer_b256.address, address!("7E5F4552091A69125d5DfCb7b8C2659029395Bdf"));
367        assert_eq!(signer_b256.chain_id, None);
368        assert_eq!(signer_b256.credential, SigningKey::from_bytes((&key.0).into()).unwrap());
369
370        let signer_str = LocalSigner::from_str(
371            "0000000000000000000000000000000000000000000000000000000000000001",
372        )
373        .unwrap();
374        assert_eq!(signer_str.address, signer_b256.address);
375        assert_eq!(signer_str.chain_id, signer_b256.chain_id);
376        assert_eq!(signer_str.credential, signer_b256.credential);
377        assert_eq!(signer_str.to_bytes(), key);
378        assert_eq!(signer_str.to_field_bytes(), key.0.into());
379
380        let signer_slice = LocalSigner::from_slice(&key[..]).unwrap();
381        assert_eq!(signer_slice.address, signer_b256.address);
382        assert_eq!(signer_slice.chain_id, signer_b256.chain_id);
383        assert_eq!(signer_slice.credential, signer_b256.credential);
384        assert_eq!(signer_slice.to_bytes(), key);
385        assert_eq!(signer_slice.to_field_bytes(), key.0.into());
386
387        let signer_field_bytes = LocalSigner::from_field_bytes((&key.0).into()).unwrap();
388        assert_eq!(signer_field_bytes.address, signer_b256.address);
389        assert_eq!(signer_field_bytes.chain_id, signer_b256.chain_id);
390        assert_eq!(signer_field_bytes.credential, signer_b256.credential);
391        assert_eq!(signer_field_bytes.to_bytes(), key);
392        assert_eq!(signer_field_bytes.to_field_bytes(), key.0.into());
393    }
394
395    #[test]
396    fn key_from_str() {
397        let signer: LocalSigner<SigningKey> =
398            "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
399
400        // Check FromStr and `0x`
401        let signer_0x: LocalSigner<SigningKey> =
402            "0x0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap();
403        assert_eq!(signer.address, signer_0x.address);
404        assert_eq!(signer.chain_id, signer_0x.chain_id);
405        assert_eq!(signer.credential, signer_0x.credential);
406
407        // Must fail because of `0z`
408        "0z0000000000000000000000000000000000000000000000000000000000000001"
409            .parse::<LocalSigner<SigningKey>>()
410            .unwrap_err();
411    }
412}