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().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 {id_path} because the file already exists."
267 )))
268 } else {
269 let keys: HoprKeys = HoprKeys {
270 id,
271 packet_key: OffchainKeypair::random(),
272 chain_key: ChainKeypair::random(),
273 };
274
275 keys.write_eth_keystore(id_path, password)?;
276
277 Ok(keys)
278 }
279 }
280 }
281 }
282
283 pub fn read_eth_keystore(path: &str, password: &str) -> Result<(Self, bool)> {
287 let json_string = read_to_string(path)?;
288 let keystore: EthKeystore = from_json_string(&json_string)?;
289
290 let key = match keystore.crypto.kdfparams {
291 KdfparamsType::Scrypt { dklen, n, p, r, salt } => {
292 let mut key = vec![0u8; dklen as usize];
293 let log_n = (n as f32).log2() as u8;
294 let scrypt_params = ScryptParams::new(log_n, r, p, dklen.into())
295 .map_err(|err| KeyPairError::KeyDerivationError(err.to_string()))?;
296 scrypt(password.as_ref(), &salt, &scrypt_params, &mut key)
298 .map_err(|err| KeyPairError::KeyDerivationError(err.to_string()))?;
299 key
300 }
301 _ => panic!("HOPR only supports scrypt"),
302 };
303
304 let derived_mac = Keccak256::new()
306 .chain(&key[16..32])
307 .chain(&keystore.crypto.ciphertext)
308 .finalize();
309
310 if *derived_mac != *keystore.crypto.mac {
311 return Err(KeyPairError::MacMismatch);
312 }
313
314 let mut decryptor = Aes128Ctr::new_from_slices(&key[..16], &keystore.crypto.cipherparams.iv[..16])
316 .map_err(|_| KeyPairError::KeyDerivationError("invalid key or iv length".into()))?;
317
318 let mut pk = keystore.crypto.ciphertext;
319
320 match pk.len() {
321 V1_PRIVKEY_LENGTH => {
322 decryptor.apply_keystream(&mut pk);
323
324 let packet_key: [u8; PACKET_KEY_LENGTH] = random_bytes();
325
326 let mut chain_key = [0u8; CHAIN_KEY_LENGTH];
327 chain_key.clone_from_slice(&pk.as_slice()[0..CHAIN_KEY_LENGTH]);
328
329 let ret: HoprKeys = (packet_key, chain_key)
330 .try_into()
331 .map_err(|_| KeyPairError::GeneralError("cannot instantiate hopr keys".into()))?;
332
333 Ok((ret, true))
334 }
335 V2_PRIVKEYS_LENGTH => {
336 decryptor.apply_keystream(&mut pk);
337
338 let private_keys = serde_json::from_slice::<PrivateKeys>(&pk)?;
339
340 if private_keys.packet_key.len() != PACKET_KEY_LENGTH {
341 return Err(KeyPairError::InvalidEncryptedKeyLength {
342 actual: private_keys.packet_key.len(),
343 expected: PACKET_KEY_LENGTH,
344 });
345 }
346
347 if private_keys.chain_key.len() != CHAIN_KEY_LENGTH {
348 return Err(KeyPairError::InvalidEncryptedKeyLength {
349 actual: private_keys.chain_key.len(),
350 expected: CHAIN_KEY_LENGTH,
351 });
352 }
353
354 let mut packet_key = [0u8; PACKET_KEY_LENGTH];
355 packet_key.clone_from_slice(private_keys.packet_key.as_slice());
356
357 let mut chain_key = [0u8; CHAIN_KEY_LENGTH];
358 chain_key.clone_from_slice(private_keys.chain_key.as_slice());
359
360 Ok((
361 HoprKeys {
362 packet_key: OffchainKeypair::from_secret(&packet_key)?,
363 chain_key: ChainKeypair::from_secret(&chain_key)?,
364 id: keystore.id,
365 },
366 false,
367 ))
368 }
369 _ => Err(KeyPairError::InvalidEncryptedKeyLength {
370 actual: pk.len(),
371 expected: V2_PRIVKEYS_LENGTH,
372 }),
373 }
374 }
375
376 pub fn write_eth_keystore(&self, path: &str, password: &str) -> Result<()> {
380 if USE_WEAK_CRYPTO {
381 warn!("USING WEAK CRYPTO -> this build is not meant for production!");
382 }
383 let salt: [u8; HOPR_KEY_SIZE] = random_bytes();
385
386 let mut key = [0u8; HOPR_KDF_PARAMS_DKLEN as usize];
388 let scrypt_params = ScryptParams::new(
389 if USE_WEAK_CRYPTO { 1 } else { HOPR_KDF_PARAMS_LOG_N },
390 HOPR_KDF_PARAMS_R,
391 HOPR_KDF_PARAMS_P,
392 HOPR_KDF_PARAMS_DKLEN.into(),
393 )
394 .map_err(|e| KeyPairError::KeyDerivationError(e.to_string()))?;
395
396 scrypt(password.as_ref(), &salt, &scrypt_params, &mut key)
398 .map_err(|e| KeyPairError::KeyDerivationError(e.to_string()))?;
399
400 let iv: [u8; HOPR_IV_SIZE] = random_bytes();
402
403 let mut encryptor = Aes128Ctr::new_from_slices(&key[..16], &iv[..16])
404 .map_err(|_| KeyPairError::KeyDerivationError("invalid key or iv".into()))?;
405
406 let private_keys = PrivateKeys {
407 chain_key: self.chain_key.secret().as_ref().to_vec(),
408 packet_key: self.packet_key.secret().as_ref().to_vec(),
409 version: VERSION,
410 };
411
412 let mut ciphertext = serde_json::to_vec(&private_keys)?;
413 encryptor.apply_keystream(&mut ciphertext);
414
415 let mac = Keccak256::new().chain(&key[16..32]).chain(&ciphertext).finalize();
417
418 let keystore = EthKeystore {
420 id: self.id,
421 version: 3,
422 crypto: CryptoJson {
423 cipher: String::from(HOPR_CIPHER),
424 cipherparams: CipherparamsJson { iv: iv.to_vec() },
425 ciphertext,
426 kdf: KdfType::Scrypt,
427 kdfparams: KdfparamsType::Scrypt {
428 dklen: HOPR_KDF_PARAMS_DKLEN,
429 n: 2u32.pow(if USE_WEAK_CRYPTO { 1 } else { HOPR_KDF_PARAMS_LOG_N } as u32),
430 p: HOPR_KDF_PARAMS_P,
431 r: HOPR_KDF_PARAMS_R,
432 salt: salt.to_vec(),
433 },
434 mac: mac.to_vec(),
435 },
436 };
437
438 let serialized = to_json_string(&keystore)?;
439
440 write(path, serialized).map_err(|e| e.into())
441 }
442
443 pub fn id(&self) -> &Uuid {
444 &self.id
445 }
446}
447
448impl Debug for HoprKeys {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 f.debug_struct("HoprKeys")
451 .field(
452 "packet_key",
453 &format_args!("(priv_key: <REDACTED>, pub_key: {}", self.packet_key.public().to_hex()),
454 )
455 .field(
456 "chain_key",
457 &format_args!("(priv_key: <REDACTED>, pub_key: {}", self.chain_key.public().to_hex()),
458 )
459 .finish()
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use std::fs;
466
467 use anyhow::Context;
468 use hopr_crypto_random::Randomizable;
469 use hopr_crypto_types::prelude::*;
470 use tempfile::tempdir;
471 use uuid::Uuid;
472
473 use super::HoprKeys;
474
475 const DEFAULT_PASSWORD: &str = "dummy password for unit testing";
476
477 #[test]
478 fn create_keys() {
479 println!("{:?}", super::HoprKeys::random())
480 }
481
482 #[test]
483 fn store_keys_and_read_them() -> anyhow::Result<()> {
484 let tmp = tempdir()?;
485
486 let identity_dir = tmp.path().join("hopr-unit-test-identity");
487
488 let keys = super::HoprKeys::random();
489
490 keys.write_eth_keystore(
491 identity_dir.to_str().context("should be convertible to string")?,
492 DEFAULT_PASSWORD,
493 )?;
494
495 let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(
496 identity_dir.to_str().context("should be convertible to string")?,
497 DEFAULT_PASSWORD,
498 )?;
499
500 assert!(!needs_migration);
501 assert_eq!(deserialized, keys);
502
503 Ok(())
504 }
505
506 #[test]
507 fn test_migration() -> anyhow::Result<()> {
508 let tmp = tempdir()?;
509
510 let identity_dir = tmp.path().join("hopr-unit-test-identity");
511
512 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"}}"#;
513
514 fs::write(
515 identity_dir.to_str().context("should be convertible to string")?,
516 old_keystore_file.as_bytes(),
517 )?;
518
519 let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(
520 identity_dir.to_str().context("should be convertible to string")?,
521 "local",
522 )?;
523
524 assert!(needs_migration);
525 assert_eq!(
526 deserialized.chain_key.public().to_address().to_string(),
527 "0x826a1bf3d51fa7f402a1e01d1b2c8a8bac28e666"
528 );
529
530 Ok(())
531 }
532
533 #[test]
534 fn test_auto_migration() -> anyhow::Result<()> {
535 let tmp = tempdir()?;
536 let identity_dir = tmp.path().join("hopr-unit-test-identity");
537 let identity_path: &str = identity_dir.to_str().context("should be convertible to string")?;
538
539 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"}}"#;
540 fs::write(identity_path, old_keystore_file.as_bytes()).unwrap();
541
542 assert!(
543 super::HoprKeys::init(super::IdentityRetrievalModes::FromFile {
544 password: "local",
545 id_path: identity_path
546 })
547 .is_ok()
548 );
549
550 let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(identity_path, "local").unwrap();
551
552 assert!(!needs_migration);
553 assert_eq!(
554 deserialized.chain_key.public().to_address().to_string(),
555 "0x826a1bf3d51fa7f402a1e01d1b2c8a8bac28e666"
556 );
557
558 Ok(())
559 }
560
561 #[test]
562 fn test_should_not_overwrite_existing() -> anyhow::Result<()> {
563 let tmp = tempdir()?;
564 let identity_dir = tmp.path().join("hopr-unit-test-identity");
565 let identity_path: &str = identity_dir.to_str().context("should be convertible to string")?;
566
567 fs::write(identity_path, "".as_bytes())?;
568
569 assert!(
571 super::HoprKeys::init(super::IdentityRetrievalModes::FromFile {
572 password: "local",
573 id_path: identity_path
574 })
575 .is_err()
576 );
577
578 Ok(())
579 }
580
581 #[test]
582 fn test_from_privatekey() {
583 let private_key = "0x56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
584
585 let from_private_key = HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey { private_key }).unwrap();
586
587 let private_key_without_prefix = "56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
588
589 let from_private_key_without_prefix = HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
590 private_key: private_key_without_prefix,
591 })
592 .unwrap();
593
594 assert_eq!(from_private_key, from_private_key_without_prefix);
596
597 let too_short_private_key = "0xb29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52ae";
598
599 assert!(
600 HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
601 private_key: too_short_private_key
602 })
603 .is_err()
604 );
605
606 let too_long_private_key = "0x56b29cefcdf576eea306ba2fd5f32e651c09e0abbc018c47bdc6ef44f6b7506f1050f95137770478f50b456267f761f1b8b341a13da68bc32e5c96984fcd52aeae";
607
608 assert!(
609 HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
610 private_key: too_long_private_key
611 })
612 .is_err()
613 );
614
615 let non_hex_private = "this is the story of hopr: in 2018 ...";
616 assert!(
617 HoprKeys::init(super::IdentityRetrievalModes::FromPrivateKey {
618 private_key: non_hex_private
619 })
620 .is_err()
621 );
622 }
623
624 #[test]
625 fn test_from_privatekey_into_file() -> anyhow::Result<()> {
626 let tmp = tempdir()?;
627 let identity_dir = tmp.path().join("hopr-unit-test-identity");
628 let identity_path = identity_dir.to_str().context("should be convertible to string")?;
629 let id = Uuid::new_v4();
630
631 let keys = HoprKeys::init(super::IdentityRetrievalModes::FromIdIntoFile {
632 password: "local",
633 id_path: identity_path,
634 id,
635 })
636 .expect("should initialize new key");
637
638 let (deserialized, needs_migration) = super::HoprKeys::read_eth_keystore(identity_path, "local").unwrap();
639
640 assert!(!needs_migration);
641 assert_eq!(
642 deserialized.chain_key.public().to_address(),
643 keys.chain_key.public().to_address()
644 );
645
646 assert!(
648 super::HoprKeys::init(super::IdentityRetrievalModes::FromIdIntoFile {
649 password: "local",
650 id_path: identity_path,
651 id
652 })
653 .is_err()
654 );
655
656 Ok(())
657 }
658}