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("");
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("")
207 .to_lowercase();
208 let uid_email = userid_packet
209 .email()
210 .ok()
211 .flatten()
212 .unwrap_or("")
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("");
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 OneCertHelper {
335 cert: Cert,
336}
337
338impl VerificationHelper for OneCertHelper {
339 fn get_certs(&mut self, _ids: &[KeyHandle]) -> anyhow::Result<Vec<Cert>> {
340 Ok(vec![self.cert.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"));
351 }
352 }
353 _ => return Err(anyhow!("Unexpected message structure")),
354 }
355 }
356 Err(anyhow!("No signature layer found"))
357 }
358}
359
360pub fn cli_verify_signature(file_path: &str, sig_path: &str, key_name: &str) -> Result<()> {
361 println!(
362 "Verifying {} with signature {} using key '{}'",
363 file_path, sig_path, key_name
364 );
365
366 let pgp_dir = get_pgp_dir()?;
367 let key_path = pgp_dir.join(format!("{}.asc", key_name));
368 if !key_path.exists() {
369 return Err(anyhow!("Key '{}' not found in local store.", key_name));
370 }
371 let key_bytes = fs::read(key_path)?;
372 let cert = Cert::from_bytes(&key_bytes)?;
373
374 verify_detached_signature(Path::new(file_path), Path::new(sig_path), &cert)?;
375
376 println!("{}", "Signature is valid.".green());
377 Ok(())
378}
379
380pub fn verify_detached_signature(
381 data_path: &Path,
382 signature_path: &Path,
383 cert: &Cert,
384) -> Result<()> {
385 let policy = &StandardPolicy::new();
386 let data = fs::read(data_path)?;
387 let signature = fs::read(signature_path)?;
388
389 let helper = OneCertHelper { cert: cert.clone() };
390
391 let mut verifier =
392 DetachedVerifierBuilder::from_bytes(&signature)?.with_policy(policy, None, helper)?;
393
394 verifier.verify_bytes(&data)?;
395
396 Ok(())
397}
398
399pub fn sign_detached(data_path: &Path, signature_path: &Path, key_id: &str) -> Result<()> {
400 if !crate::utils::command_exists("gpg") {
401 return Err(anyhow!(
402 "gpg command not found. Please install GnuPG and ensure it's in your PATH."
403 ));
404 }
405
406 let data_path_str = data_path
407 .to_str()
408 .ok_or_else(|| anyhow!("Invalid data path for signing."))?;
409 let signature_path_str = signature_path
410 .to_str()
411 .ok_or_else(|| anyhow!("Invalid signature path for signing."))?;
412
413 let output = Command::new("gpg")
414 .arg("--batch")
415 .arg("--yes")
416 .arg("--detach-sign")
417 .arg("--local-user")
418 .arg(key_id)
419 .arg("--output")
420 .arg(signature_path_str)
421 .arg(data_path_str)
422 .output()?;
423
424 if !output.status.success() {
425 let stderr = String::from_utf8_lossy(&output.stderr);
426 let mut error_message = format!("gpg signing failed with status: {}.\n", output.status);
427
428 if stderr.contains("No secret key") {
429 error_message.push_str(&format!(
430 "The secret key for '{}' was not found in your GPG keychain.\n",
431 key_id
432 ));
433 error_message.push_str("Please ensure the key is imported into GPG and is trusted.");
434 } else if stderr.contains("bad passphrase") || stderr.contains("Passphrase check failed") {
435 error_message.push_str(
436 "Incorrect passphrase provided, or the agent could not get the passphrase.\n",
437 );
438 error_message.push_str("Ensure your GPG agent is running and configured correctly if the key is password-protected.");
439 } else {
440 error_message.push_str(&format!("Stderr: {}", stderr));
441 }
442
443 return Err(anyhow!(error_message));
444 }
445
446 Ok(())
447}