hopli/
identity.rs

1//! This module contains subcommands for `hopli identity`,
2//! This command contains subcommands to read, create or update identity files, providing correct
3//! [crate::key_pair::PasswordArgs]
4//!
5//! For all three actions around identity files, at least two arguments are needed:
6//! - Path to files
7//! - Password to encrypt or decrypt the file
8//!   - To update identity files, a new password must be provided.
9//!
10//! Some sample commands:
11//!
12//! - To create identities
13//! ```text
14//! hopli identity create \
15//!     --identity-directory "./test" \
16//!     --identity-prefix nodes_ \
17//!     --number 2 \
18//!     --password-path "./test/pwd"
19//! ```
20//!
21//! - To read identities
22//! ```text
23//! hopli identity read \
24//!     --identity-directory "./test" \
25//!     --identity-prefix node_ \
26//!     --password-path "./test/pwd"
27//! ```
28//!
29//! - To update password of identities
30//! ```text
31//!     hopli identity update \
32//!     --identity-directory "./test" \
33//!     --identity-prefix node_ \
34//!     --password-path "./test/pwd" \
35//!     --new-password-path "./test/newpwd"
36//! ```
37use std::{collections::HashMap, str::FromStr};
38
39use clap::{Parser, builder::RangedU64ValueParser};
40use hopr_crypto_types::{
41    keypairs::Keypair,
42    prelude::{OffchainPublicKey, PeerId},
43};
44use hopr_primitive_types::{prelude::ToHex, primitives::Address};
45use hoprd_keypair::key_pair::HoprKeys;
46use tracing::{debug, info};
47
48use crate::{
49    key_pair::{
50        ArgEnvReader, IdentityFileArgs, NewPasswordArgs, create_identity, read_identities, read_identity,
51        update_identity_password,
52    },
53    utils::{Cmd, HelperErrors, HelperErrors::UnableToParseAddress},
54};
55
56/// CLI arguments for `hopli identity`
57#[derive(Clone, Debug, Parser)]
58pub enum IdentitySubcommands {
59    /// Create safe and module proxy if nothing exists
60    #[command(visible_alias = "cr")]
61    Create {
62        /// Arguments to locate identity file(s) of HOPR node(s)
63        #[command(flatten)]
64        local_identity: IdentityFileArgs,
65
66        /// Number of identities to be generated
67        #[clap(
68            help = "Number of identities to be generated, e.g. 1",
69            long,
70            short,
71            value_parser = RangedU64ValueParser::<u32>::new().range(1..),
72            default_value_t = 1
73        )]
74        number: u32,
75    },
76
77    /// Migrate safe and module to a new network
78    #[command(visible_alias = "rd")]
79    Read {
80        /// Arguments to locate identity file(s) of HOPR node(s)
81        #[command(flatten)]
82        local_identity: IdentityFileArgs,
83    },
84
85    /// Update the password of identity files
86    #[command(visible_alias = "up")]
87    Update {
88        /// Arguments to locate identity files of HOPR node(s)
89        #[command(flatten)]
90        local_identity: IdentityFileArgs,
91
92        /// New password
93        #[command(flatten)]
94        new_password: NewPasswordArgs,
95    },
96
97    /// Converts PeerId from base58 to public key as hex or vice-versa
98    #[command(visible_alias = "conv")]
99    ConvertPeer {
100        /// PeerID or Public key
101        #[command(flatten)]
102        peer_or_key: ConvertPeerArgs,
103    },
104}
105
106/// Arguments for PeerID or Public key
107#[derive(Debug, Clone, Parser, Default)]
108pub struct ConvertPeerArgs {
109    /// Either provide a PeerID in Base58 or public key as hex starting with 0x
110    #[clap(
111        short,
112        long,
113        help = "PeerID in Base58 or public key as hex starting with 0x",
114        name = "peer_or_key",
115        value_name = "PEER_ID_OR_PUBLIC_KEY"
116    )]
117    pub peer_or_key: String,
118}
119
120impl IdentitySubcommands {
121    /// Execute the command to create identities
122    fn execute_identity_creation_loop(local_identity: IdentityFileArgs, number: u32) -> Result<(), HelperErrors> {
123        // check if password is provided
124        let pwd = local_identity.clone().password.read_default()?;
125
126        let mut node_identities: HashMap<String, HoprKeys> = HashMap::new();
127
128        match local_identity.identity_from_directory {
129            Some(local_id) => {
130                let id_dir = local_id
131                    .identity_directory
132                    .ok_or(HelperErrors::MissingIdentityDirectory)?;
133                for index in 0..=number - 1 {
134                    // build file name
135                    let file_prefix = local_id
136                        .identity_prefix
137                        .as_ref()
138                        .map(|provided_name| provided_name.to_owned() + &index.to_string());
139
140                    let (id_filename, identity) = create_identity(&id_dir, &pwd, &file_prefix)
141                        .map_err(|_| HelperErrors::UnableToCreateIdentity)?;
142                    node_identities.insert(id_filename, identity);
143                }
144
145                info!("Identities: {:?}", node_identities);
146                Ok(())
147            }
148            None => Err(HelperErrors::MissingParameter(
149                "Missing identity_from_directory when creating identites".into(),
150            )),
151        }
152    }
153
154    /// Execute the command to read identities
155    fn execute_identity_read_loop(local_identity: IdentityFileArgs) -> Result<(), HelperErrors> {
156        // check if password is provided
157        let pwd = local_identity.clone().password.read_default()?;
158
159        // read ids
160        let files = local_identity.get_files()?;
161        debug!("Identities read {:?}", files.len());
162
163        let node_identities: HashMap<String, HoprKeys> =
164            read_identities(files, &pwd).map_err(|_| HelperErrors::UnableToReadIdentity)?;
165
166        let node_addresses: Vec<Address> = node_identities
167            .values()
168            .map(|n| n.chain_key.public().to_address())
169            .collect();
170
171        let peer_ids: Vec<String> = node_identities
172            .values()
173            .map(|n| n.packet_key.public().to_peerid_str())
174            .collect();
175
176        info!("Identities: {:?}", node_identities);
177        println!("Identity addresses: {:?}", node_addresses);
178        println!("Identity peerids: [{}]", peer_ids.join(", "));
179        Ok(())
180    }
181
182    /// update the password of an identity file
183    fn execute_identity_update(
184        local_identity: IdentityFileArgs,
185        new_password: NewPasswordArgs,
186    ) -> Result<(), HelperErrors> {
187        // check if old password is provided
188        let pwd = local_identity.clone().password.read_default()?;
189        // check if new password is provided
190        let new_pwd = new_password.read_default()?;
191
192        // read ids
193        let files = local_identity.get_files()?;
194        debug!("Identities read {:?}", files.len());
195
196        let _ = files
197            .iter()
198            .map(|file| {
199                read_identity(file, &pwd)
200                    .map_err(|_| HelperErrors::UnableToUpdateIdentityPassword)
201                    .and_then(|(_, keys)| update_identity_password(keys, file, &new_pwd))
202            })
203            .collect::<Result<Vec<_>, _>>()?;
204
205        info!("Updated password for {:?} identity files", files.len());
206        Ok(())
207    }
208}
209
210impl Cmd for IdentitySubcommands {
211    /// Run the execute_identity_creation_loop function
212    fn run(self) -> Result<(), HelperErrors> {
213        match self {
214            IdentitySubcommands::Create { local_identity, number } => {
215                IdentitySubcommands::execute_identity_creation_loop(local_identity, number)
216            }
217            IdentitySubcommands::Read { local_identity } => {
218                IdentitySubcommands::execute_identity_read_loop(local_identity)
219            }
220            IdentitySubcommands::Update {
221                local_identity,
222                new_password,
223            } => IdentitySubcommands::execute_identity_update(local_identity, new_password),
224            IdentitySubcommands::ConvertPeer { peer_or_key } => {
225                if peer_or_key.peer_or_key.to_lowercase().starts_with("0x") {
226                    let pk = OffchainPublicKey::from_hex(&peer_or_key.peer_or_key)
227                        .map_err(|_| UnableToParseAddress(peer_or_key.peer_or_key))?;
228                    println!("{}", pk.to_peerid_str());
229                    Ok(())
230                } else {
231                    let pk = PeerId::from_str(&peer_or_key.peer_or_key)
232                        .map_err(|_| UnableToParseAddress(peer_or_key.peer_or_key.clone()))
233                        .and_then(|p| {
234                            OffchainPublicKey::try_from(p).map_err(|_| UnableToParseAddress(peer_or_key.peer_or_key))
235                        })?;
236                    println!("{}", pk.to_hex());
237                    Ok(())
238                }
239            }
240        }
241    }
242
243    async fn async_run(self) -> Result<(), HelperErrors> {
244        Ok(())
245    }
246}