use std::sync::Arc; use crate::imap::client::{ImapClient, ImapCredentials}; use axum::extract::{Path, Query}; use axum::http::{HeaderMap, HeaderValue}; use axum::response::{IntoResponse, Response}; use axum::{Extension, Json}; use base64::Engine; use serde::{Deserialize, Serialize}; use crate::auth::session::SessionState; use crate::config::AppConfig; use crate::db; use crate::db::pgp::{PgpKeyRecord, PgpPreferences}; use crate::error::AppError; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GenerateRequest { #[serde(default)] pub name: String, pub email: String, #[serde(default)] pub passphrase: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImportRequest { #[serde(default)] pub key_data: Option, #[serde(default)] pub key_data_base64: Option, #[serde(default = "default_trust")] pub trust: String, } #[derive(Debug, Deserialize)] pub struct ExportQuery { #[serde(default)] pub secret: bool, } #[derive(Debug, Deserialize)] pub struct TrustRequest { pub trust: String, } #[derive(Debug, Deserialize)] pub struct DiscoverRequest { pub email: String, } #[derive(Debug, Deserialize)] pub struct RecipientsRequest { pub emails: Vec, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct RecipientStatus { pub email: String, pub has_key: bool, pub fingerprint: Option, pub trust: Option, pub source: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VerifyRequest { pub data: String, pub signature: Option, pub message: Option, pub fingerprint: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct VerifyResponse { pub verified: bool, pub plaintext: Option, pub fingerprint: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DecryptRequest { pub message: String, #[serde(default)] pub passphrase: String, pub fingerprint: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DiscoverResponse { pub found: bool, pub source: Option, pub key: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProcessMessageRequest { #[serde(default)] pub passphrase: String, pub fingerprint: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TestPassphraseRequest { #[serde(default)] pub passphrase: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChangePassphraseRequest { #[serde(default)] pub current_passphrase: String, #[serde(default)] pub new_passphrase: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct TestPassphraseResponse { pub ok: bool, } pub async fn process_message( Extension(session): Extension, Extension(config): Extension>, Extension(imap_client): Extension>, Path((folder, uid)): Path<(String, u32)>, Json(request): Json, ) -> Result, AppError> { if request.passphrase.len() > 1024 { return Err(AppError::BadRequest( "OpenPGP passphrase is too long".to_string(), )); } let creds = ImapCredentials { host: session.imap_host.clone(), port: session.imap_port, tls: session.imap_tls, tls_verify: session.imap_tls_verify, email: session.email.clone(), password: session.password.clone(), }; let raw = imap_client .fetch_raw_message(&creds, &folder, uid) .await .map_err(|e| AppError::ServiceUnavailable(format!("IMAP error: {e}")))?; if raw.len() > 50 * 1024 * 1024 { return Err(AppError::BadRequest( "OpenPGP message is too large to process".to_string(), )); } if !crate::pgp::looks_like_openpgp(&raw) { return Ok(Json(crate::pgp::ProcessedMessage { pgp: false, encrypted: false, decrypted: false, signed: false, verified: false, signer_fingerprint: None, signer_email: None, signer_trust: None, text: None, html: None, attachments: vec![], })); } let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let requested = request .fingerprint .map(|value| db::pgp::normalize_fingerprint(&value)); let secret_rows = database .run(move |conn| { if let Some(fp) = requested { Ok(db::pgp::get_key(conn, &fp)? .into_iter() .filter(|key| key.secret_key_path.is_some()) .collect::>()) } else { db::pgp::list_secret_keys(conn) } }) .await .map_err(AppError::InternalError)?; let public_keys = database .run(|conn| { Ok(db::pgp::list_keys(conn)? .into_iter() .map(|key| (key.fingerprint, key.public_key_armor)) .collect::>()) }) .await .map_err(AppError::InternalError)?; let passphrase = request.passphrase; let mut result = tokio::task::spawn_blocking(move || { let mut secrets = Vec::new(); for key in secret_rows { if let Some(path) = key.secret_key_path && let Ok(armor) = crate::pgp::read_secret_key(&path) { secrets.push(armor); } } crate::pgp::process_message(&raw, &secrets, &passphrase, &public_keys) }) .await .map_err(|e| AppError::InternalError(format!("PGP message processing task failed: {e}")))? .map_err(AppError::BadRequest)?; if let Some(fingerprint) = result.signer_fingerprint.clone() { let lookup = db::pgp::normalize_fingerprint(&fingerprint); if let Some(key) = database .run(move |conn| db::pgp::get_key(conn, &lookup)) .await .map_err(AppError::InternalError)? { if !key.record.email.trim().is_empty() { result.signer_email = Some(key.record.email.clone()); } result.signer_trust = Some(key.record.trust.clone()); } } Ok(Json(result)) } fn default_trust() -> String { "unknown".to_string() } pub async fn list_keys( Extension(session): Extension, Extension(config): Extension>, ) -> Result>, AppError> { let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let keys = database .run(db::pgp::list_keys) .await .map_err(AppError::InternalError)?; Ok(Json(keys)) } pub async fn generate_key( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result, AppError> { validate_email(&request.email)?; if request.passphrase.len() > 1024 { return Err(AppError::BadRequest( "OpenPGP passphrase is too long".to_string(), )); } let name = request.name; let email = request.email.trim().to_ascii_lowercase(); let passphrase = request.passphrase; let material = tokio::task::spawn_blocking(move || crate::pgp::generate_key(&name, &email, &passphrase)) .await .map_err(|e| AppError::InternalError(format!("PGP key generation task failed: {e}")))? .map_err(AppError::BadRequest)?; store_material(&session, &config, material, "generated", "verified").await } pub async fn import_key( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result, AppError> { validate_trust(&request.trust)?; let input = match (request.key_data, request.key_data_base64) { (Some(text), None) => { if text.len() > config.pgp_max_key_bytes.saturating_mul(2) { return Err(AppError::BadRequest( "OpenPGP key input is too large".to_string(), )); } text.into_bytes() } (None, Some(encoded)) => { let max_encoded = config .pgp_max_key_bytes .saturating_mul(4) .saturating_div(3) .saturating_add(16); if encoded.len() > max_encoded { return Err(AppError::BadRequest( "OpenPGP key input is too large".to_string(), )); } let decoded = base64::engine::general_purpose::STANDARD .decode(encoded.as_bytes()) .map_err(|_| AppError::BadRequest("Invalid base64 OpenPGP key data".to_string()))?; if decoded.len() > config.pgp_max_key_bytes { return Err(AppError::BadRequest( "OpenPGP key input is too large".to_string(), )); } decoded } (Some(_), Some(_)) => { return Err(AppError::BadRequest( "Provide either keyData or keyDataBase64, not both".to_string(), )); } (None, None) => { return Err(AppError::BadRequest( "OpenPGP key data is required".to_string(), )); } }; let material = tokio::task::spawn_blocking(move || crate::pgp::import_key(&input)) .await .map_err(|e| AppError::InternalError(format!("PGP import task failed: {e}")))? .map_err(AppError::BadRequest)?; store_material(&session, &config, material, "import", &request.trust).await } pub async fn export_key( Extension(session): Extension, Extension(config): Extension>, Path(fingerprint): Path, Query(query): Query, ) -> Result { let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let normalized = db::pgp::normalize_fingerprint(&fingerprint); let key = database .run(move |conn| db::pgp::get_key(conn, &normalized)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::NotFound("OpenPGP key not found".to_string()))?; let (body, suffix) = if query.secret { if !config.pgp_allow_private_export { return Err(AppError::Forbidden( "Private OpenPGP key export is disabled by administrator policy".to_string(), )); } let path = key .secret_key_path .ok_or_else(|| AppError::BadRequest("This key has no private material".to_string()))?; let body = tokio::task::spawn_blocking(move || crate::pgp::read_secret_key(&path)) .await .map_err(|e| AppError::InternalError(format!("PGP export task failed: {e}")))? .map_err(AppError::InternalError)?; (body, "private") } else { (key.record.public_key_armor, "public") }; let filename = format!("openpgp-{}-{suffix}.asc", key.record.key_id); let mut headers = HeaderMap::new(); headers.insert( "content-type", HeaderValue::from_static("application/pgp-keys; charset=utf-8"), ); headers.insert("cache-control", HeaderValue::from_static("no-store")); headers.insert( "x-content-type-options", HeaderValue::from_static("nosniff"), ); headers.insert( "content-disposition", HeaderValue::from_str(&format!("attachment; filename=\"{filename}\"")) .map_err(|e| AppError::InternalError(format!("Invalid export filename: {e}")))?, ); Ok((headers, body).into_response()) } pub async fn test_passphrase( Extension(session): Extension, Extension(config): Extension>, Path(fingerprint): Path, Json(request): Json, ) -> Result, AppError> { if request.passphrase.len() > 1024 { return Err(AppError::BadRequest( "OpenPGP passphrase is too long".to_string(), )); } let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let normalized = db::pgp::normalize_fingerprint(&fingerprint); let key = database .run(move |conn| db::pgp::get_key(conn, &normalized)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::NotFound("OpenPGP key not found".to_string()))?; let path = key .secret_key_path .ok_or_else(|| AppError::BadRequest("This key has no private material".to_string()))?; let passphrase = request.passphrase; tokio::task::spawn_blocking(move || { let secret = crate::pgp::read_secret_key(&path)?; crate::pgp::test_secret_key_passphrase(&secret, &passphrase) }) .await .map_err(|e| AppError::InternalError(format!("PGP passphrase test task failed: {e}")))? .map_err(|_| { AppError::BadRequest( "OpenPGP private key could not be unlocked with that passphrase".to_string(), ) })?; Ok(Json(TestPassphraseResponse { ok: true })) } pub async fn change_passphrase( Extension(session): Extension, Extension(config): Extension>, Path(fingerprint): Path, Json(request): Json, ) -> Result, AppError> { if request.current_passphrase.len() > 1024 || request.new_passphrase.len() > 1024 { return Err(AppError::BadRequest( "OpenPGP passphrase is too long".to_string(), )); } let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let normalized = db::pgp::normalize_fingerprint(&fingerprint); let lookup = normalized.clone(); let key = database .run(move |conn| db::pgp::get_key(conn, &lookup)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::NotFound("OpenPGP key not found".to_string()))?; let path = key .secret_key_path .ok_or_else(|| AppError::BadRequest("This key has no private material".to_string()))?; let data_dir = config.data_dir.clone(); let user_hash = session.user_hash.clone(); let gpg_binary = std::env::var("RUSTBOX_PGP_GPG_BINARY").unwrap_or_else(|_| "gpg".to_string()); let current = request.current_passphrase; let new_passphrase = request.new_passphrase; tokio::task::spawn_blocking(move || { crate::pgp::change_secret_key_passphrase( &data_dir, &user_hash, &normalized, &path, ¤t, &new_passphrase, &gpg_binary, ) }) .await .map_err(|e| AppError::InternalError(format!("PGP passphrase-change task failed: {e}")))? .map_err(|error| { if error.contains("requires GnuPG") || error.contains("start GnuPG") { AppError::ServiceUnavailable(error) } else { AppError::BadRequest(error) } })?; Ok(Json(TestPassphraseResponse { ok: true })) } pub async fn delete_key( Extension(session): Extension, Extension(config): Extension>, Path(fingerprint): Path, ) -> Result { let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let normalized = db::pgp::normalize_fingerprint(&fingerprint); let lookup = normalized.clone(); let key = database .run(move |conn| db::pgp::get_key(conn, &lookup)) .await .map_err(AppError::InternalError)?; let Some(key) = key else { return Err(AppError::NotFound("OpenPGP key not found".to_string())); }; let delete_fp = normalized.clone(); database .run(move |conn| { conn.execute( crate::queries::sqlite::queries::routes_pgp::Q_001, rusqlite::params![delete_fp], ) .map_err(|e| format!("Failed to detach OpenPGP key from identities: {e}"))?; db::pgp::delete_key(conn, &normalized) }) .await .map_err(AppError::InternalError)?; if let Some(path) = key.secret_key_path { tokio::task::spawn_blocking(move || crate::pgp::delete_secret_key(&path)) .await .map_err(|e| AppError::InternalError(format!("PGP delete task failed: {e}")))? .map_err(AppError::InternalError)?; } Ok(axum::http::StatusCode::NO_CONTENT.into_response()) } pub async fn set_trust( Extension(session): Extension, Extension(config): Extension>, Path(fingerprint): Path, Json(request): Json, ) -> Result, AppError> { validate_trust(&request.trust)?; let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let normalized = db::pgp::normalize_fingerprint(&fingerprint); let trust = request.trust; let update_fp = normalized.clone(); let changed = database .run(move |conn| db::pgp::set_trust(conn, &update_fp, &trust)) .await .map_err(AppError::InternalError)?; if !changed { return Err(AppError::NotFound("OpenPGP key not found".to_string())); } let key = database .run(move |conn| db::pgp::get_key(conn, &normalized)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::NotFound("OpenPGP key not found".to_string()))?; Ok(Json(key.record)) } pub async fn get_preferences( Extension(session): Extension, Extension(config): Extension>, ) -> Result, AppError> { let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let prefs = database .run(db::pgp::get_preferences) .await .map_err(AppError::InternalError)?; Ok(Json(prefs)) } pub async fn update_preferences( Extension(session): Extension, Extension(config): Extension>, Json(mut prefs): Json, ) -> Result, AppError> { if !config.pgp_wkd_enabled { prefs.wkd_enabled = false; } if !config.pgp_hkp_enabled { prefs.hkp_enabled = false; } let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let updated = database .run(move |conn| db::pgp::update_preferences(conn, &prefs)) .await .map_err(AppError::InternalError)?; Ok(Json(updated)) } pub async fn discover_key( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result, AppError> { validate_email(&request.email)?; let email = request.email.trim().to_ascii_lowercase(); let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let prefs = database .run(db::pgp::get_preferences) .await .map_err(AppError::InternalError)?; let mut discovered = None; let mut source = None; if prefs.wkd_enabled && config.pgp_wkd_enabled { match crate::pgp::discovery::discover_wkd( &email, config.pgp_network_timeout_secs, config.pgp_max_key_bytes, ) .await { Ok(Some(bytes)) => { discovered = Some(bytes); source = Some("wkd".to_string()); } Ok(None) => {} Err(error) => tracing::debug!(email, error = %error, "WKD key lookup failed"), } } if discovered.is_none() && prefs.hkp_enabled && config.pgp_hkp_enabled { match crate::pgp::discovery::discover_hkp( &config.pgp_hkp_url, &email, config.pgp_network_timeout_secs, config.pgp_max_key_bytes, ) .await { Ok(Some(bytes)) => { discovered = Some(bytes); source = Some("hkp".to_string()); } Ok(None) => {} Err(error) => tracing::debug!(email, error = %error, "HKP key lookup failed"), } } let Some(bytes) = discovered else { return Ok(Json(DiscoverResponse { found: false, source: None, key: None, })); }; let material = tokio::task::spawn_blocking(move || crate::pgp::import_key(&bytes)) .await .map_err(|e| AppError::InternalError(format!("PGP discovery import task failed: {e}")))? .map_err(AppError::BadRequest)?; if !material.email.is_empty() && !material.email.eq_ignore_ascii_case(&email) { return Err(AppError::BadRequest( "Discovered OpenPGP key does not match the requested email address".to_string(), )); } let source_value = source.clone().unwrap_or_else(|| "discovery".to_string()); let stored = store_material_raw(&session, &config, material, &source_value, "unknown").await?; Ok(Json(DiscoverResponse { found: true, source, key: Some(stored), })) } pub async fn recipient_status( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result>, AppError> { if request.emails.len() > 200 { return Err(AppError::BadRequest( "Too many OpenPGP recipients".to_string(), )); } let emails: Vec = request .emails .into_iter() .map(|value| mailbox_email(&value)) .collect::>()?; let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let result = database .run(move |conn| { let mut statuses = Vec::with_capacity(emails.len()); for email in emails { let key = db::pgp::find_keys_by_email(conn, &email)? .into_iter() .next(); statuses.push(match key { Some(key) => RecipientStatus { email, has_key: true, fingerprint: Some(key.fingerprint), trust: Some(key.trust), source: Some(key.source), }, None => RecipientStatus { email, has_key: false, fingerprint: None, trust: None, source: None, }, }); } Ok(statuses) }) .await .map_err(AppError::InternalError)?; Ok(Json(result)) } pub async fn verify( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result, AppError> { let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let fingerprint = db::pgp::normalize_fingerprint(&request.fingerprint); let lookup = fingerprint.clone(); let key = database .run(move |conn| db::pgp::get_key(conn, &lookup)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::NotFound("OpenPGP key not found".to_string()))?; let public = key.record.public_key_armor; let data = request.data; let signature = request.signature; let message = request.message; let output = tokio::task::spawn_blocking(move || { if let Some(signature) = signature { crate::pgp::verify_detached(data.as_bytes(), &signature, &public)?; Ok::, String>(None) } else if let Some(message) = message { crate::pgp::verify_inline(&message, &public).map(Some) } else { Err("Either signature or inline message is required".to_string()) } }) .await .map_err(|e| AppError::InternalError(format!("PGP verification task failed: {e}")))? .map_err(AppError::BadRequest)?; Ok(Json(VerifyResponse { verified: true, plaintext: output, fingerprint, })) } pub async fn decrypt( Extension(session): Extension, Extension(config): Extension>, Json(request): Json, ) -> Result, AppError> { if request.passphrase.len() > 1024 { return Err(AppError::BadRequest( "OpenPGP passphrase is too long".to_string(), )); } let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let requested = request .fingerprint .map(|value| db::pgp::normalize_fingerprint(&value)); let candidates = database .run(move |conn| { if let Some(fingerprint) = requested { Ok(db::pgp::get_key(conn, &fingerprint)? .into_iter() .collect::>()) } else { db::pgp::list_secret_keys(conn) } }) .await .map_err(AppError::InternalError)?; if candidates.is_empty() { return Err(AppError::BadRequest( "No OpenPGP private key is available".to_string(), )); } let public_armors = database .run(|conn| { Ok(db::pgp::list_keys(conn)? .into_iter() .map(|key| key.public_key_armor) .collect::>()) }) .await .map_err(AppError::InternalError)?; let message = request.message; let passphrase = request.passphrase; let result = tokio::task::spawn_blocking(move || { let mut last_error = "No OpenPGP private key could decrypt this message".to_string(); for candidate in candidates { let Some(path) = candidate.secret_key_path else { continue; }; let secret = match crate::pgp::read_secret_key(&path) { Ok(value) => value, Err(error) => { last_error = error; continue; } }; match crate::pgp::decrypt_inline(&message, &secret, &passphrase, &public_armors) { Ok(value) => return Ok(value), Err(error) => last_error = error, } } Err(last_error) }) .await .map_err(|e| AppError::InternalError(format!("PGP decryption task failed: {e}")))? .map_err(AppError::BadRequest)?; Ok(Json(result)) } async fn store_material( session: &SessionState, config: &AppConfig, material: crate::pgp::ImportedKey, source: &str, trust: &str, ) -> Result, AppError> { Ok(Json( store_material_raw(session, config, material, source, trust).await?, )) } async fn store_material_raw( session: &SessionState, config: &AppConfig, material: crate::pgp::ImportedKey, source: &str, trust: &str, ) -> Result { validate_trust(trust)?; let secret_store = if let Some(secret_armor) = material.secret_armor.clone() { let data_dir = config.data_dir.clone(); let user_hash = session.user_hash.clone(); let fingerprint = material.fingerprint.clone(); Some( tokio::task::spawn_blocking(move || { let target = crate::pgp::private_key_dir(&data_dir, &user_hash) .join(format!("{fingerprint}.asc")); let existed = target.exists(); crate::pgp::store_secret_key(&data_dir, &user_hash, &fingerprint, &secret_armor) .map(|path| (path, existed)) }) .await .map_err(|e| AppError::InternalError(format!("PGP key storage task failed: {e}")))? .map_err(AppError::InternalError)?, ) } else { None }; let secret_path = secret_store.as_ref().map(|(path, _)| path.clone()); let record = PgpKeyRecord { fingerprint: material.fingerprint, key_id: material.key_id, email: material.email, user_id: material.user_id, public_key_armor: material.public_armor, has_secret: secret_path.is_some(), trust: trust.to_string(), source: source.to_string(), created_at: String::new(), updated_at: String::new(), }; let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash); let stored = record.clone(); let path = secret_path.clone(); if let Err(error) = database .run(move |conn| db::pgp::upsert_key(conn, &stored, path.as_deref())) .await { if let Some((path, existed)) = secret_store && !existed { let _ = tokio::task::spawn_blocking(move || crate::pgp::delete_secret_key(&path)).await; } return Err(AppError::InternalError(error)); } let lookup = record.fingerprint.clone(); let key = database .run(move |conn| db::pgp::get_key(conn, &lookup)) .await .map_err(AppError::InternalError)? .ok_or_else(|| AppError::InternalError("Stored OpenPGP key disappeared".to_string()))?; Ok(key.record) } fn validate_trust(value: &str) -> Result<(), AppError> { if matches!(value, "unknown" | "untrusted" | "trusted" | "verified") { Ok(()) } else { Err(AppError::BadRequest( "Trust must be unknown, untrusted, trusted, or verified".to_string(), )) } } fn validate_email(value: &str) -> Result<(), AppError> { let email = value.trim(); if email.len() > 320 || !email.contains('@') || email .chars() .any(|ch| matches!(ch, '\r' | '\n' | '<' | '>')) { return Err(AppError::BadRequest("Invalid email address".to_string())); } Ok(()) } fn mailbox_email(value: &str) -> Result { let mailbox: lettre::message::Mailbox = value .parse() .map_err(|_| AppError::BadRequest(format!("Invalid email address: {value}")))?; Ok(mailbox.email.to_string().to_ascii_lowercase()) }