Files
RustBox/src/routes/pgp.rs
T
Mateusz Gruszczyński 425ea7449b
RustBox CI / rustbox (push) Canceled after 0s
v0.1.0
2026-10-30 17:22:17 +01:00

891 lines
30 KiB
Rust

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<String>,
#[serde(default)]
pub key_data_base64: Option<String>,
#[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<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RecipientStatus {
pub email: String,
pub has_key: bool,
pub fingerprint: Option<String>,
pub trust: Option<String>,
pub source: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyRequest {
pub data: String,
pub signature: Option<String>,
pub message: Option<String>,
pub fingerprint: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyResponse {
pub verified: bool,
pub plaintext: Option<String>,
pub fingerprint: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DecryptRequest {
pub message: String,
#[serde(default)]
pub passphrase: String,
pub fingerprint: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverResponse {
pub found: bool,
pub source: Option<String>,
pub key: Option<PgpKeyRecord>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessMessageRequest {
#[serde(default)]
pub passphrase: String,
pub fingerprint: Option<String>,
}
#[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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Extension(imap_client): Extension<Arc<dyn ImapClient>>,
Path((folder, uid)): Path<(String, u32)>,
Json(request): Json<ProcessMessageRequest>,
) -> Result<Json<crate::pgp::ProcessedMessage>, 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::<Vec<_>>())
} 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::<Vec<_>>())
})
.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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
) -> Result<Json<Vec<PgpKeyRecord>>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<GenerateRequest>,
) -> Result<Json<PgpKeyRecord>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<ImportRequest>,
) -> Result<Json<PgpKeyRecord>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
Query(query): Query<ExportQuery>,
) -> Result<Response, AppError> {
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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
Json(request): Json<TestPassphraseRequest>,
) -> Result<Json<TestPassphraseResponse>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
Json(request): Json<ChangePassphraseRequest>,
) -> Result<Json<TestPassphraseResponse>, 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,
&current,
&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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
) -> Result<Response, AppError> {
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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
Json(request): Json<TrustRequest>,
) -> Result<Json<PgpKeyRecord>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
) -> Result<Json<PgpPreferences>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(mut prefs): Json<PgpPreferences>,
) -> Result<Json<PgpPreferences>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<DiscoverRequest>,
) -> Result<Json<DiscoverResponse>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<RecipientsRequest>,
) -> Result<Json<Vec<RecipientStatus>>, AppError> {
if request.emails.len() > 200 {
return Err(AppError::BadRequest(
"Too many OpenPGP recipients".to_string(),
));
}
let emails: Vec<String> = request
.emails
.into_iter()
.map(|value| mailbox_email(&value))
.collect::<Result<_, _>>()?;
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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<VerifyRequest>,
) -> Result<Json<VerifyResponse>, 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::<Option<String>, 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<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<DecryptRequest>,
) -> Result<Json<crate::pgp::DecryptResult>, 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::<Vec<_>>())
} 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::<Vec<_>>())
})
.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<Json<PgpKeyRecord>, 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<PgpKeyRecord, AppError> {
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<String, AppError> {
let mailbox: lettre::message::Mailbox = value
.parse()
.map_err(|_| AppError::BadRequest(format!("Invalid email address: {value}")))?;
Ok(mailbox.email.to_string().to_ascii_lowercase())
}