Skip to main content

rns_ctl/cmd/
id.rs

1//! Identity management for Reticulum.
2//!
3//! Generate, inspect, and manage RNS identities. Standalone tool, no RPC needed.
4
5use std::fs;
6use std::io::{self, Read, Write};
7use std::path::Path;
8use std::process;
9
10use crate::args::Args;
11use crate::format::{base32_decode, base32_encode, prettyhexrep};
12use rns_core::destination::destination_hash;
13use rns_crypto::identity::Identity;
14use rns_crypto::OsRng;
15
16const LARGE_FILE_WARN: u64 = 16 * 1024 * 1024; // 16 MB
17
18pub fn run(args: Args) {
19    if args.has("version") {
20        println!("rns-ctl {}", env!("FULL_VERSION"));
21        return;
22    }
23
24    if args.has("help") {
25        print_usage();
26        return;
27    }
28
29    // Generate new identity
30    if let Some(file) = args.get("g") {
31        generate_identity(file, &args);
32        return;
33    }
34
35    // Import from hex
36    if let Some(hex_str) = args.get("m") {
37        import_from_hex(hex_str, &args);
38        return;
39    }
40
41    // Load identity from file or hash
42    if let Some(file_or_hash) = args.get("i") {
43        let path = Path::new(file_or_hash);
44        if path.exists() {
45            inspect_identity_file(path, &args);
46        } else {
47            // Treat as hash
48            println!("Hash: {}", file_or_hash);
49        }
50        return;
51    }
52
53    print_usage();
54}
55
56fn generate_identity(file: &str, args: &Args) {
57    let path = Path::new(file);
58    let force = args.has("f") || args.has("force");
59
60    if path.exists() && !force {
61        eprintln!("File already exists: {} (use -f to overwrite)", file);
62        process::exit(1);
63    }
64
65    let identity = Identity::new(&mut OsRng);
66    let Some(prv_key) = identity.get_private_key() else {
67        eprintln!("Generated identity is missing a private key");
68        process::exit(1);
69    };
70
71    fs::write(path, &prv_key).unwrap_or_else(|e| {
72        eprintln!("Error writing identity: {}", e);
73        process::exit(1);
74    });
75
76    println!("Generated new identity");
77    println!("  Hash : {}", prettyhexrep(identity.hash()));
78    println!("  Saved: {}", file);
79
80    // Show base32 if requested
81    if args.has("B") {
82        println!("  Base32: {}", base32_encode(&prv_key));
83    }
84}
85
86fn inspect_identity_file(path: &Path, args: &Args) {
87    let data = fs::read(path).unwrap_or_else(|e| {
88        eprintln!("Error reading file: {}", e);
89        process::exit(1);
90    });
91
92    let identity = if data.len() == 64 {
93        // Private key (32 enc + 32 sig)
94        let mut key = [0u8; 64];
95        key.copy_from_slice(&data);
96        Identity::from_private_key(&key)
97    } else if data.len() == 64 + 64 {
98        let mut key = [0u8; 64];
99        key.copy_from_slice(&data[..64]);
100        Identity::from_private_key(&key)
101    } else if data.len() == 32 + 32 {
102        // Public keys only (32 enc_pub + 32 sig_pub)
103        let mut key = [0u8; 64];
104        key.copy_from_slice(&data);
105        Identity::from_public_key(&key)
106    } else {
107        eprintln!("Unknown identity file format ({} bytes)", data.len());
108        process::exit(1);
109    };
110
111    println!("Identity <{}>", prettyhexrep(identity.hash()));
112    println!("  Hash      : {}", prettyhexrep(identity.hash()));
113
114    let show_private = args.has("P");
115    let show_public = args.has("p") || show_private;
116
117    if show_public {
118        if let Some(pub_key) = identity.get_public_key() {
119            println!("  Public key: {}", prettyhexrep(&pub_key));
120        }
121    }
122
123    if show_private {
124        if let Some(prv_key) = identity.get_private_key() {
125            println!("  Private key: {}", prettyhexrep(&prv_key));
126        } else {
127            println!("  Private key: (not available)");
128        }
129    }
130
131    // Compute destination hash if -H is given
132    if let Some(aspects_str) = args.get("H") {
133        let parts: Vec<&str> = aspects_str.split('.').collect();
134        if parts.len() >= 2 {
135            let app_name = parts[0];
136            let aspects: Vec<&str> = parts[1..].to_vec();
137            let dest_hash = destination_hash(app_name, &aspects, Some(identity.hash()));
138            println!("  Dest hash : {}", prettyhexrep(&dest_hash));
139        } else {
140            eprintln!("  Aspects must be in format: app_name.aspect1.aspect2");
141        }
142    }
143
144    let force = args.has("f") || args.has("force");
145    let use_stdin = args.has("stdin");
146    let use_stdout = args.has("stdout");
147
148    // Encrypt file
149    if let Some(file) = args.get("e") {
150        let plaintext = if use_stdin {
151            read_stdin()
152        } else {
153            check_file_size(file);
154            fs::read(file).unwrap_or_else(|e| {
155                eprintln!("Error reading file: {}", e);
156                process::exit(1);
157            })
158        };
159        let ciphertext = identity
160            .encrypt(&plaintext, &mut OsRng)
161            .unwrap_or_else(|e| {
162                eprintln!("Encryption failed: {:?}", e);
163                process::exit(1);
164            });
165        if use_stdout {
166            io::stdout().write_all(&ciphertext).unwrap_or_else(|e| {
167                eprintln!("Error writing to stdout: {}", e);
168                process::exit(1);
169            });
170        } else {
171            let out_file = format!("{}.enc", file);
172            write_file_checked(&out_file, &ciphertext, force);
173            println!("  Encrypted {} -> {}", file, out_file);
174        }
175    }
176
177    // Decrypt file
178    if let Some(file) = args.get("d") {
179        let ciphertext = if use_stdin {
180            read_stdin()
181        } else {
182            fs::read(file).unwrap_or_else(|e| {
183                eprintln!("Error reading file: {}", e);
184                process::exit(1);
185            })
186        };
187        match identity.decrypt(&ciphertext) {
188            Ok(plaintext) => {
189                if use_stdout {
190                    io::stdout().write_all(&plaintext).unwrap_or_else(|e| {
191                        eprintln!("Error writing to stdout: {}", e);
192                        process::exit(1);
193                    });
194                } else {
195                    let out_file = if file.ends_with(".enc") {
196                        file[..file.len() - 4].to_string()
197                    } else {
198                        format!("{}.dec", file)
199                    };
200                    write_file_checked(&out_file, &plaintext, force);
201                    println!("  Decrypted {} -> {}", file, out_file);
202                }
203            }
204            Err(e) => {
205                eprintln!("  Decryption failed: {:?}", e);
206                process::exit(1);
207            }
208        }
209    }
210
211    // Sign file
212    if let Some(file) = args.get("s") {
213        let data = if use_stdin {
214            read_stdin()
215        } else {
216            fs::read(file).unwrap_or_else(|e| {
217                eprintln!("Error reading file: {}", e);
218                process::exit(1);
219            })
220        };
221        match identity.sign(&data) {
222            Ok(sig) => {
223                if use_stdout {
224                    io::stdout().write_all(&sig).unwrap_or_else(|e| {
225                        eprintln!("Error writing to stdout: {}", e);
226                        process::exit(1);
227                    });
228                } else {
229                    let out_file = format!("{}.sig", file);
230                    write_file_checked(&out_file, &sig, force);
231                    println!("  Signed {} -> {}", file, out_file);
232                }
233            }
234            Err(e) => {
235                eprintln!("  Signing failed: {:?}", e);
236                process::exit(1);
237            }
238        }
239    }
240
241    // Verify signature
242    if let Some(sig_file) = args.get("V") {
243        let sig_data = fs::read(sig_file).unwrap_or_else(|e| {
244            eprintln!("Error reading signature: {}", e);
245            process::exit(1);
246        });
247        if sig_data.len() != 64 {
248            eprintln!(
249                "  Invalid signature (expected 64 bytes, got {})",
250                sig_data.len()
251            );
252            process::exit(1);
253        }
254        let mut sig = [0u8; 64];
255        sig.copy_from_slice(&sig_data);
256
257        // Read the data file (remove .sig extension)
258        let data_file = if sig_file.ends_with(".sig") {
259            &sig_file[..sig_file.len() - 4]
260        } else {
261            eprintln!("  Cannot determine data file (expected .sig extension)");
262            process::exit(1);
263        };
264
265        let data = fs::read(data_file).unwrap_or_else(|e| {
266            eprintln!("Error reading {}: {}", data_file, e);
267            process::exit(1);
268        });
269
270        if identity.verify(&sig, &data) {
271            println!("  Signature valid");
272        } else {
273            println!("  Signature INVALID");
274            process::exit(1);
275        }
276    }
277
278    // Export as hex
279    if args.has("x") {
280        if let Some(prv_key) = identity.get_private_key() {
281            println!("{}", prettyhexrep(&prv_key));
282        } else if let Some(pub_key) = identity.get_public_key() {
283            println!("{}", prettyhexrep(&pub_key));
284        }
285    }
286
287    // Export as base64
288    if args.has("b") {
289        if let Some(prv_key) = identity.get_private_key() {
290            println!("{}", base64_encode(&prv_key));
291        } else if let Some(pub_key) = identity.get_public_key() {
292            println!("{}", base64_encode(&pub_key));
293        }
294    }
295
296    // Export as base32
297    if args.has("B") {
298        if let Some(prv_key) = identity.get_private_key() {
299            println!("{}", base32_encode(&prv_key));
300        } else if let Some(pub_key) = identity.get_public_key() {
301            println!("{}", base32_encode(&pub_key));
302        }
303    }
304}
305
306fn import_from_hex(hex_str: &str, args: &Args) {
307    // Check if it's actually base32
308    let bytes = if args.has("B") {
309        match base32_decode(hex_str) {
310            Some(b) => b,
311            None => {
312                eprintln!("Invalid base32 string");
313                process::exit(1);
314            }
315        }
316    } else {
317        match parse_hex(hex_str) {
318            Some(b) => b,
319            None => {
320                eprintln!("Invalid hex string");
321                process::exit(1);
322            }
323        }
324    };
325
326    if bytes.len() == 64 {
327        let mut key = [0u8; 64];
328        key.copy_from_slice(&bytes);
329        let identity = Identity::from_private_key(&key);
330        println!("Identity <{}>", prettyhexrep(identity.hash()));
331
332        // Save to file if -w is provided
333        if let Some(file) = args.get("w") {
334            let force = args.has("f") || args.has("force");
335            write_file_checked(file, &key, force);
336            println!("  Saved to {}", file);
337        }
338    } else {
339        eprintln!(
340            "Expected 64 bytes (128 hex chars or base32), got {} bytes",
341            bytes.len()
342        );
343        process::exit(1);
344    }
345}
346
347fn parse_hex(s: &str) -> Option<Vec<u8>> {
348    let s = s.trim();
349    if s.len() % 2 != 0 {
350        return None;
351    }
352    let mut bytes = Vec::with_capacity(s.len() / 2);
353    for i in (0..s.len()).step_by(2) {
354        match u8::from_str_radix(&s[i..i + 2], 16) {
355            Ok(b) => bytes.push(b),
356            Err(_) => return None,
357        }
358    }
359    Some(bytes)
360}
361
362fn base64_encode(data: &[u8]) -> String {
363    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
364    let mut result = String::new();
365    let mut i = 0;
366    while i < data.len() {
367        let b0 = data[i] as u32;
368        let b1 = if i + 1 < data.len() {
369            data[i + 1] as u32
370        } else {
371            0
372        };
373        let b2 = if i + 2 < data.len() {
374            data[i + 2] as u32
375        } else {
376            0
377        };
378
379        let triple = (b0 << 16) | (b1 << 8) | b2;
380
381        result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
382        result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
383        if i + 1 < data.len() {
384            result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
385        } else {
386            result.push('=');
387        }
388        if i + 2 < data.len() {
389            result.push(CHARS[(triple & 0x3F) as usize] as char);
390        } else {
391            result.push('=');
392        }
393
394        i += 3;
395    }
396    result
397}
398
399fn read_stdin() -> Vec<u8> {
400    let mut buf = Vec::new();
401    io::stdin().read_to_end(&mut buf).unwrap_or_else(|e| {
402        eprintln!("Error reading stdin: {}", e);
403        process::exit(1);
404    });
405    buf
406}
407
408fn check_file_size(file: &str) {
409    if let Ok(meta) = fs::metadata(file) {
410        if meta.len() > LARGE_FILE_WARN {
411            eprintln!(
412                "Warning: file is {} — encryption is done in-memory",
413                crate::format::size_str(meta.len()),
414            );
415        }
416    }
417}
418
419fn write_file_checked(path: &str, data: &[u8], force: bool) {
420    let p = Path::new(path);
421    if p.exists() && !force {
422        eprintln!("File already exists: {} (use -f to overwrite)", path);
423        process::exit(1);
424    }
425    fs::write(p, data).unwrap_or_else(|e| {
426        eprintln!("Error writing: {}", e);
427        process::exit(1);
428    });
429}
430
431fn print_usage() {
432    println!("Usage: rns-ctl id [OPTIONS]");
433    println!();
434    println!("Options:");
435    println!("  -g FILE            Generate new identity and save to file");
436    println!("  -i FILE            Load and inspect identity from file");
437    println!("  -p                 Print public key");
438    println!("  -P                 Print private key (implies -p)");
439    println!("  -H APP.ASPECT      Compute destination hash");
440    println!("  -e FILE            Encrypt file with identity");
441    println!("  -d FILE            Decrypt file with identity");
442    println!("  -s FILE            Sign file with identity");
443    println!("  -V FILE.sig        Verify signature");
444    println!("  -m HEX             Import identity from hex string");
445    println!("  -w FILE            Write imported identity to file");
446    println!("  -x                 Export as hex");
447    println!("  -b                 Export as base64");
448    println!("  -B                 Export/import as base32");
449    println!("  -f, --force        Force overwrite existing files");
450    println!("  --stdin            Read input from stdin");
451    println!("  --stdout           Write output to stdout");
452    println!("  --version          Print version and exit");
453    println!("  --help, -h         Print this help");
454}