Skip to main content

sioul_sync/
keys.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! Looking up someone's OpenPGP key, only when you ask: it tells a server to
5//! whom you are writing. First their domain's own Web Key Directory
6//! (draft-koch-openpgp-webkey-service), which only that domain can fill, then
7//! keys.openpgp.org, which publishes a key only for an address that confirmed it.
8
9use crate::SyncError;
10use sioul_core::pgp::{self, KeySummary};
11use std::time::Duration;
12
13/// The keys found for `address`, kept with the others' keys.
14pub fn lookup(address: &str) -> Result<Vec<KeySummary>, SyncError> {
15    let address = address.trim();
16    // HTTPS to the end: a key fetched in clear could be anyone's.
17    let agent: ureq::Agent = ureq::Agent::config_builder().timeout_global(Some(Duration::from_secs(15))).https_only(true).build().into();
18    let mut urls: Vec<String> = pgp::wkd_urls(address).map(Vec::from).unwrap_or_default();
19    let encoded: String = address.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") }).collect();
20    urls.push(format!("https://keys.openpgp.org/vks/v1/by-email/{encoded}"));
21    let mut last_error = None;
22    for url in urls {
23        match agent.get(&url).call() {
24            Ok(mut response) => {
25                let bytes = response.body_mut().read_to_vec().unwrap_or_default();
26                let found = pgp::learn(&bytes, address);
27                if !found.is_empty() {
28                    return Ok(found);
29                }
30            }
31            Err(ureq::Error::StatusCode(_)) => {}
32            Err(e) => last_error = Some(e.to_string()),
33        }
34    }
35    match last_error {
36        Some(e) => Err(SyncError::Network(e)),
37        None => Ok(Vec::new()),
38    }
39}