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_default();
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_default()
207 .to_lowercase();
208 let uid_email = userid_packet
209 .email()
210 .ok()
211 .flatten()
212 .unwrap_or_default()
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_default();
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 MultiCertHelper {
335 certs: Vec<Cert>,
336}
337
338impl VerificationHelper for MultiCertHelper {
339 fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
340 Ok(self.certs.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 from any trusted key."));
351 }
352 }
353 _ => {
354 return Err(anyhow!(
355 "Unexpected message structure: not a signature group."
356 ));
357 }
358 }
359 }
360 Err(anyhow!(
361 "No signature layer found in the message structure."
362 ))
363 }
364}
365
366struct OneCertHelper {
367 cert: Cert,
368}
369
370impl VerificationHelper for OneCertHelper {
371 fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
372 Ok(vec![self.cert.clone()])
373 }
374
375 fn check(&mut self, structure: MessageStructure) -> anyhow::Result<()> {
376 if let Some(layer) = structure.into_iter().next() {
377 match layer {
378 MessageLayer::SignatureGroup { results } => {
379 if results.iter().any(|r| r.is_ok()) {
380 return Ok(());
381 } else {
382 return Err(anyhow!("No valid signature found"));
383 }
384 }
385 _ => return Err(anyhow!("Unexpected message structure")),
386 }
387 }
388 Err(anyhow!("No signature layer found"))
389 }
390}
391
392pub fn cli_verify_signature(file_path: &str, sig_path: &str, key_name: &str) -> Result<()> {
393 println!(
394 "Verifying {} with signature {} using key '{}'",
395 file_path, sig_path, key_name
396 );
397
398 let pgp_dir = get_pgp_dir()?;
399 let key_path = pgp_dir.join(format!("{}.asc", key_name));
400 if !key_path.exists() {
401 return Err(anyhow!("Key '{}' not found in local store.", key_name));
402 }
403 let key_bytes = fs::read(key_path)?;
404 let cert = Cert::from_bytes(&key_bytes)?;
405
406 verify_detached_signature(Path::new(file_path), Path::new(sig_path), &cert)?;
407
408 println!("{}", "Signature is valid.".green());
409 Ok(())
410}
411
412pub fn verify_detached_signature(
413 data_path: &Path,
414 signature_path: &Path,
415 cert: &Cert,
416) -> Result<()> {
417 let policy = &StandardPolicy::new();
418 let data = fs::read(data_path)?;
419 let signature = fs::read(signature_path)?;
420
421 let helper = OneCertHelper { cert: cert.clone() };
422
423 let mut verifier =
424 DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
425
426 verifier.verify_bytes(&data)?;
427
428 Ok(())
429}
430
431pub fn sign_detached(data_path: &Path, signature_path: &Path, key_id: &str) -> Result<()> {
432 if !crate::utils::command_exists("gpg") {
433 return Err(anyhow!(
434 "gpg command not found. Please install GnuPG and ensure it's in your PATH."
435 ));
436 }
437
438 let data_path_str = data_path
439 .to_str()
440 .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
441 let signature_path_str = signature_path
442 .to_str()
443 .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
444
445 let output = Command::new("gpg")
446 .arg("--batch")
447 .arg("--yes")
448 .arg("--detach-sign")
449 .arg("--local-user")
450 .arg(key_id)
451 .arg("--output")
452 .arg(signature_path_str)
453 .arg(data_path_str)
454 .output()?;
455
456 if !output.status.success() {
457 let stderr = String::from_utf8_lossy(&output.stderr);
458 let mut error_message = format!("gpg signing failed with status: {}.\n", output.status);
459
460 if stderr.contains("No secret key") {
461 error_message.push_str(&format!(
462 "The secret key for '{}' was not found in your GPG keychain.\n",
463 key_id
464 ));
465 error_message.push_str("Please ensure the key is imported into GPG and is trusted.");
466 } else if stderr.contains("bad passphrase") || stderr.contains("Passphrase check failed") {
467 error_message.push_str(
468 "Incorrect passphrase provided, or the agent could not get the passphrase.\n",
469 );
470 error_message.push_str("Ensure your GPG agent is running and configured correctly if the key is password-protected.");
471 } else {
472 error_message.push_str(&format!("Stderr: {}", stderr));
473 }
474
475 return Err(anyhow!(error_message));
476 }
477
478 Ok(())
479}
480
481pub fn get_certs_by_name_or_fingerprint(identifiers: &[String]) -> Result<Vec<Cert>> {
482 let all_keys = get_all_local_keys_info()?;
483 let mut found_certs = Vec::new();
484
485 for identifier in identifiers {
486 let identifier_lower = identifier.to_lowercase();
487 let mut found = false;
488 for key_info in &all_keys {
489 let fingerprint_lower = key_info.cert.fingerprint().to_string().to_lowercase();
490 if key_info.name == *identifier || fingerprint_lower.starts_with(&identifier_lower) {
491 found_certs.push(key_info.cert.clone());
492 found = true;
493 break;
494 }
495 }
496 if !found {
497 return Err(anyhow!(
498 "Trusted key '{}' not found in Zoi's PGP keyring.",
499 identifier
500 ));
501 }
502 }
503 Ok(found_certs)
504}
505
506pub fn verify_detached_signature_multi_key(
507 data_path: &Path,
508 signature_path: &Path,
509 trusted_certs: Vec<Cert>,
510) -> Result<()> {
511 let policy = &StandardPolicy::new();
512 let data = fs::read(data_path)?;
513 let signature = fs::read(signature_path)?;
514
515 let helper = MultiCertHelper {
516 certs: trusted_certs,
517 };
518
519 let mut verifier =
520 DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
521
522 verifier.verify_bytes(&data)?;
523
524 Ok(())
525}