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