zoi/pkg/
pgp.rs

1use anyhow::{Result, anyhow};
2use colored::*;
3use sequoia_openpgp::Cert;
4use sequoia_openpgp::parse::Parse;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9pub fn get_pgp_dir() -> Result<PathBuf> {
10    let home_dir = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
11    let pgp_dir = home_dir.join(".zoi").join("pgps");
12    fs::create_dir_all(&pgp_dir)?;
13    Ok(pgp_dir)
14}
15
16pub fn add_key_from_bytes(key_bytes: &[u8], name: &str) -> Result<()> {
17    let pgp_dir = get_pgp_dir()?;
18    let dest_path = pgp_dir.join(format!("{}.asc", name));
19
20    if dest_path.exists() {
21        let existing_bytes = fs::read(&dest_path)?;
22        if existing_bytes == key_bytes {
23            return Ok(());
24        }
25        println!(
26            "{} A different key with the name '{}' already exists. Overwriting.",
27            "Warning:".yellow(),
28            name
29        );
30    }
31
32    Cert::from_bytes(key_bytes)?;
33
34    fs::write(&dest_path, key_bytes)?;
35    println!("Successfully added/updated key '{}'.", name.cyan());
36
37    Ok(())
38}
39
40pub fn add_key_from_path(path: &str, name: Option<&str>) -> Result<()> {
41    let key_path = Path::new(path);
42    if !key_path.exists() {
43        return Err(anyhow!("Key file not found at: {}", path));
44    }
45
46    let key_name = name.unwrap_or_else(|| {
47        key_path
48            .file_stem()
49            .and_then(|s| s.to_str())
50            .unwrap_or("unnamed")
51    });
52
53    println!("Validating PGP key file...");
54    let key_bytes = fs::read(key_path)?;
55    println!("{}", "Key is valid.".green());
56
57    add_key_from_bytes(&key_bytes, key_name)
58}
59
60pub fn add_key_from_fingerprint(fingerprint: &str, name: &str) -> Result<()> {
61    let url = format!(
62        "https://keys.openpgp.org/vks/v1/by-fingerprint/{}",
63        fingerprint.to_uppercase()
64    );
65    println!(
66        "Fetching key for fingerprint {} from keys.openpgp.org...",
67        fingerprint.cyan()
68    );
69
70    let response = reqwest::blocking::get(&url)?;
71    if !response.status().is_success() {
72        return Err(anyhow!(
73            "Failed to fetch key from keyserver (HTTP {}).",
74            response.status()
75        ));
76    }
77
78    let key_bytes = response.bytes()?.to_vec();
79
80    println!("Validating PGP key...");
81    Cert::from_bytes(&key_bytes)?;
82    println!("{}", "Key is valid.".green());
83
84    add_key_from_bytes(&key_bytes, name)
85}
86
87pub fn add_key_from_url(url: &str, name: &str) -> Result<()> {
88    println!(
89        "Fetching key for {} from url {}...",
90        name.cyan(),
91        url.cyan()
92    );
93
94    let response = reqwest::blocking::get(url)?;
95    if !response.status().is_success() {
96        return Err(anyhow!(
97            "Failed to fetch key from url (HTTP {})",
98            response.status()
99        ));
100    }
101
102    let key_bytes = response.bytes()?.to_vec();
103
104    println!("Validating PGP key...");
105    Cert::from_bytes(&key_bytes)?;
106    println!("{}", "Key is valid.".green());
107
108    add_key_from_bytes(&key_bytes, name)
109}
110
111pub fn remove_key_by_name(name: &str) -> Result<()> {
112    let pgp_dir = get_pgp_dir()?;
113    let key_path = pgp_dir.join(format!("{}.asc", name));
114
115    if !key_path.exists() {
116        return Err(anyhow!("Key with name '{}' not found.", name));
117    }
118
119    fs::remove_file(&key_path)?;
120    println!("Successfully removed key '{}'.", name.cyan());
121
122    Ok(())
123}
124
125pub fn remove_key_by_fingerprint(fingerprint: &str) -> Result<()> {
126    let pgp_dir = get_pgp_dir()?;
127    let fingerprint_upper = fingerprint.to_uppercase();
128
129    for entry in fs::read_dir(pgp_dir)? {
130        let entry = entry?;
131        let path = entry.path();
132        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("asc") {
133            let key_bytes = fs::read(&path)?;
134            if let Ok(cert) = Cert::from_bytes(&key_bytes)
135                && cert.fingerprint().to_string().to_uppercase() == fingerprint_upper
136            {
137                fs::remove_file(&path)?;
138                println!(
139                    "Successfully removed key with fingerprint {}.",
140                    fingerprint.cyan()
141                );
142                return Ok(());
143            }
144        }
145    }
146
147    Err(anyhow!("Key with fingerprint '{}' not found.", fingerprint))
148}
149
150pub fn list_keys() -> Result<()> {
151    let keys = get_all_local_keys_info()?;
152
153    if keys.is_empty() {
154        println!("No PGP keys found in the store.");
155        return Ok(());
156    }
157
158    println!("{}", "--- Stored PGP Keys ---".yellow().bold());
159
160    for key_info in keys {
161        println!();
162        println!("{}: {}", "Name".cyan(), key_info.name.bold());
163        println!(
164            "  {}: {}",
165            "Fingerprint".cyan(),
166            key_info.cert.fingerprint()
167        );
168        for userid_amalgamation in key_info.cert.userids() {
169            let userid_packet = userid_amalgamation.userid();
170            let name = userid_packet
171                .name()
172                .ok()
173                .flatten()
174                .unwrap_or("[invalid name]");
175            let email = userid_packet.email().ok().flatten().unwrap_or_default();
176
177            if !email.is_empty() {
178                println!("  {}: {} <{}>", "UserID".cyan(), name, email);
179            } else {
180                println!("  {}: {}", "UserID".cyan(), name);
181            }
182        }
183    }
184
185    Ok(())
186}
187
188pub fn search_keys(term: &str) -> Result<()> {
189    let keys = get_all_local_keys_info()?;
190    let term_lower = term.to_lowercase();
191    let mut found_keys = Vec::new();
192
193    for key_info in keys {
194        let fingerprint = key_info.cert.fingerprint().to_string().to_lowercase();
195        let name = key_info.name.to_lowercase();
196
197        let mut is_match = name.contains(&term_lower) || fingerprint.contains(&term_lower);
198
199        if !is_match {
200            for userid_amalgamation in key_info.cert.userids() {
201                let userid_packet = userid_amalgamation.userid();
202                let uid_name = userid_packet
203                    .name()
204                    .ok()
205                    .flatten()
206                    .unwrap_or_default()
207                    .to_lowercase();
208                let uid_email = userid_packet
209                    .email()
210                    .ok()
211                    .flatten()
212                    .unwrap_or_default()
213                    .to_lowercase();
214
215                if uid_name.contains(&term_lower) || uid_email.contains(&term_lower) {
216                    is_match = true;
217                    break;
218                }
219            }
220        }
221
222        if is_match {
223            found_keys.push(key_info);
224        }
225    }
226
227    if found_keys.is_empty() {
228        println!("\n{}", "No keys found matching your query.".yellow());
229        return Ok(());
230    }
231
232    println!(
233        "--- Found {} key(s) matching '{}' ---",
234        found_keys.len(),
235        term.blue().bold()
236    );
237
238    for key_info in found_keys {
239        println!();
240        println!("{}: {}", "Name".cyan(), key_info.name.bold());
241        println!(
242            "  {}: {}",
243            "Fingerprint".cyan(),
244            key_info.cert.fingerprint()
245        );
246        for userid_amalgamation in key_info.cert.userids() {
247            let userid_packet = userid_amalgamation.userid();
248            let name = userid_packet
249                .name()
250                .ok()
251                .flatten()
252                .unwrap_or("[invalid name]");
253            let email = userid_packet.email().ok().flatten().unwrap_or_default();
254
255            if !email.is_empty() {
256                println!("  {}: {} <{}>", "UserID".cyan(), name, email);
257            } else {
258                println!("  {}: {}", "UserID".cyan(), name);
259            }
260        }
261    }
262
263    Ok(())
264}
265
266pub fn show_key(name: &str) -> Result<()> {
267    let pgp_dir = get_pgp_dir()?;
268    let key_path = pgp_dir.join(format!("{}.asc", name));
269
270    if !key_path.exists() {
271        return Err(anyhow!("Key with name '{}' not found.", name));
272    }
273
274    let key_contents = fs::read_to_string(&key_path)?;
275    println!("{}", key_contents);
276
277    Ok(())
278}
279
280pub struct KeyInfo {
281    pub name: String,
282    pub cert: Cert,
283}
284
285pub fn get_all_local_keys_info() -> Result<Vec<KeyInfo>> {
286    let pgp_dir = get_pgp_dir()?;
287    let mut keys = Vec::new();
288    if !pgp_dir.exists() {
289        return Ok(keys);
290    }
291    for entry in fs::read_dir(pgp_dir)? {
292        let entry = entry?;
293        let path = entry.path();
294        if path.is_file()
295            && path.extension().and_then(|s| s.to_str()) == Some("asc")
296            && let Ok(bytes) = fs::read(&path)
297            && let Ok(cert) = Cert::from_bytes(&bytes)
298        {
299            let name = path.file_stem().unwrap().to_string_lossy().to_string();
300            keys.push(KeyInfo { name, cert });
301        }
302    }
303    keys.sort_by(|a, b| a.name.cmp(&b.name));
304    Ok(keys)
305}
306
307use sequoia_openpgp::policy::StandardPolicy;
308
309pub fn get_all_local_certs() -> Result<Vec<Cert>> {
310    let pgp_dir = get_pgp_dir()?;
311    let mut certs = Vec::new();
312    if !pgp_dir.exists() {
313        return Ok(certs);
314    }
315    for entry in fs::read_dir(pgp_dir)? {
316        let entry = entry?;
317        let path = entry.path();
318        if path.is_file()
319            && path.extension().and_then(|s| s.to_str()) == Some("asc")
320            && let Ok(bytes) = fs::read(&path)
321            && let Ok(cert) = Cert::from_bytes(&bytes)
322        {
323            certs.push(cert);
324        }
325    }
326    Ok(certs)
327}
328
329use sequoia_openpgp::{
330    KeyHandle,
331    parse::stream::{DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper},
332};
333
334struct MultiCertHelper {
335    certs: Vec<Cert>,
336}
337
338impl VerificationHelper for MultiCertHelper {
339    fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
340        Ok(self.certs.clone())
341    }
342
343    fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
344        if let Some(layer) = structure.into_iter().next() {
345            match layer {
346                MessageLayer::SignatureGroup { results } => {
347                    if results.iter().any(|r| r.is_ok()) {
348                        return Ok(());
349                    } else {
350                        return Err(anyhow!("No valid signature found from any trusted key."));
351                    }
352                }
353                _ => {
354                    return Err(anyhow!(
355                        "Unexpected message structure: not a signature group."
356                    ));
357                }
358            }
359        }
360        Err(anyhow!(
361            "No signature layer found in the message structure."
362        ))
363    }
364}
365
366struct OneCertHelper {
367    cert: Cert,
368}
369
370impl VerificationHelper for OneCertHelper {
371    fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
372        Ok(vec![self.cert.clone()])
373    }
374
375    fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
376        if let Some(layer) = structure.into_iter().next() {
377            match layer {
378                MessageLayer::SignatureGroup { results } => {
379                    if results.iter().any(|r| r.is_ok()) {
380                        return Ok(());
381                    } else {
382                        return Err(anyhow!("No valid signature found"));
383                    }
384                }
385                _ => return Err(anyhow!("Unexpected message structure")),
386            }
387        }
388        Err(anyhow!("No signature layer found"))
389    }
390}
391
392pub fn cli_verify_signature(file_path: &str, sig_path: &str, key_name: &str) -> Result<()> {
393    println!(
394        "Verifying {} with signature {} using key '{}'",
395        file_path, sig_path, key_name
396    );
397
398    let pgp_dir = get_pgp_dir()?;
399    let key_path = pgp_dir.join(format!("{}.asc", key_name));
400    if !key_path.exists() {
401        return Err(anyhow!("Key '{}' not found in local store.", key_name));
402    }
403    let key_bytes = fs::read(key_path)?;
404    let cert = Cert::from_bytes(&key_bytes)?;
405
406    verify_detached_signature(Path::new(file_path), Path::new(sig_path), &cert)?;
407
408    println!("{}", "Signature is valid.".green());
409    Ok(())
410}
411
412pub fn verify_detached_signature(
413    data_path: &Path,
414    signature_path: &Path,
415    cert: &Cert,
416) -> Result<()> {
417    let policy = &StandardPolicy::new();
418    let data = fs::read(data_path)?;
419    let signature = fs::read(signature_path)?;
420
421    let helper = OneCertHelper { cert: cert.clone() };
422
423    let mut verifier =
424        DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
425
426    verifier.verify_bytes(&data)?;
427
428    Ok(())
429}
430
431pub fn sign_detached(data_path: &Path, signature_path: &Path, key_id: &str) -> Result<()> {
432    if !crate::utils::command_exists("gpg") {
433        return Err(anyhow!(
434            "gpg command not found. Please install GnuPG and ensure it's in your PATH."
435        ));
436    }
437
438    let data_path_str = data_path
439        .to_str()
440        .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
441    let signature_path_str = signature_path
442        .to_str()
443        .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
444
445    let output = Command::new("gpg")
446        .arg("--batch")
447        .arg("--yes")
448        .arg("--detach-sign")
449        .arg("--local-user")
450        .arg(key_id)
451        .arg("--output")
452        .arg(signature_path_str)
453        .arg(data_path_str)
454        .output()?;
455
456    if !output.status.success() {
457        let stderr = String::from_utf8_lossy(&output.stderr);
458        let mut error_message = format!("gpg signing failed with status: {}.\n", output.status);
459
460        if stderr.contains("No secret key") {
461            error_message.push_str(&format!(
462                "The secret key for '{}' was not found in your GPG keychain.\n",
463                key_id
464            ));
465            error_message.push_str("Please ensure the key is imported into GPG and is trusted.");
466        } else if stderr.contains("bad passphrase") || stderr.contains("Passphrase check failed") {
467            error_message.push_str(
468                "Incorrect passphrase provided, or the agent could not get the passphrase.\n",
469            );
470            error_message.push_str("Ensure your GPG agent is running and configured correctly if the key is password-protected.");
471        } else {
472            error_message.push_str(&format!("Stderr: {}", stderr));
473        }
474
475        return Err(anyhow!(error_message));
476    }
477
478    Ok(())
479}
480
481pub fn get_certs_by_name_or_fingerprint(identifiers: &[String]) -> Result<Vec<Cert>> {
482    let all_keys = get_all_local_keys_info()?;
483    let mut found_certs = Vec::new();
484
485    for identifier in identifiers {
486        let identifier_lower = identifier.to_lowercase();
487        let mut found = false;
488        for key_info in &all_keys {
489            let fingerprint_lower = key_info.cert.fingerprint().to_string().to_lowercase();
490            if key_info.name == *identifier || fingerprint_lower.starts_with(&identifier_lower) {
491                found_certs.push(key_info.cert.clone());
492                found = true;
493                break;
494            }
495        }
496        if !found {
497            return Err(anyhow!(
498                "Trusted key '{}' not found in Zoi's PGP keyring.",
499                identifier
500            ));
501        }
502    }
503    Ok(found_certs)
504}
505
506pub fn verify_detached_signature_multi_key(
507    data_path: &Path,
508    signature_path: &Path,
509    trusted_certs: Vec<Cert>,
510) -> Result<()> {
511    let policy = &StandardPolicy::new();
512    let data = fs::read(data_path)?;
513    let signature = fs::read(signature_path)?;
514
515    let helper = MultiCertHelper {
516        certs: trusted_certs,
517    };
518
519    let mut verifier =
520        DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
521
522    verifier.verify_bytes(&data)?;
523
524    Ok(())
525}