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