1use 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
37const 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 FromFile {
50 password: &'a str,
52 id_path: &'a str,
54 },
55 FromPrivateKey {
59 private_key: &'a str,
61 },
62 #[cfg(any(feature = "hopli", test))]
65 FromIdIntoFile {
66 id: Uuid,
68 password: &'a str,
70 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 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 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 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 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 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 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 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 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 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 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 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 let salt: [u8; HOPR_KEY_SIZE] = random_bytes();
386
387 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 scrypt(password.as_ref(), &salt, &scrypt_params, &mut key)
399 .map_err(|e| KeyPairError::KeyDerivationError(e.to_string()))?;
400
401 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 let mac = Keccak256::new().chain(&key[16..32]).chain(&ciphertext).finalize();
418
419 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 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 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 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}