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