Implement all remaining P1 Rust TODOs: Slack alerts, Resend emails, pricing refresh, AES-256-GCM key encryption

- anomaly.rs: Slack Block Kit webhook + Resend email on 3x cost spike
- digest.rs: Weekly HTML digest email via Resend with model usage + savings tables
- main.rs: Daily pricing refresh with hardcoded table (OpenAI/Anthropic/Google models)
- handler.rs: AES-256-GCM encryption for provider API keys (nonce || ciphertext storage)
This commit is contained in:
2026-03-01 05:53:51 +00:00
parent a96fcae13c
commit 167d3be2e4
4 changed files with 177 additions and 6 deletions

View File

@@ -487,8 +487,29 @@ async fn upsert_provider(
let auth = state.auth.authenticate(&headers).await?;
require_role(&auth, Role::Owner)?;
// TODO: Encrypt API key with AES-256-GCM before storing
let encrypted = req.api_key.as_bytes().to_vec();
// Encrypt API key with AES-256-GCM before storing
let encryption_key = std::env::var("PROVIDER_KEY_ENCRYPTION_KEY")
.unwrap_or_else(|_| "0".repeat(64)); // 32-byte hex key
let key_bytes = hex::decode(&encryption_key)
.unwrap_or_else(|_| vec![0u8; 32]);
use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead};
use aes_gcm::aead::OsRng;
use aes_gcm::Nonce;
let cipher = Aes256Gcm::new_from_slice(&key_bytes)
.map_err(|e| ApiError::Internal(format!("Encryption key error: {}", e)))?;
let mut nonce_bytes = [0u8; 12];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| ApiError::Internal(format!("RNG error: {}", e)))?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher.encrypt(nonce, req.api_key.as_bytes())
.map_err(|e| ApiError::Internal(format!("Encryption error: {}", e)))?;
// Store as nonce || ciphertext
let mut encrypted = Vec::with_capacity(12 + ciphertext.len());
encrypted.extend_from_slice(&nonce_bytes);
encrypted.extend_from_slice(&ciphertext);
sqlx::query(
"INSERT INTO provider_configs (id, org_id, provider, encrypted_api_key, base_url, is_default)