Skip to main content

aegis_server/
vault_handlers.rs

1//! Aegis Vault API Handlers
2//!
3//! REST API handlers for the integrated secrets vault.
4
5use crate::state::AppState;
6use axum::{
7    extract::{Path, Query, State},
8    http::StatusCode,
9    response::IntoResponse,
10    Json,
11};
12use serde::{Deserialize, Serialize};
13
14
15// =============================================================================
16// Request/Response Types
17// =============================================================================
18
19#[derive(Deserialize)]
20pub struct UnsealRequest {
21    pub passphrase: String,
22}
23
24#[derive(Deserialize)]
25pub struct SetSecretRequest {
26    pub value: String,
27}
28
29#[derive(Deserialize)]
30pub struct TransitEncryptRequest {
31    pub key_name: String,
32    pub plaintext: String, // base64 encoded
33}
34
35#[derive(Deserialize)]
36pub struct TransitDecryptRequest {
37    pub key_name: String,
38    pub ciphertext: String, // base64 encoded
39}
40
41#[derive(Deserialize)]
42pub struct CreateTransitKeyRequest {
43    pub name: String,
44}
45
46#[derive(Deserialize, Default)]
47pub struct ListQuery {
48    pub prefix: Option<String>,
49    pub limit: Option<usize>,
50}
51
52#[derive(Serialize)]
53pub struct VaultStatusResponse {
54    pub sealed: bool,
55    pub secret_count: usize,
56    pub transit_key_count: usize,
57    pub uptime_secs: Option<u64>,
58}
59
60// =============================================================================
61// Handlers
62// =============================================================================
63
64/// GET /api/v1/vault/status
65pub async fn vault_status(State(state): State<AppState>) -> impl IntoResponse {
66    let status = state.vault.status();
67    Json(serde_json::json!({
68        "sealed": status.sealed,
69        "secret_count": status.secret_count,
70        "transit_key_count": status.transit_key_count,
71        "uptime_secs": status.uptime_secs,
72    }))
73}
74
75/// POST /api/v1/vault/unseal
76pub async fn vault_unseal(
77    State(state): State<AppState>,
78    Json(req): Json<UnsealRequest>,
79) -> impl IntoResponse {
80    match state.vault.unseal(&req.passphrase) {
81        Ok(()) => (
82            StatusCode::OK,
83            Json(serde_json::json!({"status": "unsealed"})),
84        ),
85        Err(e) => (
86            StatusCode::FORBIDDEN,
87            Json(serde_json::json!({"error": e.to_string()})),
88        ),
89    }
90}
91
92/// POST /api/v1/vault/seal
93pub async fn vault_seal(State(state): State<AppState>) -> impl IntoResponse {
94    match state.vault.seal() {
95        Ok(()) => (
96            StatusCode::OK,
97            Json(serde_json::json!({"status": "sealed"})),
98        ),
99        Err(e) => (
100            StatusCode::INTERNAL_SERVER_ERROR,
101            Json(serde_json::json!({"error": e.to_string()})),
102        ),
103    }
104}
105
106/// GET /api/v1/vault/secrets
107pub async fn list_secrets(
108    State(state): State<AppState>,
109    Query(params): Query<ListQuery>,
110) -> impl IntoResponse {
111    let prefix = params.prefix.as_deref().unwrap_or("");
112    match state.vault.list(prefix, "api") {
113        Ok(keys) => (StatusCode::OK, Json(serde_json::json!({"keys": keys}))),
114        Err(e) => (
115            StatusCode::INTERNAL_SERVER_ERROR,
116            Json(serde_json::json!({"error": e.to_string()})),
117        ),
118    }
119}
120
121/// GET /api/v1/vault/secrets/:key
122pub async fn get_secret(
123    State(state): State<AppState>,
124    Path(key): Path<String>,
125) -> impl IntoResponse {
126    match state.vault.get(&key, "api") {
127        Ok(value) => (
128            StatusCode::OK,
129            Json(serde_json::json!({"key": key, "value": value})),
130        ),
131        Err(e) => (
132            StatusCode::NOT_FOUND,
133            Json(serde_json::json!({"error": e.to_string()})),
134        ),
135    }
136}
137
138/// PUT /api/v1/vault/secrets/:key
139pub async fn set_secret(
140    State(state): State<AppState>,
141    Path(key): Path<String>,
142    Json(req): Json<SetSecretRequest>,
143) -> impl IntoResponse {
144    match state.vault.set(&key, &req.value, "api") {
145        Ok(()) => (
146            StatusCode::OK,
147            Json(serde_json::json!({"status": "ok", "key": key})),
148        ),
149        Err(e) => (
150            StatusCode::INTERNAL_SERVER_ERROR,
151            Json(serde_json::json!({"error": e.to_string()})),
152        ),
153    }
154}
155
156/// DELETE /api/v1/vault/secrets/:key
157pub async fn delete_secret(
158    State(state): State<AppState>,
159    Path(key): Path<String>,
160) -> impl IntoResponse {
161    match state.vault.delete(&key, "api") {
162        Ok(()) => (
163            StatusCode::OK,
164            Json(serde_json::json!({"status": "deleted", "key": key})),
165        ),
166        Err(e) => (
167            StatusCode::NOT_FOUND,
168            Json(serde_json::json!({"error": e.to_string()})),
169        ),
170    }
171}
172
173/// POST /api/v1/vault/transit/encrypt
174pub async fn transit_encrypt(
175    State(state): State<AppState>,
176    Json(req): Json<TransitEncryptRequest>,
177) -> impl IntoResponse {
178    match state
179        .vault
180        .transit_encrypt(&req.key_name, req.plaintext.as_bytes())
181    {
182        Ok(ciphertext) => {
183            let encoded = data_encoding_hex(&ciphertext);
184            (
185                StatusCode::OK,
186                Json(serde_json::json!({"ciphertext": encoded})),
187            )
188        }
189        Err(e) => (
190            StatusCode::INTERNAL_SERVER_ERROR,
191            Json(serde_json::json!({"error": e.to_string()})),
192        ),
193    }
194}
195
196/// POST /api/v1/vault/transit/decrypt
197pub async fn transit_decrypt(
198    State(state): State<AppState>,
199    Json(req): Json<TransitDecryptRequest>,
200) -> impl IntoResponse {
201    match hex_decode(&req.ciphertext) {
202        Ok(ciphertext) => match state.vault.transit_decrypt(&req.key_name, &ciphertext) {
203            Ok(plaintext) => {
204                let text = String::from_utf8_lossy(&plaintext).to_string();
205                (StatusCode::OK, Json(serde_json::json!({"plaintext": text})))
206            }
207            Err(e) => (
208                StatusCode::INTERNAL_SERVER_ERROR,
209                Json(serde_json::json!({"error": e.to_string()})),
210            ),
211        },
212        Err(e) => (
213            StatusCode::BAD_REQUEST,
214            Json(serde_json::json!({"error": format!("Invalid hex: {}", e)})),
215        ),
216    }
217}
218
219/// POST /api/v1/vault/transit/keys
220pub async fn create_transit_key(
221    State(state): State<AppState>,
222    Json(req): Json<CreateTransitKeyRequest>,
223) -> impl IntoResponse {
224    match state.vault.transit_create_key(&req.name) {
225        Ok(()) => (
226            StatusCode::CREATED,
227            Json(serde_json::json!({"status": "created", "key_name": req.name})),
228        ),
229        Err(e) => (
230            StatusCode::INTERNAL_SERVER_ERROR,
231            Json(serde_json::json!({"error": e.to_string()})),
232        ),
233    }
234}
235
236/// GET /api/v1/vault/transit/keys
237pub async fn list_transit_keys(State(state): State<AppState>) -> impl IntoResponse {
238    let keys = state.vault.transit_list_keys();
239    Json(serde_json::json!({"keys": keys}))
240}
241
242/// GET /api/v1/vault/audit
243pub async fn vault_audit(
244    State(state): State<AppState>,
245    Query(params): Query<ListQuery>,
246) -> impl IntoResponse {
247    let limit = params.limit.unwrap_or(100);
248    let entries = state.vault.audit_entries(limit);
249    Json(serde_json::json!({"entries": entries}))
250}
251
252// =============================================================================
253// Helpers
254// =============================================================================
255
256fn data_encoding_hex(data: &[u8]) -> String {
257    data.iter().map(|b| format!("{:02x}", b)).collect()
258}
259
260fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
261    if hex.len() % 2 != 0 {
262        return Err("odd length".to_string());
263    }
264    (0..hex.len())
265        .step_by(2)
266        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| e.to_string()))
267        .collect()
268}