use crate::error::{Error, Result};
use crate::internal::parse_cert;
#[cfg(feature = "network")]
pub fn fetch_key_by_email(email: &str) -> Result<Vec<u8>> {
let (local, domain) = parse_email(email)?;
let urls = wkd_urls(&local, &domain);
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let mut last_error = None;
for url in urls {
match client.get(&url).send() {
Ok(response) => {
if response.status().is_success() {
let bytes = response.bytes()
.map_err(|e| Error::Network(e.to_string()))?;
let _ = parse_cert(&bytes)?;
return Ok(bytes.to_vec());
}
}
Err(e) => {
last_error = Some(e.to_string());
}
}
}
Err(Error::KeyNotFound(format!(
"No key found for email '{}': {}",
email,
last_error.unwrap_or_else(|| "Not found".to_string())
)))
}
#[cfg(feature = "network")]
pub fn fetch_key_by_fingerprint(
fingerprint: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-fingerprint/{}", server, fingerprint.to_uppercase());
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver: {}",
fingerprint
)));
}
let bytes = response.bytes()
.map_err(|e| Error::Network(e.to_string()))?;
let _ = parse_cert(&bytes)?;
Ok(bytes.to_vec())
}
#[cfg(feature = "network")]
pub fn fetch_key_by_keyid(
key_id: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-keyid/{}", server, key_id.to_uppercase());
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver: {}",
key_id
)));
}
let bytes = response.bytes()
.map_err(|e| Error::Network(e.to_string()))?;
let _ = parse_cert(&bytes)?;
Ok(bytes.to_vec())
}
#[cfg(feature = "network")]
pub fn fetch_key_by_email_from_keyserver(
email: &str,
keyserver: Option<&str>,
) -> Result<Vec<u8>> {
let server = keyserver.unwrap_or("https://keys.openpgp.org");
let url = format!("{}/vks/v1/by-email/{}", server, email);
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| Error::Network(e.to_string()))?;
let response = client.get(&url)
.send()
.map_err(|e| Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::KeyNotFound(format!(
"Key not found on keyserver for email: {}",
email
)));
}
let bytes = response.bytes()
.map_err(|e| Error::Network(e.to_string()))?;
let _ = parse_cert(&bytes)?;
Ok(bytes.to_vec())
}
fn parse_email(email: &str) -> Result<(String, String)> {
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err(Error::InvalidInput(format!("Invalid email address: {}", email)));
}
Ok((parts[0].to_lowercase(), parts[1].to_lowercase()))
}
#[cfg(feature = "network")]
fn wkd_urls(local: &str, domain: &str) -> Vec<String> {
use sha1::{Sha1, Digest};
let hash = {
let mut hasher = Sha1::new();
hasher.update(local.as_bytes());
let result = hasher.finalize();
zbase32_encode(&result)
};
vec![
format!(
"https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{hash}?l={local}",
domain = domain,
hash = hash,
local = local
),
format!(
"https://{domain}/.well-known/openpgpkey/hu/{hash}?l={local}",
domain = domain,
hash = hash,
local = local
),
]
}
#[cfg(feature = "network")]
fn zbase32_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ybndrfg8ejkmcpqxot1uwisza345h769";
let mut result = String::new();
let mut buffer: u64 = 0;
let mut bits_in_buffer = 0;
for &byte in data {
buffer = (buffer << 8) | byte as u64;
bits_in_buffer += 8;
while bits_in_buffer >= 5 {
bits_in_buffer -= 5;
let index = ((buffer >> bits_in_buffer) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
}
if bits_in_buffer > 0 {
let index = ((buffer << (5 - bits_in_buffer)) & 0x1f) as usize;
result.push(ALPHABET[index] as char);
}
result
}
#[cfg(test)]
mod tests {
#[cfg(feature = "network")]
use super::*;
#[test]
#[cfg(feature = "network")]
fn test_zbase32_encode() {
let input = b"test";
let encoded = zbase32_encode(input);
assert!(!encoded.is_empty());
}
#[test]
fn test_parse_email() {
let (local, domain) = parse_email("user@example.com").unwrap();
assert_eq!(local, "user");
assert_eq!(domain, "example.com");
assert!(parse_email("invalid").is_err());
}
#[test]
#[cfg(feature = "network")]
fn test_wkd_urls() {
let urls = wkd_urls("test", "example.com");
assert_eq!(urls.len(), 2);
assert!(urls[0].contains("openpgpkey.example.com"));
assert!(urls[1].contains("example.com/.well-known"));
}
}