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("");
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("")
207                    .to_lowercase();
208                let uid_email = userid_packet
209                    .email()
210                    .ok()
211                    .flatten()
212                    .unwrap_or("")
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("");
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 OneCertHelper {
335    cert: Cert,
336}
337
338impl VerificationHelper for OneCertHelper {
339    fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
340        Ok(vec![self.cert.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"));
351                    }
352                }
353                _ => return Err(anyhow!("Unexpected message structure")),
354            }
355        }
356        Err(anyhow!("No signature layer found"))
357    }
358}
359
360pub fn cli_verify_signature(file_path: &str, sig_path: &str, key_name: &str) -> Result<()> {
361    println!(
362        "Verifying {} with signature {} using key '{}'",
363        file_path, sig_path, key_name
364    );
365
366    let pgp_dir = get_pgp_dir()?;
367    let key_path = pgp_dir.join(format!("{}.asc", key_name));
368    if !key_path.exists() {
369        return Err(anyhow!("Key '{}' not found in local store.", key_name));
370    }
371    let key_bytes = fs::read(key_path)?;
372    let cert = Cert::from_bytes(&key_bytes)?;
373
374    verify_detached_signature(Path::new(file_path), Path::new(sig_path), &cert)?;
375
376    println!("{}", "Signature is valid.".green());
377    Ok(())
378}
379
380pub fn verify_detached_signature(
381    data_path: &Path,
382    signature_path: &Path,
383    cert: &Cert,
384) -> Result<()> {
385    let policy = &StandardPolicy::new();
386    let data = fs::read(data_path)?;
387    let signature = fs::read(signature_path)?;
388
389    let helper = OneCertHelper { cert: cert.clone() };
390
391    let mut verifier =
392        DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
393
394    verifier.verify_bytes(&data)?;
395
396    Ok(())
397}
398
399pub fn sign_detached(data_path: &Path, signature_path: &Path, key_id: &str) -> Result<()> {
400    if !crate::utils::command_exists("gpg") {
401        return Err(anyhow!(
402            "gpg command not found. Please install GnuPG and ensure it's in your PATH."
403        ));
404    }
405
406    let data_path_str = data_path
407        .to_str()
408        .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
409    let signature_path_str = signature_path
410        .to_str()
411        .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
412
413    let output = Command::new("gpg")
414        .arg("--batch")
415        .arg("--yes")
416        .arg("--detach-sign")
417        .arg("--local-user")
418        .arg(key_id)
419        .arg("--output")
420        .arg(signature_path_str)
421        .arg(data_path_str)
422        .output()?;
423
424    if !output.status.success() {
425        let stderr = String::from_utf8_lossy(&output.stderr);
426        let mut error_message = format!("gpg signing failed with status: {}.\n", output.status);
427
428        if stderr.contains("No secret key") {
429            error_message.push_str(&format!(
430                "The secret key for '{}' was not found in your GPG keychain.\n",
431                key_id
432            ));
433            error_message.push_str("Please ensure the key is imported into GPG and is trusted.");
434        } else if stderr.contains("bad passphrase") || stderr.contains("Passphrase check failed") {
435            error_message.push_str(
436                "Incorrect passphrase provided, or the agent could not get the passphrase.\n",
437            );
438            error_message.push_str("Ensure your GPG agent is running and configured correctly if the key is password-protected.");
439        } else {
440            error_message.push_str(&format!("Stderr: {}", stderr));
441        }
442
443        return Err(anyhow!(error_message));
444    }
445
446    Ok(())
447}