hoprd_keypair/
key_pair.rs

1use crate::{
2    errors::{KeyPairError, Result},
3    keystore::{CipherparamsJson, CryptoJson, EthKeystore, KdfType, KdfparamsType, PrivateKeys},
4};
5use aes::{
6    cipher::{self, InnerIvInit, KeyInit, StreamCipherCore},
7    Aes128,
8};
9use hex;
10use hopr_crypto_random::random_bytes;
11use hopr_crypto_types::prelude::*;
12use hopr_platform::file::native::{metadata, read_to_string, write};
13use hopr_primitive_types::prelude::*;
14use scrypt::{scrypt, Params as ScryptParams};
15use serde::{ser::SerializeStruct, Serialize, Serializer};
16use serde_json::{from_str as from_json_string, to_string as to_json_string};
17use sha3::{digest::Update, Digest, Keccak256};
18use std::fmt::Debug;
19use tracing::{info, warn};
20use typenum::Unsigned;
21use uuid::Uuid;
22
23const HOPR_CIPHER: &str = "aes-128-ctr";
24const HOPR_KEY_SIZE: usize = 32usize;
25const HOPR_IV_SIZE: usize = 16usize;
26const HOPR_KDF_PARAMS_DKLEN: u8 = 32u8;
27const HOPR_KDF_PARAMS_LOG_N: u8 = 13u8;
28const HOPR_KDF_PARAMS_R: u32 = 8u32;
29const HOPR_KDF_PARAMS_P: u32 = 1u32;
30
31const PACKET_KEY_LENGTH: usize = <OffchainKeypair as Keypair>::SecretLen::USIZE;
32const CHAIN_KEY_LENGTH: usize = <ChainKeypair as Keypair>::SecretLen::USIZE;
33
34const V1_PRIVKEY_LENGTH: usize = 32;
35const V2_PRIVKEYS_LENGTH: usize = 172;
36
37// Current version, deviates from pre 2.0
38const VERSION: u32 = 2;
39
40#[cfg(any(debug_assertions, test))]
41const USE_WEAK_CRYPTO: bool = true;
42
43#[cfg(all(not(debug_assertions), not(test)))]
44const USE_WEAK_CRYPTO: bool = false;
45
46struct Aes128Ctr {
47    inner: ctr::CtrCore<Aes128, ctr::flavors::Ctr128BE>,
48}
49
50impl Aes128Ctr {
51    fn new(key: &[u8], iv: &[u8]) -> std::result::Result<Self, cipher::InvalidLength> {
52        let cipher = aes::Aes128::new_from_slice(key).unwrap();
53        let inner = ctr::CtrCore::inner_iv_slice_init(cipher, iv).unwrap();
54        Ok(Self { inner })
55    }
56
57    fn apply_keystream(self, buf: &mut [u8]) {
58        self.inner.apply_keystream_partial(buf.into());
59    }
60}
61
62pub enum IdentityRetrievalModes<'a> {
63    /// hoprd starts with a previously generated identitiy file.
64    /// If none is present, create a new one
65    FromFile {
66        /// Used encrypt / decrypt identity file
67        password: &'a str,
68        /// path to store / load identity file
69        id_path: &'a str,
70    },
71    /// hoprd receives at startup a private which it will use
72    /// for the entire runtime. The private stays in memory and
73    /// thus remains fluent
74    FromPrivateKey {
75        /// hex string containing the private key
76        private_key: &'a str,
77    },
78    /// takes a private key and create an identity file at the
79    /// provided file destination
80    #[cfg(any(feature = "hopli", test))]
81    FromIdIntoFile {
82        /// identifier of this keypair
83        id: Uuid,
84        /// Used encrypt / decrypt identity file
85        password: &'a str,
86        /// path to store / load identity file
87        id_path: &'a str,
88    },
89}
90
91pub struct HoprKeys {
92    pub packet_key: OffchainKeypair,
93    pub chain_key: ChainKeypair,
94    id: Uuid,
95}
96
97impl TryFrom<IdentityRetrievalModes<'_>> for HoprKeys {
98    type Error = KeyPairError;
99
100    fn try_from(value: IdentityRetrievalModes) -> std::result::Result<Self, Self::Error> {
101        Self::init(value)
102    }
103}
104
105impl Serialize for HoprKeys {
106    /// Serialize without private keys
107    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        let mut s = serializer.serialize_struct("HoprKeys", 3)?;
112        s.serialize_field("peer_id", self.packet_key.public().to_peerid_str().as_str())?;
113        s.serialize_field("packet_key", self.packet_key.public().to_hex().as_str())?;
114        s.serialize_field("chain_key", &self.chain_key.public().to_hex().as_str())?;
115        s.serialize_field("native_address", &self.chain_key.public().to_address().to_string())?;
116        s.serialize_field("uuid", &self.id)?;
117        s.end()
118    }
119}
120
121impl std::fmt::Display for HoprKeys {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.write_str(
124            format!(
125                "packet_key: {}, chain_key: {} (Ethereum address: {})\nUUID: {}",
126                self.packet_key.public().to_peerid_str(),
127                self.chain_key.public().to_hex(),
128                self.chain_key.public().0.to_address(),
129                self.id
130            )
131            .as_str(),
132        )
133    }
134}
135
136impl TryFrom<&str> for HoprKeys {
137    type Error = KeyPairError;
138
139    /// Deserializes HoprKeys from string
140    ///
141    /// ```rust
142    /// use hoprd_keypair::key_pair::HoprKeys;
143    ///
144    /// let priv_keys = "0x56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
145    /// assert!(HoprKeys::try_from(priv_keys).is_ok());
146    /// ```
147    fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
148        let maybe_priv_key = s.strip_prefix("0x").unwrap_or(s);
149
150        if maybe_priv_key.len() != 2 * (PACKET_KEY_LENGTH + CHAIN_KEY_LENGTH) {
151            return Err(KeyPairError::InvalidPrivateKeySize {
152                actual: maybe_priv_key.len(),
153                expected: 2 * (PACKET_KEY_LENGTH + CHAIN_KEY_LENGTH),
154            });
155        }
156
157        let mut priv_key_raw = [0u8; PACKET_KEY_LENGTH + CHAIN_KEY_LENGTH];
158        hex::decode_to_slice(maybe_priv_key, &mut priv_key_raw[..])?;
159
160        priv_key_raw.try_into()
161    }
162}
163
164impl TryFrom<[u8; PACKET_KEY_LENGTH + CHAIN_KEY_LENGTH]> for HoprKeys {
165    type Error = KeyPairError;
166    /// Deserializes HoprKeys from binary string
167    ///
168    /// ```rust
169    /// use hoprd_keypair::key_pair::HoprKeys;
170    ///
171    /// let priv_keys = [
172    ///     0x56,0xb2,0x9c,0xef,0xcd,0xf5,0x76,0xee,0xa3,0x06,0xba,0x2f,0xd5,0xf3,0x2e,0x65,
173    ///     0x1c,0x09,0xe0,0xab,0xbc,0x01,0x8c,0x47,0xbd,0xc6,0xef,0x44,0xf6,0xb7,0x50,0x6f,
174    ///     0x10,0x50,0xf9,0x51,0x37,0x77,0x04,0x78,0xf5,0x0b,0x45,0x62,0x67,0xf7,0x61,0xf1,
175    ///     0xb8,0xb3,0x41,0xa1,0x3d,0xa6,0x8b,0xc3,0x2e,0x5c,0x96,0x98,0x4f,0xcd,0x52,0xae
176    /// ];
177    /// assert!(HoprKeys::try_from(priv_keys).is_ok());
178    /// ```
179    fn try_from(value: [u8; CHAIN_KEY_LENGTH + PACKET_KEY_LENGTH]) -> std::result::Result<Self, Self::Error> {
180        let mut packet_key = [0u8; PACKET_KEY_LENGTH];
181        packet_key.copy_from_slice(&value[0..32]);
182        let mut chain_key = [0u8; CHAIN_KEY_LENGTH];
183        chain_key.copy_from_slice(&value[32..64]);
184
185        (packet_key, chain_key).try_into()
186    }
187}
188
189impl TryFrom<([u8; PACKET_KEY_LENGTH], [u8; CHAIN_KEY_LENGTH])> for HoprKeys {
190    type Error = KeyPairError;
191
192    /// Deserializes HoprKeys from tuple of two binary private keys
193    ///
194    /// ```rust
195    /// use hoprd_keypair::key_pair::HoprKeys;
196    ///
197    /// let priv_keys = (
198    /// [
199    ///     0x56,0xb2,0x9c,0xef,0xcd,0xf5,0x76,0xee,0xa3,0x06,0xba,0x2f,0xd5,0xf3,0x2e,0x65,
200    ///     0x1c,0x09,0xe0,0xab,0xbc,0x01,0x8c,0x47,0xbd,0xc6,0xef,0x44,0xf6,0xb7,0x50,0x6f,
201    /// ], [
202    ///     0x10,0x50,0xf9,0x51,0x37,0x77,0x04,0x78,0xf5,0x0b,0x45,0x62,0x67,0xf7,0x61,0xf1,
203    ///     0xb8,0xb3,0x41,0xa1,0x3d,0xa6,0x8b,0xc3,0x2e,0x5c,0x96,0x98,0x4f,0xcd,0x52,0xae
204    /// ]);
205    /// assert!(HoprKeys::try_from(priv_keys).is_ok());
206    /// ```
207    fn try_from(value: ([u8; PACKET_KEY_LENGTH], [u8; CHAIN_KEY_LENGTH])) -> std::result::Result<Self, Self::Error> {
208        Ok(HoprKeys {
209            packet_key: OffchainKeypair::from_secret(&value.0)?,
210            chain_key: ChainKeypair::from_secret(&value.1)?,
211            id: Uuid::new_v4(),
212        })
213    }
214}
215
216impl PartialEq for HoprKeys {
217    fn eq(&self, other: &Self) -> bool {
218        self.packet_key.public().eq(other.packet_key.public()) && self.chain_key.public().eq(other.chain_key.public())
219    }
220}
221
222impl HoprKeys {
223    /// Creates two new keypairs, one for off-chain affairs and
224    /// another one to be used within the smart contract
225    pub fn random() -> Self {
226        Self {
227            packet_key: OffchainKeypair::random(),
228            chain_key: ChainKeypair::random(),
229            id: Uuid::new_v4(),
230        }
231    }
232
233    /// Initializes HoprKeys using the provided retrieval mode
234    fn init(retrieval_mode: IdentityRetrievalModes) -> Result<Self> {
235        match retrieval_mode {
236            IdentityRetrievalModes::FromFile { password, id_path } => {
237                let identity_file_exists = metadata(id_path).is_ok();
238
239                if identity_file_exists {
240                    info!("identity file exists at {}", id_path);
241
242                    match HoprKeys::read_eth_keystore(id_path, password) {
243                        Ok((keys, needs_migration)) => {
244                            info!("migration needed = {}", needs_migration);
245                            if needs_migration {
246                                keys.write_eth_keystore(id_path, password)?
247                            }
248                            Ok(keys)
249                        }
250                        Err(e) => Err(KeyPairError::GeneralError(
251                            format!("An identity file is present at {id_path} but the provided password <REDACTED> is not sufficient to decrypt it {e}"),
252                        )),
253                    }
254                } else {
255                    let keys = HoprKeys::random();
256
257                    info!("created a new set of keypairs at {}", id_path);
258                    info!("{}", keys);
259
260                    keys.write_eth_keystore(id_path, password)?;
261                    Ok(keys)
262                }
263            }
264            IdentityRetrievalModes::FromPrivateKey { private_key } => {
265                info!("initializing HoprKeys with provided private keys <REDACTED>");
266
267                private_key.try_into()
268            }
269            #[cfg(any(feature = "hopli", test))]
270            IdentityRetrievalModes::FromIdIntoFile { id, password, id_path } => {
271                let identity_file_exists = metadata(id_path).is_ok();
272
273                if identity_file_exists {
274                    info!("identity file exists at {}", id_path);
275
276                    Err(KeyPairError::GeneralError(format!(
277                        "Cannot create identity file at {} because the file already exists.",
278                        id_path
279                    )))
280                } else {
281                    let keys: HoprKeys = HoprKeys {
282                        id,
283                        packet_key: OffchainKeypair::random(),
284                        chain_key: ChainKeypair::random(),
285                    };
286
287                    keys.write_eth_keystore(id_path, password)?;
288
289                    Ok(keys)
290                }
291            }
292        }
293    }
294
295    /// Reads a keystore file using custom FS operations
296    ///
297    /// Highly inspired by `<https://github.com/roynalnaruto/eth-keystore-rs>`
298    pub fn read_eth_keystore(path: &str, password: &str) -> Result<(Self, bool)> {
299        let json_string = read_to_string(path)?;
300        let keystore: EthKeystore = from_json_string(&json_string)?;
301
302        let key = match keystore.crypto.kdfparams {
303            KdfparamsType::Scrypt { dklen, n, p, r, salt } => {
304                let mut key = vec![0u8; dklen as usize];
305                let log_n = (n as f32).log2() as u8;
306                let scrypt_params = ScryptParams::new(log_n, r, p, dklen.into())
307                    .map_err(|err| KeyPairError::KeyDerivationError(err.to_string()))?;
308                // derive "master key" and store it in `key`
309                scrypt(password.as_ref(), &salt, &scrypt_params, &mut key)
310                    .map_err(|err| KeyPairError::KeyDerivationError(err.to_string()))?;
311                key
312            }
313            _ => panic!("HOPR only supports scrypt"),
314        };
315
316        // Derive the MAC from the derived key and ciphertext.
317        let derived_mac = Keccak256::new()
318            .chain(&key[16..32])
319            .chain(&keystore.crypto.ciphertext)
320            .finalize();
321
322        if *derived_mac != *keystore.crypto.mac {
323            return Err(KeyPairError::MacMismatch);
324        }
325
326        // Decrypt the private key bytes using AES-128-CTR
327        let decryptor = Aes128Ctr::new(&key[..16], &keystore.crypto.cipherparams.iv[..16]).expect("invalid length");
328
329        let mut pk = keystore.crypto.ciphertext;
330
331        match pk.len() {
332            V1_PRIVKEY_LENGTH => {
333                decryptor.apply_keystream(&mut pk);
334
335                let packet_key: [u8; PACKET_KEY_LENGTH] = random_bytes();
336
337                let mut chain_key = [0u8; CHAIN_KEY_LENGTH];
338                chain_key.clone_from_slice(&pk.as_slice()[0..CHAIN_KEY_LENGTH]);
339
340                let ret: HoprKeys = (packet_key, chain_key).try_into().unwrap();
341
342                Ok((ret, true))
343            }
344            V2_PRIVKEYS_LENGTH => {
345                decryptor.apply_keystream(&mut pk);
346
347                let private_keys = serde_json::from_slice::<PrivateKeys>(&pk)?;
348
349                if private_keys.packet_key.len() != PACKET_KEY_LENGTH {
350                    return Err(KeyPairError::InvalidEncryptedKeyLength {
351                        actual: private_keys.packet_key.len(),
352                        expected: PACKET_KEY_LENGTH,
353                    });
354                }
355
356                if private_keys.chain_key.len() != CHAIN_KEY_LENGTH {
357                    return Err(KeyPairError::InvalidEncryptedKeyLength {
358                        actual: private_keys.chain_key.len(),
359                        expected: CHAIN_KEY_LENGTH,
360                    });
361                }
362
363                let mut packet_key = [0u8; PACKET_KEY_LENGTH];
364                packet_key.clone_from_slice(private_keys.packet_key.as_slice());
365
366                let mut chain_key = [0u8; CHAIN_KEY_LENGTH];
367                chain_key.clone_from_slice(private_keys.chain_key.as_slice());
368
369                Ok((
370                    HoprKeys {
371                        packet_key: OffchainKeypair::from_secret(&packet_key).unwrap(),
372                        chain_key: ChainKeypair::from_secret(&chain_key).unwrap(),
373                        id: keystore.id,
374                    },
375                    false,
376                ))
377            }
378            _ => Err(KeyPairError::InvalidEncryptedKeyLength {
379                actual: pk.len(),
380                expected: V2_PRIVKEYS_LENGTH,
381            }),
382        }
383    }
384
385    /// Writes a keystore file using custom FS operation and custom entropy source
386    ///
387    /// Highly inspired by `<https://github.com/roynalnaruto/eth-keystore-rs>`
388    pub fn write_eth_keystore(&self, path: &str, password: &str) -> Result<()> {
389        if USE_WEAK_CRYPTO {
390            warn!("USING WEAK CRYPTO -> this build is not meant for production!");
391        }
392        // Generate a random salt.
393        let salt: [u8; HOPR_KEY_SIZE] = random_bytes();
394
395        // Derive the key.
396        let mut key = [0u8; HOPR_KDF_PARAMS_DKLEN as usize];
397        let scrypt_params = ScryptParams::new(
398            if USE_WEAK_CRYPTO { 1 } else { HOPR_KDF_PARAMS_LOG_N },
399            HOPR_KDF_PARAMS_R,
400            HOPR_KDF_PARAMS_P,
401            HOPR_KDF_PARAMS_DKLEN.into(),
402        )
403        .map_err(|e| KeyPairError::KeyDerivationError(e.to_string()))?;
404
405        // derive "master key" and store it in `key`
406        scrypt(password.as_ref(), &salt, &scrypt_params, &mut key)
407            .map_err(|e| KeyPairError::KeyDerivationError(e.to_string()))?;
408
409        // Encrypt the private key using AES-128-CTR.
410        let iv: [u8; HOPR_IV_SIZE] = random_bytes();
411
412        let encryptor = Aes128Ctr::new(&key[..16], &iv[..16]).expect("invalid length");
413
414        let private_keys = PrivateKeys {
415            chain_key: self.chain_key.secret().as_ref().to_vec(),
416            packet_key: self.packet_key.secret().as_ref().to_vec(),
417            version: VERSION,
418        };
419
420        let mut ciphertext = serde_json::to_vec(&private_keys)?;
421        encryptor.apply_keystream(&mut ciphertext);
422
423        // Calculate the MAC.
424        let mac = Keccak256::new().chain(&key[16..32]).chain(&ciphertext).finalize();
425
426        // Construct and serialize the encrypted JSON keystore.
427        let keystore = EthKeystore {
428            id: self.id,
429            version: 3,
430            crypto: CryptoJson {
431                cipher: String::from(HOPR_CIPHER),
432                cipherparams: CipherparamsJson { iv: iv.to_vec() },
433                ciphertext,
434                kdf: KdfType::Scrypt,
435                kdfparams: KdfparamsType::Scrypt {
436                    dklen: HOPR_KDF_PARAMS_DKLEN,
437                    n: 2u32.pow(if USE_WEAK_CRYPTO { 1 } else { HOPR_KDF_PARAMS_LOG_N } as u32),
438                    p: HOPR_KDF_PARAMS_P,
439                    r: HOPR_KDF_PARAMS_R,
440                    salt: salt.to_vec(),
441                },
442                mac: mac.to_vec(),
443            },
444        };
445
446        let serialized = to_json_string(&keystore)?;
447
448        write(path, serialized).map_err(|e| e.into())
449    }
450
451    pub fn id(&self) -> &Uuid {
452        &self.id
453    }
454}
455
456impl Debug for HoprKeys {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        f.debug_struct("HoprKeys")
459            .field(
460                "packet_key",
461                &format_args!("(priv_key: <REDACTED>, pub_key: {}", self.packet_key.public().to_hex()),
462            )
463            .field(
464                "chain_key",
465                &format_args!("(priv_key: <REDACTED>, pub_key: {}", self.chain_key.public().to_hex()),
466            )
467            .finish()
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use std::fs;
474
475    use anyhow::Context;
476    use hopr_crypto_types::prelude::*;
477    use tempfile::tempdir;
478    use uuid::Uuid;
479
480    use super::HoprKeys;
481
482    const DEFAULT_PASSWORD: &str = "dummy password for unit testing";
483
484    #[test]
485    fn create_keys() {
486        println!("{:?}", super::HoprKeys::random())
487    }
488
489    #[test]
490    fn store_keys_and_read_them() -> anyhow::Result<()> {
491        let tmp = tempdir()?;
492
493        let identity_dir = tmp.path().join("hopr-unit-test-identity");
494
495        let keys = super::HoprKeys::random();
496
497        keys.write_eth_keystore(
498            identity_dir.to_str().context("should be convertible to string")?,
499            DEFAULT_PASSWORD,
500        )?;
501
502        let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(
503            identity_dir.to_str().context("should be convertible to string")?,
504            DEFAULT_PASSWORD,
505        )?;
506
507        assert!(!needs_migration);
508        assert_eq!(deserialized, keys);
509
510        Ok(())
511    }
512
513    #[test]
514    fn test_migration() -> anyhow::Result<()> {
515        let tmp = tempdir()?;
516
517        let identity_dir = tmp.path().join("hopr-unit-test-identity");
518
519        let old_keystore_file = r#"{"id":"8e5fe142-6ef9-4fbb-aae8-5de32b680e31","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"04141354edb9dfb0c65e6905a3a0b9dd"},"ciphertext":"74f12f72cf2d3d73ff09f783cb9b57995b3808f7d3f71aa1fa1968696aedfbdd","kdf":"scrypt","kdfparams":{"salt":"f5e3f04eaa0c9efffcb5168c6735d7e1fe4d96f48a636c4f00107e7c34722f45","n":1,"dklen":32,"p":1,"r":8},"mac":"d0daf0e5d14a2841f0f7221014d805addfb7609d85329d4c6424a098e50b6fbe"}}"#;
520
521        fs::write(
522            identity_dir.to_str().context("should be convertible to string")?,
523            old_keystore_file.as_bytes(),
524        )?;
525
526        let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(
527            identity_dir.to_str().context("should be convertible to string")?,
528            "local",
529        )?;
530
531        assert!(needs_migration);
532        assert_eq!(
533            deserialized.chain_key.public().0.to_address().to_string(),
534            "0x826a1bf3d51fa7f402a1e01d1b2c8a8bac28e666"
535        );
536
537        Ok(())
538    }
539
540    #[test]
541    fn test_auto_migration() -> anyhow::Result<()> {
542        let tmp = tempdir()?;
543        let identity_dir = tmp.path().join("hopr-unit-test-identity");
544        let identity_path: &str = identity_dir.to_str().context("should be convertible to string")?;
545
546        let old_keystore_file = r#"{"id":"8e5fe142-6ef9-4fbb-aae8-5de32b680e31","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"04141354edb9dfb0c65e6905a3a0b9dd"},"ciphertext":"74f12f72cf2d3d73ff09f783cb9b57995b3808f7d3f71aa1fa1968696aedfbdd","kdf":"scrypt","kdfparams":{"salt":"f5e3f04eaa0c9efffcb5168c6735d7e1fe4d96f48a636c4f00107e7c34722f45","n":1,"dklen":32,"p":1,"r":8},"mac":"d0daf0e5d14a2841f0f7221014d805addfb7609d85329d4c6424a098e50b6fbe"}}"#;
547        fs::write(identity_path, old_keystore_file.as_bytes()).unwrap();
548
549        assert!(super::HoprKeys::init(super::IdentityRetrievalModes::FromFile {
550            password: "local".into(),
551            id_path: identity_path.into()
552        })
553        .is_ok());
554
555        let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(identity_path, "local").unwrap();
556
557        assert!(!needs_migration);
558        assert_eq!(
559            deserialized.chain_key.public().0.to_address().to_string(),
560            "0x826a1bf3d51fa7f402a1e01d1b2c8a8bac28e666"
561        );
562
563        Ok(())
564    }
565
566    #[test]
567    fn test_should_not_overwrite_existing() -> anyhow::Result<()> {
568        let tmp = tempdir()?;
569        let identity_dir = tmp.path().join("hopr-unit-test-identity");
570        let identity_path: &str = identity_dir.to_str().context("should be convertible to string")?;
571
572        fs::write(identity_path, "".as_bytes())?;
573
574        // Overwriting existing keys must not be possible
575        assert!(super::HoprKeys::init(super::IdentityRetrievalModes::FromFile {
576            password: "local".into(),
577            id_path: identity_path.into()
578        })
579        .is_err());
580
581        Ok(())
582    }
583
584    #[test]
585    fn test_from_privatekey() {
586        let private_key = "0x56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
587
588        let from_private_key = HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey { private_key }).unwrap();
589
590        let private_key_without_prefix = "56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
591
592        let from_private_key_without_prefix = HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
593            private_key: private_key_without_prefix,
594        })
595        .unwrap();
596
597        // both ways should work
598        assert_eq!(from_private_key, from_private_key_without_prefix);
599
600        let too_short_private_key = "0xb29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
601
602        assert!(HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
603            private_key: too_short_private_key
604        })
605        .is_err());
606
607        let too_long_private_key = "0x56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52aeae";
608
609        assert!(HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
610            private_key: too_long_private_key
611        })
612        .is_err());
613
614        let non_hex_private = "this is the story of hopr: in 2018 ...";
615        assert!(HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
616            private_key: non_hex_private
617        })
618        .is_err());
619    }
620
621    #[test]
622    fn test_from_privatekey_into_file() -> anyhow::Result<()> {
623        let tmp = tempdir()?;
624        let identity_dir = tmp.path().join("hopr-unit-test-identity");
625        let identity_path = identity_dir.to_str().context("should be convertible to string")?;
626        let id = Uuid::new_v4();
627
628        let keys = HoprKeys::init(super::IdentityRetrievalModes::FromIdIntoFile {
629            password: "local",
630            id_path: identity_path,
631            id,
632        })
633        .expect("should initialize new key");
634
635        let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(identity_path, "local").unwrap();
636
637        assert!(!needs_migration);
638        assert_eq!(
639            deserialized.chain_key.public().to_address(),
640            keys.chain_key.public().to_address()
641        );
642
643        // Overwriting existing keys must not be possible
644        assert!(super::HoprKeys::init(super::IdentityRetrievalModes::FromIdIntoFile {
645            password: "local",
646            id_path: identity_path,
647            id
648        })
649        .is_err());
650
651        Ok(())
652    }
653}