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 { output, seed } => self.handle_generate(output, seed).await,
163 IdentityCommand::Show { path } => self.handle_show(path).await,
164 IdentityCommand::Verify { path } => self.handle_verify(path).await,
165 IdentityCommand::Export {
166 path,
167 output,
168 format,
169 } => self.handle_export(path, output, format).await,
170 IdentityCommand::Sign {
171 identity,
172 message,
173 output,
174 } => self.handle_sign(identity, message, output).await,
175 }
176 }
177
178 async fn handle_generate(
179 &self,
180 output: Option<PathBuf>,
181 seed: Option<String>,
182 ) -> Result<String> {
183 let output_path = output
184 .or_else(|| self.default_path.clone())
185 .ok_or_else(|| {
186 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
187 "No output path specified".into(),
188 ))
189 })?;
190
191 let identity = if let Some(seed_str) = seed {
192 let mut seed_bytes = [0u8; 32];
194 let seed_hash = sha2::Sha256::digest(seed_str.as_bytes());
195 seed_bytes.copy_from_slice(&seed_hash);
196 NodeIdentity::from_seed(&seed_bytes)?
197 } else {
198 NodeIdentity::generate()?
199 };
200
201 identity.save_to_file(&output_path).await?;
202
203 Ok(format!(
204 "Generated new identity\nNode ID: {}\nSaved to: {}",
205 identity.node_id(),
206 output_path.display()
207 ))
208 }
209
210 async fn handle_show(&self, path: Option<PathBuf>) -> Result<String> {
211 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
212 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
213 "No identity found".into(),
214 ))
215 })?;
216
217 let identity = NodeIdentity::load_from_file(&path).await?;
218
219 Ok(format!(
220 "Identity Information\nNode ID: {}\nPublic Key: {}",
221 identity.node_id(),
222 hex::encode(identity.public_key().as_bytes())
223 ))
224 }
225
226 async fn handle_verify(&self, path: Option<PathBuf>) -> Result<String> {
227 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
228 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
229 "No identity found".into(),
230 ))
231 })?;
232
233 let _identity = NodeIdentity::load_from_file(&path).await?;
234
235 let keys_valid = true; if keys_valid {
238 Ok("Identity is valid\nā Cryptographic keys: Valid".to_string())
239 } else {
240 Ok("Identity validation failed".to_string())
241 }
242 }
243
244 async fn handle_export(
245 &self,
246 path: Option<PathBuf>,
247 output: PathBuf,
248 format: String,
249 ) -> Result<String> {
250 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
251 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
252 "No identity found".into(),
253 ))
254 })?;
255
256 let identity = NodeIdentity::load_from_file(&path).await?;
257
258 match format.as_str() {
259 "json" => {
260 identity.save_to_file(&output).await?;
261 Ok(format!("Identity exported to {}", output.display()))
262 }
263 _ => Err(crate::P2PError::Identity(
264 crate::error::IdentityError::InvalidFormat(
265 format!("Unsupported format: {}", format).into(),
266 ),
267 )),
268 }
269 }
270
271 async fn handle_sign(
272 &self,
273 identity_path: Option<PathBuf>,
274 message: MessageInput,
275 output: Option<PathBuf>,
276 ) -> Result<String> {
277 let path = identity_path
278 .or_else(|| self.default_path.clone())
279 .ok_or_else(|| {
280 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
281 "No identity found".into(),
282 ))
283 })?;
284
285 let identity = NodeIdentity::load_from_file(&path).await?;
286
287 let message_bytes = match message {
288 MessageInput::Text(s) => s.into_bytes(),
289 MessageInput::File(p) => tokio::fs::read(&p).await.map_err(|e| {
290 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
291 format!("Failed to read message file: {}", e).into(),
292 ))
293 })?,
294 };
295
296 let signature = identity.sign(&message_bytes)?;
297 let sig_hex = hex::encode(signature.as_bytes());
298
299 if let Some(output_path) = output {
300 tokio::fs::write(&output_path, &sig_hex)
301 .await
302 .map_err(|e| {
303 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
304 format!("Failed to write signature: {}", e).into(),
305 ))
306 })?;
307 }
308
309 let message_hash = hex::encode(sha2::Sha256::digest(&message_bytes));
310 Ok(format!(
311 "Signature: {}\nMessage hash: {}",
312 sig_hex, message_hash
313 ))
314 }
315}
316
317impl IdentityCommand {
318 pub fn try_parse_from<I, T>(iter: I) -> std::result::Result<Self, String>
319 where
320 I: IntoIterator<Item = T>,
321 T: Into<std::ffi::OsString> + Clone,
322 {
323 let args: Vec<String> = iter
325 .into_iter()
326 .map(|s| s.into().into_string().unwrap_or_default())
327 .collect();
328
329 if args.len() < 2 || args[0] != "identity" {
330 return Err("invalid subcommand".to_string());
331 }
332
333 match args[1].as_str() {
334 "generate" => {
335 let mut i = 2;
336 while i < args.len() {
337 i += 1;
338 }
339 Ok(IdentityCommand::Generate {
340 output: None,
341 seed: None,
342 })
343 }
344 "show" => {
345 let mut path = None;
346 let mut i = 2;
347 while i < args.len() {
348 if args[i] == "--path" && i + 1 < args.len() {
349 path = Some(PathBuf::from(&args[i + 1]));
350 i += 2;
351 } else {
352 i += 1;
353 }
354 }
355 Ok(IdentityCommand::Show { path })
356 }
357 _ => Err("invalid subcommand".to_string()),
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use tempfile::TempDir;
366
367 #[test]
368 fn test_save_and_load_identity() {
369 let temp_dir = TempDir::new().expect("Should create temp directory for test");
370 let identity_path = temp_dir.path().join("test_identity.json");
371
372 let identity = NodeIdentity::generate().expect("Should generate identity in test");
374 let original_id = identity.node_id().clone();
375
376 save_identity(&identity, &identity_path).expect("Should save identity in test");
378
379 let loaded = load_identity(&identity_path).expect("Should load identity in test");
381
382 assert_eq!(loaded.node_id(), &original_id);
384 }
385}