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