hopli/
key_pair.rs

1//! This module contains struct definition,  utility functions around private keys, password, and keystores.
2//!
3//! Keystore file is often referred as HOPR node identity file, which is an encrypted private key for an Ethereum
4//! wallet. This identity file uses password (received from [PasswordArgs]) for encryption.
5//!
6//! Location of identity files can be provided with [IdentityFileArgs].
7//!
8//! This module also contains definition of argument for private key, defined in [PrivateKeyArgs].
9
10use std::{
11    collections::HashMap,
12    env, fs,
13    path::{Path, PathBuf},
14};
15
16use clap::{Parser, ValueHint};
17use hopr_crypto_types::keypairs::{ChainKeypair, Keypair};
18use hopr_primitive_types::primitives::Address;
19use hoprd_keypair::key_pair::{HoprKeys, IdentityRetrievalModes};
20use tracing::{debug, error, info, warn};
21use uuid::Uuid;
22
23use crate::utils::HelperErrors;
24
25pub fn read_identity(file: &Path, password: &str) -> Result<(String, HoprKeys), HelperErrors> {
26    let file_str = file
27        .to_str()
28        .ok_or(HelperErrors::IncorrectFilename(file.to_string_lossy().to_string()))?;
29
30    match HoprKeys::read_eth_keystore(file_str, password) {
31        Ok((keys, needs_migration)) => {
32            if needs_migration {
33                keys.write_eth_keystore(file_str, password)
34                    .map_err(HelperErrors::KeyStoreError)?;
35            }
36            let file_key = file
37                .file_name()
38                .ok_or(HelperErrors::ParseError("Failed to extract file name".into()))?;
39            Ok((
40                String::from(
41                    file_key
42                        .to_str()
43                        .ok_or(HelperErrors::ParseError("Failed to extract file key".into()))?,
44                ),
45                keys,
46            ))
47        }
48        Err(e) => {
49            error!("Could not decrypt keystore file at {}. {}", file_str, e.to_string());
50            Err(HelperErrors::UnableToReadIdentity)
51        }
52    }
53}
54
55/// Decrypts identity files and returns an vec of PeerIds and Ethereum Addresses
56///
57/// # Arguments
58///
59/// * `files` - Paths to identity files
60/// * `password` - Password to unlock all the identity files
61pub fn read_identities(files: Vec<PathBuf>, password: &str) -> Result<HashMap<String, HoprKeys>, HelperErrors> {
62    let mut results: HashMap<String, HoprKeys> = HashMap::with_capacity(files.len());
63
64    for file in files.iter() {
65        let id_path = file
66            .to_str()
67            .ok_or(HelperErrors::IncorrectFilename(file.to_string_lossy().to_string()))?;
68
69        match HoprKeys::try_from(IdentityRetrievalModes::FromFile { password, id_path }) {
70            Ok(keys) => {
71                results.insert(id_path.into(), keys);
72            }
73            Err(e) => {
74                warn!("Could not read keystore file at {} due to {}", id_path, e.to_string())
75            }
76        }
77    }
78
79    Ok(results)
80}
81
82/// encrypt HoprKeys with a new password to an identity file
83pub fn update_identity_password(
84    keys: HoprKeys,
85    path: &Path,
86    password: &str,
87) -> Result<(String, HoprKeys), HelperErrors> {
88    let file_path = path
89        .to_str()
90        .ok_or(HelperErrors::IncorrectFilename(path.to_string_lossy().to_string()))?;
91
92    if path.exists() && path.is_file() && file_path.ends_with(".id") {
93        // insert remove actual file with name `file_path`
94        fs::remove_file(file_path).map_err(|_err| HelperErrors::UnableToUpdateIdentityPassword)?;
95        keys.write_eth_keystore(file_path, password)?;
96        Ok((String::from(file_path), keys))
97    } else {
98        warn!(
99            "Could not update keystore file at {}. {}",
100            file_path, "File name does not end with `.id`"
101        );
102        Err(HelperErrors::UnableToUpdateIdentityPassword)
103    }
104}
105
106/// Create one identity file and return the ethereum address
107///
108/// # Arguments
109///
110/// * `dir_name` - Directory to the storage of an identity file
111/// * `password` - Password to encrypt the identity file
112/// * `name` - Prefix of identity files.
113pub fn create_identity(
114    dir_name: &str,
115    password: &str,
116    maybe_name: &Option<String>,
117) -> Result<(String, HoprKeys), HelperErrors> {
118    // create dir if not exist
119    fs::create_dir_all(dir_name)?;
120
121    let id = Uuid::new_v4();
122
123    // check if `name` is end with `.id`, if not, append it
124    let file_name = match maybe_name {
125        Some(name) => {
126            // check if ending with `.id`
127            if name.ends_with(".id") {
128                name.to_owned()
129            } else {
130                format!("{name}.id")
131            }
132        }
133        // if none is specified, use UUID
134        None => format!("{}.id", { id.to_string() }),
135    };
136
137    let mut file_path = PathBuf::from(dir_name);
138
139    // add filename, depending on platform
140    file_path.push(file_name);
141
142    let file_path_str = file_path
143        .to_str()
144        .ok_or(HelperErrors::IncorrectFilename(file_path.to_string_lossy().to_string()))?;
145
146    Ok((
147        file_path_str.into(),
148        IdentityRetrievalModes::FromIdIntoFile {
149            id,
150            password,
151            id_path: file_path_str,
152        }
153        .try_into()?,
154    ))
155}
156
157pub trait ArgEnvReader<T, K> {
158    /// return the wrapped key
159    fn get_key(&self) -> Option<K>;
160
161    /// Try to read the value from the cli param (given by the key), or read from the env variable
162    fn read(&self, default_env_name: &str) -> Result<T, HelperErrors>;
163
164    /// Read the private key with a default env value and return an address string
165    fn read_default(&self) -> Result<T, HelperErrors>;
166}
167
168/// Arguments for private key.
169#[derive(Debug, Clone, Parser, Default)]
170pub struct PrivateKeyArgs {
171    /// Either provide a private key as argument or as a specific environment variable, e.g. `PRIVATE_KEY`,
172    /// `MANAGER_PRIVATE_KEY`
173    #[clap(
174        long,
175        short = 'k',
176        help = "Private key to unlock the account that broadcasts the transaction",
177        name = "private_key",
178        value_name = "PRIVATE_KEY"
179    )]
180    pub private_key: Option<String>,
181}
182
183impl ArgEnvReader<ChainKeypair, String> for PrivateKeyArgs {
184    /// Return the wrapped key. cli arg: --private-key
185    fn get_key(&self) -> Option<String> {
186        self.private_key.to_owned()
187    }
188
189    /// Read the value from either the cli arg or env
190    fn read(&self, default_env_name: &str) -> Result<ChainKeypair, HelperErrors> {
191        let pri_key = if let Some(pk) = self.get_key() {
192            info!("Reading private key from CLI");
193            pk
194        } else if let Ok(env_pk) = env::var(default_env_name) {
195            info!("Reading private key from environment variable {:?}", default_env_name);
196            env_pk
197        } else if let Ok(prompt_pk) = rpassword::prompt_password("Enter private key:") {
198            info!("Reading private key from prompt");
199            prompt_pk
200        } else {
201            error!(
202                "Unable to read private key from environment variable: {:?}",
203                default_env_name
204            );
205            return Err(HelperErrors::UnableToReadPrivateKey(default_env_name.into()));
206        };
207
208        // trim the 0x prefix if needed
209        let priv_key_without_prefix = pri_key.strip_prefix("0x").unwrap_or(&pri_key).to_string();
210
211        let decoded_key = hex::decode(priv_key_without_prefix)
212            .map_err(|e| HelperErrors::UnableToReadPrivateKey(format!("Failed to decode private key: {:?}", e)))?;
213        ChainKeypair::from_secret(&decoded_key)
214            .map_err(|e| HelperErrors::UnableToReadPrivateKey(format!("Failed to create keypair: {:?}", e)))
215    }
216
217    /// Read the default private key and return an address string
218    fn read_default(&self) -> Result<ChainKeypair, HelperErrors> {
219        self.read("PRIVATE_KEY")
220    }
221}
222
223/// Arguments for private key.
224#[derive(Debug, Clone, Parser, Default)]
225pub struct ManagerPrivateKeyArgs {
226    /// Either provide a private key as argument or as a specific environment variable, e.g. `PRIVATE_KEY`,
227    /// `MANAGER_PRIVATE_KEY`
228    #[clap(
229        long,
230        short = 'q',
231        help = "Private key to unlock the account with privilege that broadcasts the transaction",
232        name = "manager_private_key",
233        value_name = "MANAGER_PRIVATE_KEY"
234    )]
235    pub manager_private_key: Option<String>,
236}
237
238impl ArgEnvReader<ChainKeypair, String> for ManagerPrivateKeyArgs {
239    /// Return the wrapped key. cli arg: --manager-private-key
240    fn get_key(&self) -> Option<String> {
241        self.manager_private_key.to_owned()
242    }
243
244    /// Read the value from either the cli arg or env
245    fn read(&self, default_env_name: &str) -> Result<ChainKeypair, HelperErrors> {
246        let pri_key = if let Some(pk) = self.get_key() {
247            info!("Reading manager private key from CLI");
248            pk
249        } else if let Ok(env_pk) = env::var(default_env_name) {
250            info!(
251                "Reading manager private key from environment variable {:?}",
252                default_env_name
253            );
254            env_pk
255        } else if let Ok(prompt_pk) = rpassword::prompt_password("Enter manager private key:") {
256            info!("Reading manager private key from prompt");
257            prompt_pk
258        } else {
259            error!(
260                "Unable to read private key from environment variable: {:?}",
261                default_env_name
262            );
263            return Err(HelperErrors::UnableToReadPrivateKey(default_env_name.into()));
264        };
265
266        // trim the 0x prefix if needed
267        let priv_key_without_prefix = pri_key.strip_prefix("0x").unwrap_or(&pri_key).to_string();
268        let decoded_key = hex::decode(priv_key_without_prefix)
269            .map_err(|e| HelperErrors::UnableToReadPrivateKey(format!("Failed to decode private key: {:?}", e)))?;
270        ChainKeypair::from_secret(&decoded_key)
271            .map_err(|e| HelperErrors::UnableToReadPrivateKey(format!("Failed to create keypair: {:?}", e)))
272    }
273
274    /// Read the default private key and return an address string
275    fn read_default(&self) -> Result<ChainKeypair, HelperErrors> {
276        self.read("MANAGER_PRIVATE_KEY")
277    }
278}
279
280/// Arguments for password.
281///
282/// Password is used for encrypting an identity file
283/// Password can be passed as an environment variable `IDENTITY_PASSWORD`, or
284/// in a file of which the path is supplied in `--password_path`
285#[derive(Debug, Clone, Parser, Default)]
286pub struct PasswordArgs {
287    /// The path to a file containing the password that encrypts the identity file
288    #[clap(
289        short,
290        long,
291        help = "The path to read the password. If not specified, use the IDENTITY_PASSWORD environment variable.",
292        value_hint = ValueHint::FilePath,
293        name = "password_path",
294        value_name = "PASSWORD_PATH"
295    )]
296    pub password_path: Option<PathBuf>,
297}
298
299impl ArgEnvReader<String, PathBuf> for PasswordArgs {
300    /// Return the wrapped key. cli arg: --password-path
301    fn get_key(&self) -> Option<PathBuf> {
302        self.password_path.clone()
303    }
304
305    /// Read the value from either the cli arg or env
306    fn read(&self, default_env_name: &str) -> Result<String, HelperErrors> {
307        let pwd = if let Some(pwd_path) = self.get_key() {
308            info!("reading password from cli");
309            fs::read_to_string(pwd_path).map_err(HelperErrors::UnableToReadFromPath)?
310        } else {
311            info!("reading password from env {:?}", default_env_name);
312            env::var(default_env_name).map_err(|_| HelperErrors::UnableToReadPassword)?
313        };
314
315        Ok(pwd)
316    }
317
318    /// Read the default private key and return an address string
319    fn read_default(&self) -> Result<String, HelperErrors> {
320        self.read("IDENTITY_PASSWORD")
321    }
322}
323
324/// Arguments for new password.
325///
326/// Password is used for encrypting an identity file
327/// Password can be passed as an environment variable `NEW_IDENTITY_PASSWORD`, or
328/// in a file of which the path is supplied in `--new_password_path`
329#[derive(Debug, Clone, Parser, Default)]
330pub struct NewPasswordArgs {
331    /// The path to a file containing the password that encrypts the identity file
332    #[clap(
333        short,
334        long,
335        help = "The path to read the new password. If not specified, use the NEW_IDENTITY_PASSWORD environment variable.",
336        value_hint = ValueHint::FilePath,
337        name = "new_password_path",
338        value_name = "NEW_IDENTITY_PASSWORD"
339    )]
340    pub new_password_path: Option<PathBuf>,
341}
342
343impl ArgEnvReader<String, PathBuf> for NewPasswordArgs {
344    /// Return the wrapped key. cli arg: --new-password-path
345    fn get_key(&self) -> Option<PathBuf> {
346        self.new_password_path.clone()
347    }
348
349    /// Read the value from either the cli arg or env
350    fn read(&self, default_env_name: &str) -> Result<String, HelperErrors> {
351        let pwd = if let Some(pwd_path) = self.get_key() {
352            info!("reading password from cli");
353            fs::read_to_string(pwd_path).map_err(HelperErrors::UnableToReadFromPath)?
354        } else {
355            info!("reading password from env {:?}", default_env_name);
356            env::var(default_env_name).map_err(|_| HelperErrors::UnableToReadPassword)?
357        };
358
359        Ok(pwd)
360    }
361
362    /// Read the default private key and return an address string
363    fn read_default(&self) -> Result<String, HelperErrors> {
364        self.read("NEW_IDENTITY_PASSWORD")
365    }
366}
367
368/// CLI arguments to specify the directory of one or multiple identity files
369#[derive(Debug, Clone, Parser, Default)]
370pub struct IdentityFromDirectoryArgs {
371    /// Directory to all the identity files
372    #[clap(
373        help = "Path to the directory that stores identity files",
374        long,
375        short = 'd',
376        value_hint = ValueHint::DirPath,
377        required = false
378    )]
379    pub identity_directory: Option<String>,
380
381    /// Prefix of identity files. Only identity files with the provided are decrypted with the password
382    #[clap(
383        help = "Only use identity files with prefix",
384        long,
385        short = 'x',
386        default_value = None,
387        required = false
388    )]
389    pub identity_prefix: Option<String>,
390}
391
392impl IdentityFromDirectoryArgs {
393    /// read files from given directory or file path
394    pub fn get_files_from_directory(self) -> Result<Vec<PathBuf>, HelperErrors> {
395        let IdentityFromDirectoryArgs {
396            identity_directory,
397            identity_prefix,
398        } = self;
399        let id_dir = identity_directory.ok_or(HelperErrors::MissingIdentityDirectory)?;
400        debug!(target: "identity_reader_from_directory", "Reading dir {}", &id_dir);
401
402        // early return if failed in reading identity directory
403        let directory = fs::read_dir(Path::new(&id_dir))?;
404        // read all the files from the directory that contains
405        // 1) "id" in its name
406        // 2) the provided idetity_prefix
407        let files: Vec<PathBuf> = directory
408            .into_iter() // read all the files from the directory
409            .filter_map(|r| r.ok())
410            .map(|r| r.path()) // Read all the files from the given directory
411            .filter(|r| r.is_file() && r.to_str().unwrap().contains("id")) // Filter out folders
412            .filter(|r| match &identity_prefix {
413                Some(id_prf) => r.file_stem().unwrap().to_str().unwrap().starts_with(id_prf.as_str()),
414                _ => true,
415            })
416            .collect();
417        info!(target: "identity_reader_from_directory", "{} path read from dir", &files.len());
418        Ok(files)
419    }
420}
421
422/// CLI arguments to specify the directory of one or multiple identity files
423#[derive(Debug, Clone, Parser, Default)]
424pub struct IdentityFileArgs {
425    /// Directory that contains one or multiple identity files
426    #[clap(help = "Get identity file(s) from a directory", flatten)]
427    pub identity_from_directory: Option<IdentityFromDirectoryArgs>,
428
429    /// Path to one identity file
430    #[clap(
431        short,
432        long,
433        help = "The path to an identity file",
434        value_hint = ValueHint::FilePath,
435        name = "identity_from_path"
436    )]
437    pub identity_from_path: Option<PathBuf>,
438
439    /// Password to encrypt identity file(s)
440    #[clap(help = "Password for the identit(ies)", flatten)]
441    pub password: PasswordArgs,
442}
443
444impl IdentityFileArgs {
445    /// read identity files from given directory or file path
446    pub fn get_files(self) -> Result<Vec<PathBuf>, HelperErrors> {
447        let IdentityFileArgs {
448            identity_from_directory,
449            identity_from_path,
450            ..
451        } = self;
452        debug!(target: "identity_location_reader", "Read from dir {}, path {}", &identity_from_directory.is_some(), &identity_from_path.is_some());
453
454        let mut files: Vec<PathBuf> = Vec::new();
455        if let Some(id_dir_args) = identity_from_directory {
456            files = id_dir_args.get_files_from_directory()?;
457        };
458        if let Some(id_path) = identity_from_path {
459            debug!(target: "identity_location_reader", "Reading path {}", &id_path.as_path().display().to_string());
460            if id_path.exists() {
461                files.push(id_path);
462                info!(target: "identity_location_reader", "path read from path");
463            } else {
464                error!(target: "identity_location_reader",  "Path {} does not exist.", &id_path.as_path().display().to_string());
465            }
466        }
467        Ok(files)
468    }
469
470    /// read identity files and return their Ethereum addresses
471    pub fn to_addresses(self) -> Result<Vec<Address>, HelperErrors> {
472        let files = self.clone().get_files()?;
473
474        // get Ethereum addresses from identity files
475        if !files.is_empty() {
476            // check if password is provided
477            let pwd = self.password.read_default()?;
478
479            // read all the identities from the directory
480            Ok(read_identities(files, &pwd)?
481                .values()
482                .map(|ni| ni.chain_key.public().0.to_address())
483                .collect())
484        } else {
485            Ok(Vec::<Address>::new())
486        }
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use anyhow::Context;
493    use tempfile::tempdir;
494
495    use super::*;
496
497    const DUMMY_PRIVATE_KEY: &str = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
498    const SPECIAL_ENV_KEY: &str = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
499
500    #[test]
501    fn read_pk_with_0x() -> anyhow::Result<()> {
502        let private_key_args = PrivateKeyArgs {
503            private_key: Some("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string()),
504        };
505        let key = private_key_args.read_default()?;
506
507        let ref_decoded_value = hex::decode(DUMMY_PRIVATE_KEY)?;
508        println!("ref_decoded_value {:?}", ref_decoded_value);
509
510        assert_eq!(
511            key.public().to_address().to_checksum(),
512            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
513            "cannot read private key with 0x prefix"
514        );
515        Ok(())
516    }
517
518    #[test]
519    fn create_identities_from_directory_with_id_files() -> anyhow::Result<()> {
520        let _ = env_logger::builder().is_test(true).try_init();
521        let tmp = tempdir()?;
522
523        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
524        let pwd = "password_create";
525        assert!(create_identity(path, pwd, &Some(String::from("node1"))).is_ok());
526        Ok(())
527    }
528
529    #[test]
530    fn read_identity_from_path() -> anyhow::Result<()> {
531        let _ = env_logger::builder().is_test(true).try_init();
532        let tmp = tempdir()?;
533
534        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
535        let pwd = "password";
536        let (_, created_id) = create_identity(path, pwd, &None)?;
537
538        // created and the read id is identical
539        let files = get_files(path, &None);
540        assert_eq!(files.len(), 1, "must have one identity file");
541
542        let read_id = read_identity(files[0].as_path(), pwd)?;
543        assert_eq!(
544            read_id.1.chain_key.public().0.to_address(),
545            created_id.chain_key.public().0.to_address()
546        );
547        Ok(())
548    }
549
550    #[test]
551    fn update_identity_password_at_path() -> anyhow::Result<()> {
552        let _ = env_logger::builder().is_test(true).try_init();
553        let tmp = tempdir()?;
554
555        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
556        let pwd = "password";
557        let (_, created_id) = create_identity(path, pwd, &None)?;
558
559        // created and the read id is identical
560        let files = get_files(path, &None);
561        assert_eq!(files.len(), 1, "must have one identity file");
562        let address = created_id.chain_key.public().0.to_address();
563
564        let new_pwd = "supersecured";
565        let (_, returned_key) = update_identity_password(created_id, files[0].as_path(), new_pwd)?;
566
567        // check the returned value
568        assert_eq!(
569            returned_key.chain_key.public().0.to_address(),
570            address,
571            "returned keys are identical"
572        );
573
574        // check the read value
575        let (_, read_id) = read_identity(files[0].as_path(), new_pwd)?;
576        assert_eq!(
577            read_id.chain_key.public().0.to_address(),
578            address,
579            "cannot use the new password to read files"
580        );
581        Ok(())
582    }
583
584    #[test]
585    fn read_identities_from_directory_with_id_files() -> anyhow::Result<()> {
586        let _ = env_logger::builder().is_test(true).try_init();
587        let tmp = tempdir()?;
588
589        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
590        let pwd = "password";
591        let (_, created_id) = create_identity(path, pwd, &None)?;
592
593        // created and the read id is identical
594        let files = get_files(path, &None);
595        let read_id = read_identities(files, pwd)?;
596        assert_eq!(read_id.len(), 1);
597        assert_eq!(
598            read_id.values().next().unwrap().chain_key.public().0.to_address(),
599            created_id.chain_key.public().0.to_address()
600        );
601
602        // print the read id
603        debug!("Debug {:#?}", read_id);
604        debug!("Display {}", read_id.values().next().unwrap());
605        Ok(())
606    }
607
608    #[test]
609    fn read_identities_from_directory_with_id_files_but_wrong_password() -> anyhow::Result<()> {
610        let _ = env_logger::builder().is_test(true).try_init();
611        let tmp = tempdir()?;
612
613        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
614        let pwd = "password";
615        let wrong_pwd = "wrong_password";
616        create_identity(path, pwd, &None)?;
617        let files = get_files(path, &None);
618        assert_eq!(read_identities(files, wrong_pwd)?.len(), 0);
619
620        Ok(())
621    }
622
623    #[test]
624    fn read_identities_from_directory_without_id_files() -> anyhow::Result<()> {
625        let tmp = tempdir()?;
626
627        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
628        let files = get_files(path, &None);
629        assert_eq!(read_identities(files, "")?.len(), 0);
630
631        Ok(())
632    }
633
634    #[test]
635    fn read_identities_from_tmp_folder() -> anyhow::Result<()> {
636        let _ = env_logger::builder().is_test(true).try_init();
637        let tmp = tempdir()?;
638
639        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
640        let pwd = "local";
641        create_identity(path, pwd, &Some("local-alice".into()))?;
642        let files = get_files(path, &None);
643        assert_eq!(read_identities(files, pwd)?.len(), 1);
644
645        Ok(())
646    }
647
648    #[test]
649    fn read_identities_from_tmp_folder_with_prefix() -> anyhow::Result<()> {
650        let _ = env_logger::builder().is_test(true).try_init();
651        let tmp = tempdir()?;
652
653        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
654        let pwd = "local";
655        create_identity(path, pwd, &Some("local-alice".into()))?;
656        let files = get_files(path, &Some("local".to_string()));
657        assert_eq!(read_identities(files, pwd)?.len(), 1);
658        Ok(())
659    }
660
661    #[test]
662    fn read_identities_from_tmp_folder_no_match() -> anyhow::Result<()> {
663        let _ = env_logger::builder().is_test(true).try_init();
664        let tmp = tempdir()?;
665
666        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
667        let pwd = "local";
668        create_identity(path, pwd, &Some("local-alice".into()))?;
669        let files = get_files(path, &Some("npm-".to_string()));
670        assert_eq!(read_identities(files, pwd)?.len(), 0);
671
672        Ok(())
673    }
674
675    #[test]
676    fn read_identities_from_tmp_folder_with_wrong_prefix() -> anyhow::Result<()> {
677        let _ = env_logger::builder().is_test(true).try_init();
678        let tmp = tempdir()?;
679
680        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
681        let pwd = "local";
682        create_identity(path, pwd, &Some("local-alice".into()))?;
683
684        let files = get_files(path, &Some("alice".to_string()));
685        assert_eq!(read_identities(files, pwd)?.len(), 0);
686
687        Ok(())
688    }
689
690    #[test]
691    fn read_complete_identities_from_tmp_folder() -> anyhow::Result<()> {
692        let _ = env_logger::builder().is_test(true).try_init();
693        let tmp = tempdir()?;
694
695        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
696        let name = "alice.id";
697        let pwd = "e2e-test";
698
699        let weak_crypto_alice_keystore = r#"{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"6084fab56497402930d0833fbc17e7ea"},"ciphertext":"50c0cf2537d7bc0ab6dbb7909d21d3da6445e5bd2cb1236de7efbab33302ddf1dd6a0393c986f8c111fe73a22f36af88858d79d23882a5f991713cb798172069d060f28c680afc28743e8842e8e849ebc21209825e23465afcee52a49f9c4f6734061f91a45b4cc8fbd6b4c95cc4c1b487f0007ed88a1b46b5ebdda616013b3f7ba465f97352b9412e69e6690cee0330c0b25bcf5fc3cdf12e4167336997920df9d6b7d816943ab3817481b9","kdf":"scrypt","kdfparams":{"dklen":32,"n":2,"p":1,"r":8,"salt":"46e30c2d74ba04b881e99fb276ae6a970974499f6abe286a00a69ba774ace095"},"mac":"70dccb366e8ddde13ebeef9a6f35bbc1333176cff3d33a72c925ce23753b34f4"},"id":"b5babdf4-da20-4cc1-9484-58ea24f1b3ae","version":3}"#;
700        // let alice_peer_id = "16Uiu2HAmUYnGY3USo8iy13SBFW7m5BMQvC4NETu1fGTdoB86piw7";
701        let alice_address = "0x838d3c1d2ff5c576d7b270aaaaaa67e619217aac";
702
703        // create dir if not exist.
704        fs::create_dir_all(path)?;
705        // save the keystore as file
706        fs::write(PathBuf::from(path).join(name), weak_crypto_alice_keystore.as_bytes())?;
707
708        let files = get_files(path, &None);
709        let val = read_identities(files, pwd)?;
710        assert_eq!(val.len(), 1);
711        assert_eq!(
712            val.values()
713                .next()
714                .unwrap()
715                .chain_key
716                .public()
717                .0
718                .to_address()
719                .to_string(),
720            alice_address
721        );
722        Ok(())
723    }
724
725    fn get_files(identity_directory: &str, identity_prefix: &Option<String>) -> Vec<PathBuf> {
726        // early return if failed in reading identity directory
727        let directory = fs::read_dir(Path::new(identity_directory))
728            .unwrap_or_else(|_| panic!("cannot read directory {}", identity_directory));
729
730        // read all the files from the directory that contains
731        // 1) "id" in its name
732        // 2) the provided idetity_prefix
733        let files: Vec<PathBuf> = directory
734            .into_iter() // read all the files from the directory
735            .filter(|r| r.is_ok()) // Get rid of Err variants for Result<DirEntry>
736            .map(|r| r.unwrap().path()) // Read all the files from the given directory
737            .filter(|r| r.is_file()) // Filter out folders
738            .filter(|r| r.to_str().unwrap().contains("id")) // file name should contain "id"
739            .filter(|r| match &identity_prefix {
740                Some(identity_prefix) => r
741                    .file_stem()
742                    .unwrap()
743                    .to_str()
744                    .unwrap()
745                    .starts_with(identity_prefix.as_str()),
746                _ => true,
747            })
748            .collect();
749        files
750    }
751
752    #[test]
753    fn private_key_args_can_read_env_or_cli_args_in_different_scenarios() {
754        // possible private key args
755        let pk_args_none = PrivateKeyArgs { private_key: None };
756        let pk_args_some = PrivateKeyArgs {
757            private_key: Some(DUMMY_PRIVATE_KEY.into()),
758        };
759
760        // when a special env is set but no cli arg, it returns the special env value
761        env::set_var("MANAGER_PK", SPECIAL_ENV_KEY);
762        if let Ok(kp_0) = pk_args_none.clone().read("MANAGER_PK") {
763            assert_eq!(
764                SPECIAL_ENV_KEY,
765                hex::encode(kp_0.secret().as_ref()),
766                "read a wrong private key from env with a special name"
767            );
768        } else {
769            panic!("cannot read private key from env when no cli arg is provied");
770        }
771
772        // when env is set but no cli arg, it returns the env value
773        env::set_var("PRIVATE_KEY", DUMMY_PRIVATE_KEY);
774        if let Ok(kp_1) = pk_args_none.clone().read_default() {
775            assert_eq!(
776                DUMMY_PRIVATE_KEY,
777                hex::encode(kp_1.secret().as_ref()),
778                "read a wrong private key from env"
779            );
780        } else {
781            panic!("cannot read private key from env when no cli arg is provied");
782        }
783
784        // when both env and cli args are set, it still uses cli
785        env::set_var("PRIVATE_KEY", "0123");
786        if let Ok(kp_2) = pk_args_some.clone().read_default() {
787            assert_eq!(
788                DUMMY_PRIVATE_KEY,
789                hex::encode(kp_2.secret().as_ref()),
790                "read a wrong private key from cli"
791            );
792        } else {
793            panic!("cannot read private key from cli when both are provied");
794        }
795
796        // when no env and no cli arg, it spawns an interactive CLI
797        env::remove_var("PRIVATE_KEY");
798
799        // when no env is supplied, but private key is supplied
800        if let Ok(kp_3) = pk_args_some.read_default() {
801            assert_eq!(
802                DUMMY_PRIVATE_KEY,
803                hex::encode(kp_3.secret().as_ref()),
804                "read a wrong private key from env"
805            );
806        } else {
807            panic!("cannot read private key from env when no cli arg is provied nor env is set");
808        }
809
810        // when no env and no cli arg, it spawns an interactive CLI and inputs
811        // "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
812        // the test is commented out as there's no way to simulate the write in the interactive CLI,
813        // unless directly mocking the buffer read.
814        // if let Ok(kp_3) = pk_args_none.read_default() {
815        //     assert_eq!(
816        //         DUMMY_PRIVATE_KEY,
817        //         hex::encode(kp_3.secret().as_ref()),
818        //         "read a wrong private key from env"
819        //     );
820        // } else {
821        //     panic!("cannot read private key from env when no cli arg is provied nor env is set");
822        // }
823    }
824
825    #[test]
826    fn password_args_can_read_env_or_cli_args_in_different_scenarios() -> anyhow::Result<()> {
827        let tmp = tempdir()?;
828        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
829        create_file(path, None, 2)?;
830
831        // possible password args
832        let pwd_args_some = PasswordArgs {
833            password_path: Some(PathBuf::from(path).join("fileid1")),
834        };
835        let pwd_args_none = PasswordArgs { password_path: None };
836        let new_pwd_args_some = NewPasswordArgs {
837            new_password_path: Some(PathBuf::from(path).join("fileid2")),
838        };
839        let new_pwd_args_none = NewPasswordArgs {
840            new_password_path: None,
841        };
842        // let file_path = PathBuf::from(path).join("fileid2");
843        fs::write(PathBuf::from(path).join("fileid2"), "supersound")?;
844
845        // test new_password_path
846        env::set_var("NEW_IDENTITY_PASSWORD", "ultraviolet");
847        if let Ok(pwd_0) = new_pwd_args_some.read_default() {
848            assert_eq!(
849                pwd_0,
850                "supersound".to_string(),
851                "read a wrong password from path in new_password_path"
852            );
853        } else {
854            panic!("cannot read new password from path");
855        }
856        if let Ok(pwd_1) = new_pwd_args_none.read_default() {
857            assert_eq!(
858                pwd_1,
859                "ultraviolet".to_string(),
860                "read a wrong password from cli in new_password_path"
861            );
862        } else {
863            panic!("cannot read new password from path");
864        }
865
866        env::set_var("IDENTITY_PASSWORD", "Hello");
867        // fail to take cli password path when both cli arg and env are supplied
868        if let Ok(kp_1) = pwd_args_some.clone().read_default() {
869            assert_eq!(kp_1, "Hello".to_string(), "read a wrong password from env");
870        } else {
871            panic!("cannot read password from env when cli arg is also provied");
872        }
873        // ok when no password path is supplied but env is supplied
874        if let Ok(kp_2) = pwd_args_none.clone().read_default() {
875            assert_eq!(kp_2, "Hello".to_string(), "read a wrong password from env");
876        } else {
877            panic!("cannot read password from env when no cli arg is provied");
878        }
879
880        // revert when no password path or identity password env is supplied
881        env::remove_var("IDENTITY_PASSWORD");
882        assert!(pwd_args_none.read_default().is_err());
883
884        // ok when no env is supplied but password path is supplied
885        if let Ok(kp_3) = pwd_args_some.clone().read_default() {
886            assert_eq!(kp_3, "Hello".to_string(), "read a wrong password from path");
887        } else {
888            panic!("cannot read password from path when no env is provied");
889        }
890        Ok(())
891    }
892
893    #[test]
894    fn revert_get_dir_from_non_existing_dir() {
895        let path = "./tmp_non_exist";
896
897        let dir_args = IdentityFromDirectoryArgs {
898            identity_directory: Some(path.to_string()),
899            identity_prefix: None,
900        };
901
902        assert!(dir_args.get_files_from_directory().is_err());
903    }
904
905    #[test]
906    fn pass_get_empty_dir_from_existing_dir() -> anyhow::Result<()> {
907        let tmp = tempdir()?;
908        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
909        create_file(path, None, 0)?;
910
911        let dir_args = IdentityFromDirectoryArgs {
912            identity_directory: Some(path.to_string()),
913            identity_prefix: None,
914        };
915
916        if let Ok(vp) = dir_args.get_files_from_directory() {
917            assert!(vp.is_empty())
918        } else {
919            panic!("failed to revert when the path contains no file")
920        }
921        Ok(())
922    }
923
924    #[test]
925    fn pass_get_dir_from_existing_dir() -> anyhow::Result<()> {
926        let tmp = tempdir()?;
927        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
928        create_file(path, None, 4)?;
929
930        let dir_args = IdentityFromDirectoryArgs {
931            identity_directory: Some(path.to_string()),
932            identity_prefix: None,
933        };
934
935        if let Ok(vp) = dir_args.get_files_from_directory() {
936            assert_eq!(4, vp.len())
937        } else {
938            panic!("failed to get files")
939        }
940        Ok(())
941    }
942
943    #[test]
944    fn pass_get_path_from_existing_path() -> anyhow::Result<()> {
945        let tmp = tempdir()?;
946        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
947        create_file(path, None, 4)?;
948
949        let id_path = PathBuf::from(format!("{path}/fileid1"));
950        let path_args: IdentityFileArgs = IdentityFileArgs {
951            identity_from_directory: None,
952            identity_from_path: Some(id_path),
953            password: PasswordArgs { password_path: None },
954        };
955
956        let vp = path_args.get_files()?;
957        assert_eq!(1, vp.len());
958        Ok(())
959    }
960
961    #[test]
962    fn pass_get_files_from_directory_and_path() -> anyhow::Result<()> {
963        // an path to file
964        let tmp_file = tempdir()?;
965        let path_file = tmp_file.path().to_str().unwrap();
966        create_file(path_file, None, 4)?;
967        let id_path = PathBuf::from(format!("{path_file}/fileid1"));
968
969        // a dir for files
970        let tmp = tempdir()?;
971        let path = tmp.path().to_str().context("should produce a valid tmp path string")?;
972        create_file(path, None, 4)?;
973
974        let dir_args = IdentityFromDirectoryArgs {
975            identity_directory: Some(path.to_string()),
976            identity_prefix: None,
977        };
978
979        let path_args: IdentityFileArgs = IdentityFileArgs {
980            identity_from_directory: Some(dir_args),
981            identity_from_path: Some(id_path),
982            password: PasswordArgs { password_path: None },
983        };
984
985        let vp = path_args.get_files()?;
986        assert_eq!(5, vp.len());
987
988        Ok(())
989    }
990
991    fn create_file(dir_name: &str, prefix: Option<String>, num: u32) -> anyhow::Result<()> {
992        // create dir if not exist
993        fs::create_dir_all(dir_name)?;
994
995        if num > 0 {
996            for _n in 1..=num {
997                let file_name = match prefix {
998                    Some(ref file_prefix) => format!("{file_prefix}{_n}"),
999                    None => format!("fileid{_n}"),
1000                };
1001
1002                let file_path = PathBuf::from(dir_name).join(file_name);
1003                fs::write(&file_path, "Hello")?;
1004            }
1005        }
1006        Ok(())
1007    }
1008}