hoprd_keypair/
key_pair.rs

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