use crate::server::APPLICATION;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use bytes::Bytes;
use chacha20poly1305::{
XChaCha20Poly1305, XNonce,
aead::{Aead, KeyInit},
};
use ordinary_config::SecretSource;
use serde::Deserialize;
use std::sync::Arc;
use tracing::Instrument;
use utoipa::IntoParams;
use x25519_dalek::PublicKey;
#[derive(Deserialize, IntoParams)]
pub struct StoreParams {
d: String,
n: String,
}
const MIN_BODY_LEN: usize = 32 + 24 + 16 + 1;
const ZEROED_KEY: [u8; 32] = [0u8; 32];
#[utoipa::path(
put,
path = "/secrets",
tag = APPLICATION,
request_body(content = [u8], content_type = "application/octet-stream"),
params(StoreParams),
responses(
(status = 401, description = "unauthorized for operation"),
(status = 200, description = "update an application secret"),
),
security(
("access" = []),
),
)]
pub async fn store(
State(state): State<Arc<crate::server::OrdinaryApiServerState>>,
Query(StoreParams { d, n }): Query<StoreParams>,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
let domain = d;
let span = tracing::info_span!("app", %domain);
let span = span.in_scope(|| tracing::info_span!("secrets"));
let span = span.in_scope(|| tracing::info_span!("store"));
async {
if body.len() < MIN_BODY_LEN {
tracing::error!(secret = %n, "too small");
return StatusCode::BAD_REQUEST.into_response();
}
if let Ok(max_size) = usize::try_from(state.config.limits.storage.secrets.max_size)
&& body.len() > max_size + MIN_BODY_LEN - 1
{
tracing::error!(secret = %n, "too big");
return StatusCode::PAYLOAD_TOO_LARGE.into_response();
}
let account = match crate::server::check_ordinary_auth(&state, &headers, 0, &domain) {
Ok(account) => account,
Err(code) => return code.into_response(),
};
let client_public_key: [u8; 32] = match (&body[0..32]).try_into() {
Ok(v) => v,
Err(err) => {
tracing::error!(%err);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
if client_public_key == ZEROED_KEY {
return StatusCode::BAD_REQUEST.into_response();
}
tracing::info!(account, "storing");
let apps = state.apps.read().await;
if let Some(wrapped_app) = apps.get(&domain) {
if wrapped_app.app.config.secrets.is_none() {
tracing::error!("no secrets configured for app");
return StatusCode::NOT_FOUND.into_response();
}
let mut doesnt_exist = true;
if let Some(secrets) = &wrapped_app.app.config.secrets {
for secret in secrets {
if let SecretSource::Stored = secret.source
&& secret.name == n
{
doesnt_exist = false;
break;
}
}
}
if doesnt_exist {
tracing::error!(secret = %n, "not found");
return StatusCode::NOT_FOUND.into_response();
}
let public_key = PublicKey::from(client_public_key);
let shared_secret = wrapped_app.dh_keypair.0.diffie_hellman(&public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from_slice(&body[32..32 + 24]);
let Ok(secret) = cipher.decrypt(nonce, &body[32 + 24..]) else {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
};
if let Err(err) = wrapped_app.app.put_secret(n.as_str(), &secret) {
tracing::error!(%err, "failed to store secret");
} else {
tracing::info!("stored secret");
return StatusCode::OK.into_response();
}
}
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
.instrument(span)
.await
}