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