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