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