1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use std::time::SystemTime;
5
6use anyhow::{Result, anyhow};
7use chrono::{DateTime, Utc};
8use colored::Colorize;
9pub use sequoia_openpgp;
10use sequoia_openpgp::Cert;
11use sequoia_openpgp::parse::Parse;
12use sequoia_openpgp::policy::StandardPolicy;
13use sequoia_openpgp::types::RevocationStatus;
14
15include!(concat!(env!("OUT_DIR"), "/generated_pgp_keys.rs"));
27
28pub fn ensure_builtin_keys() -> Result<()> {
39 for (name, bytes) in BUILTIN_KEYS {
40 if let Err(e) = add_key_from_bytes(bytes, name, true) {
41 eprintln!(
42 "Warning: Failed to ensure builtin PGP key '{name}': {e}"
43 );
44 }
45 }
46 Ok(())
47}
48
49pub fn get_cert_status(cert: &Cert) -> String {
55 let policy = StandardPolicy::new();
56 let now = SystemTime::now();
57 match cert.with_policy(&policy, now) {
58 Ok(vc) => {
59 if let RevocationStatus::Revoked(_) = vc.revocation_status() {
60 return "Revoked".red().bold().to_string();
61 }
62 if let Some(expiration) = vc.primary_key().key_expiration_time() {
63 let datetime: DateTime<Utc> = DateTime::<Utc>::from(expiration);
64 if expiration < now {
65 return format!(
66 "Expired ({})",
67 datetime.format("%Y-%m-%d")
68 )
69 .red()
70 .to_string();
71 }
72 return format!(
73 "Valid (expires {})",
74 datetime.format("%Y-%m-%d")
75 )
76 .green()
77 .to_string();
78 }
79 "Valid (no expiration)".green().to_string()
80 }
81 Err(e) => format!("Invalid: {e}").red().to_string()
82 }
83}
84
85pub fn validate_cert(cert: &Cert) -> Result<()> {
94 let policy = StandardPolicy::new();
95 let now = SystemTime::now();
96 match cert.with_policy(&policy, now) {
97 Ok(vc) => {
98 if let RevocationStatus::Revoked(_) = vc.revocation_status() {
99 return Err(anyhow!("The PGP key is revoked."));
100 }
101 if let Some(expiration) = vc.primary_key().key_expiration_time()
102 && expiration < now
103 {
104 let datetime: DateTime<Utc> = DateTime::<Utc>::from(expiration);
105 return Err(anyhow!(
106 "The PGP key expired on {}.",
107 datetime.format("%Y-%m-%d")
108 ));
109 }
110 Ok(())
111 }
112 Err(e) => Err(anyhow!("The PGP key is invalid: {e}"))
113 }
114}
115
116pub fn get_pgp_dir() -> Result<PathBuf> {
126 let pgp_dir = crate::utils::get_user_data_dir()?.join("pgps");
127 fs::create_dir_all(&pgp_dir)?;
128 Ok(pgp_dir)
129}
130
131pub fn add_key_from_bytes(
142 key_bytes: &[u8],
143 name: &str,
144 quiet: bool
145) -> Result<()> {
146 let pgp_dir = get_pgp_dir()?;
147 let dest_path = pgp_dir.join(format!("{name}.asc"));
148
149 if dest_path.exists() {
150 let existing_bytes = fs::read(&dest_path)?;
151 if existing_bytes == key_bytes {
152 return Ok(());
153 }
154 if !quiet {
155 println!(
156 "{} A different key with the name '{name}' already exists. \
157 Overwriting.",
158 "Warning:".yellow(),
159 );
160 }
161 }
162
163 let cert = Cert::from_bytes(key_bytes)?;
164 validate_cert(&cert)?;
165
166 fs::write(&dest_path, key_bytes)?;
167 if !quiet {
168 println!("Successfully added/updated key '{}'.", name.cyan());
169 }
170
171 Ok(())
172}
173
174pub fn add_key_from_path(
183 path: &str,
184 name: Option<&str>,
185 quiet: bool
186) -> Result<()> {
187 let key_path = Path::new(path);
188 if !key_path.exists() {
189 return Err(anyhow!("Key file not found at: {path}"));
190 }
191
192 let key_name = name.unwrap_or_else(|| {
193 key_path
194 .file_stem()
195 .and_then(|s| s.to_str())
196 .unwrap_or("unnamed")
197 });
198
199 if !quiet {
200 println!("Validating PGP key file...");
201 }
202 let key_bytes = fs::read(key_path)?;
203 if !quiet {
204 println!("{}", "Key is valid.".green());
205 }
206
207 add_key_from_bytes(&key_bytes, key_name, quiet)
208}
209
210pub fn add_key_from_fingerprint(
220 fingerprint: &str,
221 name: &str,
222 quiet: bool
223) -> Result<()> {
224 let url = format!(
225 "https://keys.openpgp.org/vks/v1/by-fingerprint/{}",
226 fingerprint.to_uppercase()
227 );
228 if !quiet {
229 println!(
230 "Fetching key for fingerprint {} from keys.openpgp.org...",
231 fingerprint.cyan()
232 );
233 }
234
235 let client = crate::utils::get_http_client()?;
236 let response = client.get(&url).send()?;
237 if !response.status().is_success() {
238 return Err(anyhow!(
239 "Failed to fetch key from keyserver (HTTP {}).",
240 response.status()
241 ));
242 }
243
244 let key_bytes = response.bytes()?.to_vec();
245
246 if !quiet {
247 println!("Validating PGP key...");
248 }
249 Cert::from_bytes(&key_bytes)?;
250 if !quiet {
251 println!("{}", "Key is valid.".green());
252 }
253
254 add_key_from_bytes(&key_bytes, name, quiet)
255}
256
257pub fn add_key_from_url(url: &str, name: &str, quiet: bool) -> Result<()> {
263 if !quiet {
264 println!(
265 "Fetching key for {} from url {}...",
266 name.cyan(),
267 url.cyan()
268 );
269 }
270
271 let client = crate::utils::get_http_client()?;
272 let response = client.get(url).send()?;
273 if !response.status().is_success() {
274 return Err(anyhow!(
275 "Failed to fetch key from url (HTTP {})",
276 response.status()
277 ));
278 }
279
280 let key_bytes = response.bytes()?.to_vec();
281
282 if !quiet {
283 println!("Validating PGP key...");
284 }
285 Cert::from_bytes(&key_bytes)?;
286 if !quiet {
287 println!("{}", "Key is valid.".green());
288 }
289
290 add_key_from_bytes(&key_bytes, name, quiet)
291}
292
293pub fn remove_key_by_name(name: &str) -> Result<()> {
300 let pgp_dir = get_pgp_dir()?;
301 let key_path = pgp_dir.join(format!("{name}.asc"));
302
303 if !key_path.exists() {
304 return Err(anyhow!("Key with name '{name}' not found."));
305 }
306
307 fs::remove_file(&key_path)?;
308 println!("Successfully removed key '{}'.", name.cyan());
309
310 Ok(())
311}
312
313pub fn remove_key_by_fingerprint(fingerprint: &str) -> Result<()> {
321 let pgp_dir = get_pgp_dir()?;
322 let fingerprint_upper = fingerprint.to_uppercase();
323
324 for entry in fs::read_dir(pgp_dir)? {
325 let entry = entry?;
326 let path = entry.path();
327 if path.is_file()
328 && path.extension().and_then(|s| s.to_str()) == Some("asc")
329 {
330 let key_bytes = fs::read(&path)?;
331 if let Ok(cert) = Cert::from_bytes(&key_bytes)
332 && cert.fingerprint().to_string().to_uppercase()
333 == fingerprint_upper
334 {
335 fs::remove_file(&path)?;
336 println!(
337 "Successfully removed key with fingerprint {}.",
338 fingerprint.cyan()
339 );
340 return Ok(());
341 }
342 }
343 }
344
345 Err(anyhow!("Key with fingerprint '{fingerprint}' not found."))
346}
347
348pub fn list_keys() -> Result<()> {
354 let keys = get_all_local_keys_info()?;
355
356 if keys.is_empty() {
357 println!("No PGP keys found in the store.");
358 return Ok(());
359 }
360
361 println!("{} Stored PGP Keys", "::".bold().blue());
362
363 for key_info in keys {
364 println!();
365 println!("{}: {}", "Name".cyan(), key_info.name.bold());
366 println!("{}: {}", " Status".cyan(), get_cert_status(&key_info.cert));
367 println!(
368 " {}: {}",
369 "Fingerprint".cyan(),
370 key_info.cert.fingerprint()
371 );
372 for userid_amalgamation in key_info.cert.userids() {
373 let userid_packet = userid_amalgamation.userid();
374 let name = userid_packet
375 .name()
376 .ok()
377 .flatten()
378 .unwrap_or("[invalid name]");
379 let email =
380 userid_packet.email().ok().flatten().unwrap_or_default();
381
382 if email.is_empty() {
383 println!(" {}: {}", "UserID".cyan(), name);
384 } else {
385 println!(" {}: {} <{}>", "UserID".cyan(), name, email);
386 }
387 }
388 }
389
390 Ok(())
391}
392
393pub fn search_keys(term: &str) -> Result<()> {
400 let keys = get_all_local_keys_info()?;
401 let term_lower = term.to_lowercase();
402 let mut found_keys = Vec::new();
403
404 for key_info in keys {
405 let fingerprint =
406 key_info.cert.fingerprint().to_string().to_lowercase();
407 let name = key_info.name.to_lowercase();
408
409 let mut is_match =
410 name.contains(&term_lower) || fingerprint.contains(&term_lower);
411
412 if !is_match {
413 for userid_amalgamation in key_info.cert.userids() {
414 let userid_packet = userid_amalgamation.userid();
415 let uid_name = userid_packet
416 .name()
417 .ok()
418 .flatten()
419 .unwrap_or_default()
420 .to_lowercase();
421 let uid_email = userid_packet
422 .email()
423 .ok()
424 .flatten()
425 .unwrap_or_default()
426 .to_lowercase();
427
428 if uid_name.contains(&term_lower)
429 || uid_email.contains(&term_lower)
430 {
431 is_match = true;
432 break;
433 }
434 }
435 }
436
437 if is_match {
438 found_keys.push(key_info);
439 }
440 }
441
442 if found_keys.is_empty() {
443 println!("\n{}", "No keys found matching your query.".yellow());
444 return Ok(());
445 }
446
447 println!(
448 "{} Found {} key(s) matching '{}'",
449 "::".bold().blue(),
450 found_keys.len(),
451 term.blue().bold()
452 );
453
454 for key_info in found_keys {
455 println!();
456 println!("{}: {}", "Name".cyan(), key_info.name.bold());
457 println!("{}: {}", " Status".cyan(), get_cert_status(&key_info.cert));
458 println!(
459 " {}: {}",
460 "Fingerprint".cyan(),
461 key_info.cert.fingerprint()
462 );
463 for userid_amalgamation in key_info.cert.userids() {
464 let userid_packet = userid_amalgamation.userid();
465 let name = userid_packet
466 .name()
467 .ok()
468 .flatten()
469 .unwrap_or("[invalid name]");
470 let email =
471 userid_packet.email().ok().flatten().unwrap_or_default();
472
473 if email.is_empty() {
474 println!(" {}: {}", "UserID".cyan(), name);
475 } else {
476 println!(" {}: {} <{}>", "UserID".cyan(), name, email);
477 }
478 }
479 }
480
481 Ok(())
482}
483
484pub fn show_key(name: &str) -> Result<()> {
491 let pgp_dir = get_pgp_dir()?;
492 let key_path = pgp_dir.join(format!("{name}.asc"));
493
494 if !key_path.exists() {
495 return Err(anyhow!("Key with name '{name}' not found."));
496 }
497
498 let key_contents = fs::read_to_string(&key_path)?;
499 println!("{key_contents}");
500
501 Ok(())
502}
503
504pub struct KeyInfo {
506 pub name: String,
508 pub cert: Cert
510}
511
512pub fn get_all_local_keys_info() -> Result<Vec<KeyInfo>> {
519 let pgp_dir = get_pgp_dir()?;
520 let mut keys = Vec::new();
521 if !pgp_dir.exists() {
522 return Ok(keys);
523 }
524 for entry in fs::read_dir(pgp_dir)? {
525 let entry = entry?;
526 let path = entry.path();
527 if path.is_file()
528 && path.extension().and_then(|s| s.to_str()) == Some("asc")
529 && let Ok(bytes) = fs::read(&path)
530 && let Ok(cert) = Cert::from_bytes(&bytes)
531 {
532 let name = path
533 .file_stem()
534 .ok_or_else(|| {
535 anyhow!("Path should have a file stem: {}", path.display())
536 })?
537 .to_string_lossy()
538 .to_string();
539 keys.push(KeyInfo { name, cert });
540 }
541 }
542 keys.sort_by(|a, b| a.name.cmp(&b.name));
543 Ok(keys)
544}
545
546pub fn get_all_local_certs() -> Result<Vec<Cert>> {
553 let pgp_dir = get_pgp_dir()?;
554 let mut certs = Vec::new();
555 if !pgp_dir.exists() {
556 return Ok(certs);
557 }
558 for entry in fs::read_dir(pgp_dir)? {
559 let entry = entry?;
560 let path = entry.path();
561 if path.is_file()
562 && path.extension().and_then(|s| s.to_str()) == Some("asc")
563 && let Ok(bytes) = fs::read(&path)
564 && let Ok(cert) = Cert::from_bytes(&bytes)
565 {
566 certs.push(cert);
567 }
568 }
569 Ok(certs)
570}
571
572use sequoia_openpgp::KeyHandle;
573use sequoia_openpgp::parse::stream::{
574 DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper
575};
576
577struct MultiCertHelper {
580 certs: Vec<Cert>
582}
583
584impl VerificationHelper for MultiCertHelper {
585 fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
586 Ok(self.certs.clone())
587 }
588
589 fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
590 if let Some(layer) = structure.into_iter().next() {
591 match layer {
592 MessageLayer::SignatureGroup { results } => {
593 if results.iter().any(Result::is_ok) {
594 return Ok(());
595 }
596 return Err(anyhow!(
597 "No valid signature found from any trusted key."
598 ));
599 }
600 _ => {
601 return Err(anyhow!(
602 "Unexpected message structure: not a signature group."
603 ));
604 }
605 }
606 }
607 Err(anyhow!(
608 "No signature layer found in the message structure."
609 ))
610 }
611}
612
613struct OneCertHelper {
616 cert: Cert
618}
619
620impl VerificationHelper for OneCertHelper {
621 fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
622 Ok(vec![self.cert.clone()])
623 }
624
625 fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
626 if let Some(layer) = structure.into_iter().next() {
627 match layer {
628 MessageLayer::SignatureGroup { results } => {
629 if results.iter().any(Result::is_ok) {
630 return Ok(());
631 }
632 return Err(anyhow!("No valid signature found"));
633 }
634 _ => return Err(anyhow!("Unexpected message structure"))
635 }
636 }
637 Err(anyhow!("No signature layer found"))
638 }
639}
640
641pub fn cli_verify_signature(
648 file_path: &str,
649 sig_path: &str,
650 key_name: &str
651) -> Result<()> {
652 println!(
653 "Verifying {file_path} with signature {sig_path} using key \
654 '{key_name}'"
655 );
656
657 let pgp_dir = get_pgp_dir()?;
658 let key_path = pgp_dir.join(format!("{key_name}.asc"));
659 if !key_path.exists() {
660 return Err(anyhow!("Key '{key_name}' not found in local store."));
661 }
662 let key_bytes = fs::read(key_path)?;
663 let cert = Cert::from_bytes(&key_bytes)?;
664
665 verify_detached_signature(
666 Path::new(file_path),
667 Path::new(sig_path),
668 &cert
669 )?;
670
671 println!("{}", "Signature is valid.".green());
672 Ok(())
673}
674
675pub fn verify_detached_signature(
681 data_path: &Path,
682 signature_path: &Path,
683 cert: &Cert
684) -> Result<()> {
685 let data = fs::read(data_path)?;
686 let signature = fs::read(signature_path)?;
687 verify_detached_signature_raw(&data, &signature, cert)
688}
689
690pub fn verify_detached_signature_raw(
696 data: &[u8],
697 signature: &[u8],
698 cert: &Cert
699) -> Result<()> {
700 let policy = &StandardPolicy::new();
701 let helper = OneCertHelper { cert: cert.clone() };
702
703 let mut verifier = DetachedVerifierBuilder::from_bytes(signature)?
704 .with_policy(policy, None, helper)?;
705
706 verifier.verify_bytes(data)?;
707
708 Ok(())
709}
710
711pub fn sign_detached(
722 data_path: &Path,
723 signature_path: &Path,
724 key_id: &str
725) -> Result<()> {
726 if !crate::utils::command_exists("gpg") {
727 return Err(anyhow!(
728 "gpg command not found. Please install GnuPG and ensure it's in \
729 your PATH."
730 ));
731 }
732
733 let data_path_str = data_path
734 .to_str()
735 .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
736 let signature_path_str = signature_path
737 .to_str()
738 .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
739
740 let mut command = Command::new("gpg");
741 command
742 .arg("--batch")
743 .arg("--no-tty")
744 .arg("--yes")
745 .arg("--detach-sign");
746
747 if let Ok(password) = std::env::var("GPG_PASSWORD") {
748 command
749 .arg("--pinentry-mode")
750 .arg("loopback")
751 .arg("--passphrase")
752 .arg(password);
753 }
754
755 command
756 .arg("--local-user")
757 .arg(key_id)
758 .arg("--output")
759 .arg(signature_path_str)
760 .arg(data_path_str);
761
762 let output = command.output()?;
763
764 if !output.status.success() {
765 use std::fmt::Write;
766 let stderr = String::from_utf8_lossy(&output.stderr);
767 let mut error_message =
768 format!("gpg signing failed with status: {}.\n", output.status);
769 if stderr.contains("No secret key") {
770 let _ = writeln!(
771 error_message,
772 "The secret key for '{key_id}' was not found in your GPG \
773 keychain."
774 );
775 error_message.push_str(
776 "Please ensure the key is imported into GPG and is trusted."
777 );
778 } else if stderr.contains("bad passphrase")
779 || stderr.contains("Passphrase check failed")
780 {
781 error_message.push_str(
782 "Incorrect passphrase provided, or the agent could not get \
783 the passphrase.\n"
784 );
785 error_message.push_str(
786 "Ensure your GPG agent is running and configured correctly if \
787 the key is password-protected."
788 );
789 } else {
790 let _ = write!(error_message, "Stderr: {stderr}");
791 }
792
793 return Err(anyhow!(error_message));
794 }
795
796 Ok(())
797}
798
799pub fn get_certs_by_name_or_fingerprint(
807 identifiers: &[String]
808) -> Result<Vec<Cert>> {
809 let all_keys = get_all_local_keys_info()?;
810 let mut found_certs = Vec::new();
811
812 for identifier in identifiers {
813 let identifier_lower = identifier.to_lowercase();
814 let mut found = false;
815 for key_info in &all_keys {
816 let fingerprint_lower =
817 key_info.cert.fingerprint().to_string().to_lowercase();
818 if key_info.name == *identifier
819 || fingerprint_lower.starts_with(&identifier_lower)
820 {
821 found_certs.push(key_info.cert.clone());
822 found = true;
823 break;
824 }
825 }
826 if !found {
827 return Err(anyhow!(
828 "Trusted key '{identifier}' not found in Zoi's PGP keyring."
829 ));
830 }
831 }
832 Ok(found_certs)
833}
834
835pub fn verify_detached_signature_multi_key(
845 data_path: &Path,
846 signature_path: &Path,
847 trusted_certs: Vec<Cert>
848) -> Result<()> {
849 let policy = &StandardPolicy::new();
850 let data = fs::read(data_path)?;
851 let signature = fs::read(signature_path)?;
852
853 let helper = MultiCertHelper {
854 certs: trusted_certs
855 };
856
857 let mut verifier = DetachedVerifierBuilder::from_bytes(&signature)?
858 .with_policy(policy, None, helper)?;
859
860 verifier.verify_bytes(&data)?;
861
862 Ok(())
863}