Skip to main content

hopr_crypto_keypair/
key_pair.rs

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