1use super::node_identity::{IdentityData, NodeIdentity};
7use crate::Result;
8use sha2::Digest;
9use std::fs;
10use std::path::{Path, PathBuf};
11use tracing::info;
12
13pub fn generate_identity() -> Result<()> {
15 let start = std::time::Instant::now();
16 let identity = NodeIdentity::generate()?;
17 let elapsed = start.elapsed();
18
19 info!("ā
Identity generated successfully (no PoW)");
20 info!("ā±ļø Generation time: {:?}", elapsed);
21 info!("š Identity Details:");
22 info!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
23 info!("Node ID: {}", identity.node_id());
24 info!(
25 "Public Key: {}",
26 hex::encode(identity.public_key().as_bytes())
27 );
28 info!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
29
30 Ok(())
31}
32
33pub fn save_identity(identity: &NodeIdentity, path: &Path) -> Result<()> {
35 let data = identity.export();
36 let json = serde_json::to_string_pretty(&data).map_err(|e| {
37 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
38 format!("Failed to serialize identity: {}", e).into(),
39 ))
40 })?;
41
42 fs::write(path, json).map_err(|e| {
43 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
44 format!("Failed to write identity file: {}", e).into(),
45 ))
46 })?;
47
48 info!("ā
Identity saved to: {}", path.display());
49 Ok(())
50}
51
52pub fn load_identity(path: &Path) -> Result<NodeIdentity> {
54 let json = fs::read_to_string(path).map_err(|e| {
55 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
56 format!("Failed to read identity file: {}", e).into(),
57 ))
58 })?;
59
60 let data: IdentityData = serde_json::from_str(&json).map_err(|e| {
61 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
62 format!("Failed to parse identity file: {}", e).into(),
63 ))
64 })?;
65
66 let identity = NodeIdentity::import(&data)?;
67
68 info!("ā
Identity loaded from: {}", path.display());
69 info!("Node ID: {}", identity.node_id());
70
71 Ok(identity)
72}
73
74pub fn show_identity(identity: &NodeIdentity) -> Result<()> {
76 info!("š P2P Identity Information");
77 info!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
78 info!("Node ID: {}", identity.node_id());
79 info!(
80 "Public Key: {}",
81 hex::encode(identity.public_key().as_bytes())
82 );
83 info!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
84
85 Ok(())
86}
87
88#[derive(Debug)]
91pub enum IdentityCommand {
92 Generate {
94 output: Option<PathBuf>,
96
97 seed: Option<String>,
99 },
100
101 Show {
103 path: Option<PathBuf>,
105 },
106
107 Verify {
109 path: Option<PathBuf>,
111 },
112
113 Export {
115 path: Option<PathBuf>,
117
118 output: PathBuf,
120
121 format: String,
123 },
124
125 Sign {
127 identity: Option<PathBuf>,
129
130 message: MessageInput,
132
133 output: Option<PathBuf>,
135 },
136}
137
138#[derive(Debug, Clone)]
139pub enum MessageInput {
140 Text(String),
141 File(PathBuf),
142}
143
144#[derive(Debug)]
145pub enum ExportFormat {
146 Json,
147 Base64,
148 Hex,
149}
150
151pub struct IdentityCliHandler {
152 default_path: Option<PathBuf>,
153}
154
155impl IdentityCliHandler {
156 pub fn new(default_path: Option<PathBuf>) -> Self {
157 Self { default_path }
158 }
159
160 pub async fn execute(&self, command: IdentityCommand) -> Result<String> {
161 match command {
162 IdentityCommand::Generate {
163 output,
164 seed,
165 } => self.handle_generate(output, seed).await,
166 IdentityCommand::Show { path } => self.handle_show(path).await,
167 IdentityCommand::Verify { path } => self.handle_verify(path).await,
168 IdentityCommand::Export {
169 path,
170 output,
171 format,
172 } => self.handle_export(path, output, format).await,
173 IdentityCommand::Sign {
174 identity,
175 message,
176 output,
177 } => self.handle_sign(identity, message, output).await,
178 }
179 }
180
181 async fn handle_generate(
182 &self,
183 output: Option<PathBuf>,
184 seed: Option<String>,
185 ) -> Result<String> {
186 let output_path = output
187 .or_else(|| self.default_path.clone())
188 .ok_or_else(|| {
189 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
190 "No output path specified".into(),
191 ))
192 })?;
193
194 let identity = if let Some(seed_str) = seed {
195 let mut seed_bytes = [0u8; 32];
197 let seed_hash = sha2::Sha256::digest(seed_str.as_bytes());
198 seed_bytes.copy_from_slice(&seed_hash);
199 NodeIdentity::from_seed(&seed_bytes)?
200 } else {
201 NodeIdentity::generate()?
202 };
203
204 identity.save_to_file(&output_path).await?;
205
206 Ok(format!(
207 "Generated new identity\nNode ID: {}\nSaved to: {}",
208 identity.node_id(),
209 output_path.display()
210 ))
211 }
212
213 async fn handle_show(&self, path: Option<PathBuf>) -> Result<String> {
214 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
215 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
216 "No identity found".into(),
217 ))
218 })?;
219
220 let identity = NodeIdentity::load_from_file(&path).await?;
221
222 Ok(format!(
223 "Identity Information\nNode ID: {}\nPublic Key: {}",
224 identity.node_id(),
225 hex::encode(identity.public_key().as_bytes())
226 ))
227 }
228
229 async fn handle_verify(&self, path: Option<PathBuf>) -> Result<String> {
230 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
231 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
232 "No identity found".into(),
233 ))
234 })?;
235
236 let identity = NodeIdentity::load_from_file(&path).await?;
237
238 let keys_valid = true; if keys_valid {
241 Ok("Identity is valid\nā Cryptographic keys: Valid".to_string())
242 } else {
243 Ok("Identity validation failed".to_string())
244 }
245 }
246
247 async fn handle_export(
248 &self,
249 path: Option<PathBuf>,
250 output: PathBuf,
251 format: String,
252 ) -> Result<String> {
253 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
254 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
255 "No identity found".into(),
256 ))
257 })?;
258
259 let identity = NodeIdentity::load_from_file(&path).await?;
260
261 match format.as_str() {
262 "json" => {
263 identity.save_to_file(&output).await?;
264 Ok(format!("Identity exported to {}", output.display()))
265 }
266 _ => Err(crate::P2PError::Identity(
267 crate::error::IdentityError::InvalidFormat(
268 format!("Unsupported format: {}", format).into(),
269 ),
270 )),
271 }
272 }
273
274 async fn handle_sign(
275 &self,
276 identity_path: Option<PathBuf>,
277 message: MessageInput,
278 output: Option<PathBuf>,
279 ) -> Result<String> {
280 let path = identity_path
281 .or_else(|| self.default_path.clone())
282 .ok_or_else(|| {
283 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
284 "No identity found".into(),
285 ))
286 })?;
287
288 let identity = NodeIdentity::load_from_file(&path).await?;
289
290 let message_bytes = match message {
291 MessageInput::Text(s) => s.into_bytes(),
292 MessageInput::File(p) => tokio::fs::read(&p).await.map_err(|e| {
293 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
294 format!("Failed to read message file: {}", e).into(),
295 ))
296 })?,
297 };
298
299 let signature = identity.sign(&message_bytes)?;
300 let sig_hex = hex::encode(signature.as_bytes());
301
302 if let Some(output_path) = output {
303 tokio::fs::write(&output_path, &sig_hex)
304 .await
305 .map_err(|e| {
306 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
307 format!("Failed to write signature: {}", e).into(),
308 ))
309 })?;
310 }
311
312 let message_hash = hex::encode(sha2::Sha256::digest(&message_bytes));
313 Ok(format!(
314 "Signature: {}\nMessage hash: {}",
315 sig_hex, message_hash
316 ))
317 }
318}
319
320impl IdentityCommand {
321 pub fn try_parse_from<I, T>(iter: I) -> std::result::Result<Self, String>
322 where
323 I: IntoIterator<Item = T>,
324 T: Into<std::ffi::OsString> + Clone,
325 {
326 let args: Vec<String> = iter
328 .into_iter()
329 .map(|s| s.into().into_string().unwrap_or_default())
330 .collect();
331
332 if args.len() < 2 || args[0] != "identity" {
333 return Err("invalid subcommand".to_string());
334 }
335
336 match args[1].as_str() {
337 "generate" => {
338 let mut i = 2;
339 while i < args.len() {
340 i += 1;
341 }
342 Ok(IdentityCommand::Generate {
343 output: None,
344 seed: None,
345 })
346 }
347 "show" => {
348 let mut path = None;
349 let mut i = 2;
350 while i < args.len() {
351 if args[i] == "--path" && i + 1 < args.len() {
352 path = Some(PathBuf::from(&args[i + 1]));
353 i += 2;
354 } else {
355 i += 1;
356 }
357 }
358 Ok(IdentityCommand::Show { path })
359 }
360 _ => Err("invalid subcommand".to_string()),
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use tempfile::TempDir;
369
370 #[test]
371 fn test_save_and_load_identity() {
372 let temp_dir = TempDir::new().expect("Should create temp directory for test");
373 let identity_path = temp_dir.path().join("test_identity.json");
374
375 let identity = NodeIdentity::generate().expect("Should generate identity in test");
377 let original_id = identity.node_id().clone();
378
379 save_identity(&identity, &identity_path).expect("Should save identity in test");
381
382 let loaded = load_identity(&identity_path).expect("Should load identity in test");
384
385 assert_eq!(loaded.node_id(), &original_id);
387 }
388}