1use super::node_identity::{IdentityData, NodeId, 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 let word_address = derive_word_address(identity.node_id());
204
205 Ok(format!(
206 "Generated new identity\nNode ID: {}\nWord Address: {}\nSaved to: {}",
207 identity.node_id(),
208 word_address,
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 let word_address = derive_word_address(identity.node_id());
223
224 Ok(format!(
225 "Identity Information\nNode ID: {}\nWord Address: {}\nPublic Key: {}\nPoW Difficulty: N/A",
226 identity.node_id(),
227 word_address,
228 hex::encode(identity.public_key().as_bytes())
229 ))
230 }
231
232 async fn handle_verify(&self, path: Option<PathBuf>) -> Result<String> {
233 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
234 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
235 "No identity found".into(),
236 ))
237 })?;
238
239 let identity = NodeIdentity::load_from_file(&path).await?;
240
241 let _word_address = derive_word_address(identity.node_id());
242
243 Ok("Identity is valid\nā Proof of Work: Valid\nā Cryptographic keys: Valid\nā Word address: Matches".to_string())
244 }
245
246 async fn handle_export(
247 &self,
248 path: Option<PathBuf>,
249 output: PathBuf,
250 format: String,
251 ) -> Result<String> {
252 let path = path.or_else(|| self.default_path.clone()).ok_or_else(|| {
253 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
254 "No identity found".into(),
255 ))
256 })?;
257
258 let identity = NodeIdentity::load_from_file(&path).await?;
259
260 match format.as_str() {
261 "json" => {
262 identity.save_to_file(&output).await?;
263 Ok(format!("Identity exported to {}", output.display()))
264 }
265 _ => Err(crate::P2PError::Identity(
266 crate::error::IdentityError::InvalidFormat(
267 format!("Unsupported format: {}", format).into(),
268 ),
269 )),
270 }
271 }
272
273 async fn handle_sign(
274 &self,
275 identity_path: Option<PathBuf>,
276 message: MessageInput,
277 output: Option<PathBuf>,
278 ) -> Result<String> {
279 let path = identity_path
280 .or_else(|| self.default_path.clone())
281 .ok_or_else(|| {
282 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
283 "No identity found".into(),
284 ))
285 })?;
286
287 let identity = NodeIdentity::load_from_file(&path).await?;
288
289 let message_bytes = match message {
290 MessageInput::Text(s) => s.into_bytes(),
291 MessageInput::File(p) => tokio::fs::read(&p).await.map_err(|e| {
292 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
293 format!("Failed to read message file: {}", e).into(),
294 ))
295 })?,
296 };
297
298 let signature = identity.sign(&message_bytes)?;
299 let sig_hex = hex::encode(signature.as_bytes());
300
301 if let Some(output_path) = output {
302 tokio::fs::write(&output_path, &sig_hex)
303 .await
304 .map_err(|e| {
305 crate::P2PError::Identity(crate::error::IdentityError::InvalidFormat(
306 format!("Failed to write signature: {}", e).into(),
307 ))
308 })?;
309 }
310
311 let message_hash = hex::encode(sha2::Sha256::digest(&message_bytes));
312 Ok(format!(
313 "Signature: {}\nMessage hash: {}",
314 sig_hex, message_hash
315 ))
316 }
317}
318
319fn derive_word_address(node_id: &NodeId) -> String {
320 let hex = hex::encode(node_id.to_bytes());
321 if hex.len() >= 16 {
322 format!(
323 "{}-{}-{}-{}",
324 &hex[0..4],
325 &hex[4..8],
326 &hex[8..12],
327 &hex[12..16]
328 )
329 } else {
330 hex
331 }
332}
333
334impl IdentityCommand {
335 pub fn try_parse_from<I, T>(iter: I) -> std::result::Result<Self, String>
336 where
337 I: IntoIterator<Item = T>,
338 T: Into<std::ffi::OsString> + Clone,
339 {
340 let args: Vec<String> = iter
342 .into_iter()
343 .map(|s| s.into().into_string().unwrap_or_default())
344 .collect();
345
346 if args.len() < 2 || args[0] != "identity" {
347 return Err("invalid subcommand".to_string());
348 }
349
350 match args[1].as_str() {
351 "generate" => {
352 let mut i = 2;
353 while i < args.len() {
354 i += 1;
355 }
356 Ok(IdentityCommand::Generate {
357 output: None,
358 seed: None,
359 })
360 }
361 "show" => {
362 let mut path = None;
363 let mut i = 2;
364 while i < args.len() {
365 if args[i] == "--path" && i + 1 < args.len() {
366 path = Some(PathBuf::from(&args[i + 1]));
367 i += 2;
368 } else {
369 i += 1;
370 }
371 }
372 Ok(IdentityCommand::Show { path })
373 }
374 _ => Err("invalid subcommand".to_string()),
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use tempfile::TempDir;
383
384 #[test]
385 fn test_save_and_load_identity() {
386 let temp_dir = TempDir::new().expect("Should create temp directory for test");
387 let identity_path = temp_dir.path().join("test_identity.json");
388
389 let identity = NodeIdentity::generate().expect("Should generate identity in test");
391 let original_id = identity.node_id().clone();
392
393 save_identity(&identity, &identity_path).expect("Should save identity in test");
395
396 let loaded = load_identity(&identity_path).expect("Should load identity in test");
398
399 assert_eq!(loaded.node_id(), &original_id);
401 }
402}