use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes256Gcm, Nonce};
use arc_swap::ArcSwap;
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
use tracing::warn;
use zeroize::Zeroizing;
#[derive(Debug, Error)]
pub enum FleetSecretsError {
#[error("master key derivation failed: {0}")]
MasterKey(String),
#[error("encryption failed: {0}")]
Encrypt(String),
#[error("decryption failed: {0}")]
Decrypt(String),
#[error("ciphertext is malformed: {0}")]
Malformed(String),
#[error("master key is not configured; fleet-secrets lookups return None")]
MasterKeyUnset,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProviderOverride {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_base: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit_rpm: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit_tpm: Option<i32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fallback_providers: Vec<String>,
#[serde(default)]
pub updated_at: i64,
#[serde(default)]
pub updated_by: String,
}
#[derive(Debug, Clone, Default)]
pub struct FleetSecrets {
inner: Arc<ArcSwap<FleetSecretsInner>>,
}
#[derive(Debug, Default)]
struct FleetSecretsInner {
providers: HashMap<String, ProviderOverride>,
}
impl FleetSecrets {
pub fn new() -> Self {
Self::default()
}
pub fn from_providers(providers: HashMap<String, ProviderOverride>) -> Self {
Self {
inner: Arc::new(ArcSwap::from_pointee(FleetSecretsInner { providers })),
}
}
pub fn store(&self, providers: HashMap<String, ProviderOverride>) {
self.inner.store(Arc::new(FleetSecretsInner { providers }));
}
pub fn get(&self, provider_name: &str) -> Option<ProviderOverride> {
self.inner.load().providers.get(provider_name).cloned()
}
pub fn list(&self) -> Vec<(String, ProviderOverride)> {
self.inner
.load()
.providers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn len(&self) -> usize {
self.inner.load().providers.len()
}
pub fn is_empty(&self) -> bool {
self.inner.load().providers.is_empty()
}
}
impl cordis::Service for FleetSecrets {
fn name(&self) -> &'static str {
"fleet_secrets"
}
fn init(&self, _ctx: &std::sync::Arc<cordis::Context>) -> cordis::ServiceInitFuture<'_> {
Box::pin(async { Ok(None) })
}
fn check(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedPayload {
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct MasterKey {
bytes: Arc<Zeroizing<[u8; 32]>>,
}
impl MasterKey {
pub fn from_env() -> Option<Self> {
match std::env::var("FLEET_SECRETS_KEY") {
Ok(raw) if !raw.is_empty() => Some(Self::from_secret(&raw)),
_ => {
warn!(
"FLEET_SECRETS_KEY is not set; fleet provider secrets will be disabled. \
Set it to a >=32-char random string in /etc/dirmacs/fleet-secrets.env \
and reload ares.service to enable encrypted provider overrides."
);
None
}
}
}
pub fn from_secret(secret: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
let digest = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&digest);
Self {
bytes: Arc::new(Zeroizing::new(bytes)),
}
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.bytes
}
}
pub fn encrypt_api_key(
plaintext: &str,
master: &MasterKey,
) -> Result<EncryptedPayload, FleetSecretsError> {
let cipher = Aes256Gcm::new_from_slice(master.as_bytes())
.map_err(|e| FleetSecretsError::Encrypt(e.to_string()))?;
let mut nonce_bytes = [0u8; 12];
rand::rng().fill(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(
nonce,
Payload {
msg: plaintext.as_bytes(),
aad: b"ares.fleet_secrets.v1",
},
)
.map_err(|e| FleetSecretsError::Encrypt(e.to_string()))?;
Ok(EncryptedPayload {
nonce: nonce_bytes.to_vec(),
ciphertext,
})
}
pub fn decrypt_api_key(
payload: &EncryptedPayload,
master: &MasterKey,
) -> Result<String, FleetSecretsError> {
if payload.nonce.len() != 12 {
return Err(FleetSecretsError::Malformed(format!(
"nonce must be 12 bytes, got {}",
payload.nonce.len()
)));
}
if payload.ciphertext.is_empty() {
return Err(FleetSecretsError::Malformed("ciphertext is empty".into()));
}
let cipher = Aes256Gcm::new_from_slice(master.as_bytes())
.map_err(|e| FleetSecretsError::Decrypt(e.to_string()))?;
let nonce = Nonce::from_slice(&payload.nonce);
let plaintext = cipher
.decrypt(
nonce,
Payload {
msg: &payload.ciphertext,
aad: b"ares.fleet_secrets.v1",
},
)
.map_err(|e| FleetSecretsError::Decrypt(e.to_string()))?;
String::from_utf8(plaintext).map_err(|e| FleetSecretsError::Decrypt(e.to_string()))
}
pub fn last_n_visible(key: &str, n: usize) -> Option<String> {
if key.is_empty() {
return None;
}
let len = key.chars().count();
if len <= n {
return Some(key.to_string());
}
let start_byte = key.char_indices().nth(len - n).map(|(i, _)| i).unwrap_or(0);
Some(format!("…{}", &key[start_byte..]))
}
#[cfg(test)]
pub fn hex_key(master: &MasterKey) -> String {
hex::encode(master.as_bytes().as_slice())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_encrypt_decrypt() {
let master = MasterKey::from_secret("test-secret");
let plaintext = "nvapi-abc123-XYZ";
let payload = encrypt_api_key(plaintext, &master).expect("encrypt");
assert_eq!(payload.nonce.len(), 12);
assert!(!payload.ciphertext.is_empty());
let decrypted = decrypt_api_key(&payload, &master).expect("decrypt");
assert_eq!(decrypted, plaintext);
}
#[test]
fn wrong_key_fails_decrypt() {
let a = MasterKey::from_secret("key-a");
let b = MasterKey::from_secret("key-b");
let payload = encrypt_api_key("secret", &a).expect("encrypt");
let result = decrypt_api_key(&payload, &b);
assert!(result.is_err(), "wrong master key must fail to decrypt");
}
#[test]
fn tampered_ciphertext_fails() {
let master = MasterKey::from_secret("key");
let mut payload = encrypt_api_key("secret", &master).expect("encrypt");
let mid = payload.ciphertext.len() / 2;
payload.ciphertext[mid] ^= 0xFF;
let result = decrypt_api_key(&payload, &master);
assert!(result.is_err(), "tampered ciphertext must fail to decrypt");
}
#[test]
fn malformed_nonce_rejected() {
let master = MasterKey::from_secret("key");
let payload = EncryptedPayload {
nonce: vec![0; 8], ciphertext: vec![1, 2, 3],
};
let result = decrypt_api_key(&payload, &master);
assert!(result.is_err());
}
#[test]
fn empty_ciphertext_rejected() {
let master = MasterKey::from_secret("key");
let payload = EncryptedPayload {
nonce: vec![0; 12],
ciphertext: vec![],
};
let result = decrypt_api_key(&payload, &master);
assert!(result.is_err());
}
#[test]
fn unique_nonce_per_encryption() {
let master = MasterKey::from_secret("key");
let a = encrypt_api_key("same", &master).expect("a");
let b = encrypt_api_key("same", &master).expect("b");
assert_ne!(a.nonce, b.nonce, "nonces must be unique");
}
#[test]
fn last_n_visible_truncates() {
assert_eq!(
last_n_visible("nvapi-abc12345XYZ", 4),
Some("…5XYZ".to_string())
);
assert_eq!(
last_n_visible("nvapi-abc", 8),
Some("…vapi-abc".to_string())
);
assert_eq!(
last_n_visible("nvapi-abc", 10),
Some("nvapi-abc".to_string())
);
assert_eq!(last_n_visible("", 4), None);
}
#[test]
fn fleet_secrets_swap_is_visible_to_readers() {
let secrets = FleetSecrets::new();
assert!(secrets.get("nvidia").is_none());
assert!(secrets.is_empty());
let mut map = HashMap::new();
map.insert(
"nvidia".to_string(),
ProviderOverride {
api_key: Some("nvapi-X".into()),
api_base: None,
default_model: Some("meta/llama-3.3-70b-instruct".into()),
updated_at: 1,
updated_by: "admin".into(),
..Default::default()
},
);
secrets.store(map);
let entry = secrets.get("nvidia").expect("entry present");
assert_eq!(entry.api_key.as_deref(), Some("nvapi-X"));
assert_eq!(secrets.len(), 1);
secrets.store(HashMap::new());
assert!(secrets.get("nvidia").is_none());
assert!(secrets.is_empty());
}
#[test]
fn from_env_returns_none_when_unset() {
let prev = std::env::var("FLEET_SECRETS_KEY").ok();
std::env::remove_var("FLEET_SECRETS_KEY");
assert!(MasterKey::from_env().is_none());
if let Some(p) = prev {
std::env::set_var("FLEET_SECRETS_KEY", p);
}
}
#[test]
fn from_env_resolves_when_set() {
std::env::set_var("FLEET_SECRETS_KEY", "test-only-secret-not-real");
let m = MasterKey::from_env().expect("key resolves");
let expected = MasterKey::from_secret("test-only-secret-not-real");
assert_eq!(m.as_bytes(), expected.as_bytes());
}
}