#[cfg(feature = "aws-secrets")]
pub mod aws;
#[cfg(feature = "azure-secrets")]
pub mod azure;
#[cfg(feature = "gcp-secrets")]
pub mod gcp;
#[cfg(feature = "k8s-secrets")]
pub mod k8s;
#[cfg(feature = "keyring")]
mod keyring;
mod plaintext;
#[cfg(feature = "vault-secrets")]
pub mod vault;
#[cfg(feature = "setup-wizard")]
pub mod wizard;
#[cfg(feature = "aws-secrets")]
pub use aws::AwsSecretStore;
#[cfg(feature = "azure-secrets")]
pub use azure::AzureKeyVaultStore;
#[cfg(feature = "gcp-secrets")]
pub use gcp::GcpSecretStore;
#[cfg(feature = "k8s-secrets")]
pub use k8s::K8sSecretStore;
#[cfg(feature = "keyring")]
pub use keyring::KeyringSecretStore;
#[cfg(feature = "vault-secrets")]
pub use vault::VaultSecretStore;
use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
use crate::server::config::SecretsConfig;
use crate::server::error::AppError;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Clone, Serialize, Deserialize)]
pub struct ServerSecrets {
pub signing_key: String,
pub key_agreement_key: String,
pub jwt_signing_key: String,
#[serde(default)]
pub vta_credential: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retired: Vec<RetiredKeys>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct RetiredKeys {
pub ka_kid: String,
pub key_agreement_key: String,
pub signing_kid: String,
pub signing_key: String,
}
impl std::fmt::Debug for RetiredKeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RetiredKeys")
.field("ka_kid", &self.ka_kid)
.field("key_agreement_key", &"<redacted>")
.field("signing_kid", &self.signing_kid)
.field("signing_key", &"<redacted>")
.finish()
}
}
impl std::fmt::Debug for ServerSecrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerSecrets")
.field("signing_key", &"<redacted>")
.field("key_agreement_key", &"<redacted>")
.field("jwt_signing_key", &"<redacted>")
.field(
"vta_credential",
&self.vta_credential.as_ref().map(|_| "<redacted>"),
)
.field("retired", &self.retired)
.finish()
}
}
#[cfg(any(
feature = "aws-secrets",
feature = "gcp-secrets",
feature = "azure-secrets",
feature = "vault-secrets",
feature = "k8s-secrets"
))]
#[derive(Default, Clone, Serialize, Deserialize)]
pub(crate) struct StoredSecrets {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub secrets: Option<ServerSecrets>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bootstrap_seed: Option<String>,
}
#[cfg(any(
feature = "aws-secrets",
feature = "gcp-secrets",
feature = "azure-secrets",
feature = "vault-secrets",
feature = "k8s-secrets"
))]
impl std::fmt::Debug for StoredSecrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoredSecrets")
.field("secrets", &self.secrets)
.field(
"bootstrap_seed",
&self.bootstrap_seed.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
#[cfg(any(
feature = "aws-secrets",
feature = "gcp-secrets",
feature = "azure-secrets",
feature = "vault-secrets",
feature = "k8s-secrets"
))]
impl StoredSecrets {
pub(crate) fn parse(json: &str) -> Result<Self, serde_json::Error> {
if let Ok(bare) = serde_json::from_str::<ServerSecrets>(json) {
return Ok(Self {
secrets: Some(bare),
bootstrap_seed: None,
});
}
serde_json::from_str(json)
}
pub(crate) fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[allow(dead_code)] pub(crate) fn encode_seed(seed: &[u8; 32]) -> String {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
B64.encode(seed)
}
#[allow(dead_code)] pub(crate) fn decode_seed(b64: &str) -> Result<[u8; 32], AppError> {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
let bytes = B64.decode(b64.trim().as_bytes()).map_err(|e| {
AppError::SecretStore(format!("failed to base64-decode bootstrap seed: {e}"))
})?;
bytes.as_slice().try_into().map_err(|_| {
AppError::SecretStore(format!(
"bootstrap seed has {} bytes, expected 32",
bytes.len()
))
})
}
}
pub trait SecretStore: Send + Sync {
fn get(&self) -> BoxFuture<'_, Result<Option<ServerSecrets>, AppError>>;
fn set(&self, secrets: &ServerSecrets) -> BoxFuture<'_, Result<(), AppError>>;
fn get_bootstrap_seed(&self) -> BoxFuture<'_, Result<Option<[u8; 32]>, AppError>>;
fn set_bootstrap_seed(&self, seed: &[u8; 32]) -> BoxFuture<'_, Result<(), AppError>>;
fn clear_bootstrap_seed(&self) -> BoxFuture<'_, Result<(), AppError>>;
}
#[allow(unused_variables)]
pub fn is_plaintext_backend(secrets: &SecretsConfig) -> bool {
#[cfg(feature = "aws-secrets")]
if secrets.aws_secret_name.is_some() {
return false;
}
#[cfg(feature = "gcp-secrets")]
if secrets.gcp_secret_name.is_some() {
return false;
}
#[cfg(feature = "azure-secrets")]
if secrets.azure_secret_name.is_some() {
return false;
}
#[cfg(feature = "vault-secrets")]
if secrets.vault_addr.is_some() {
return false;
}
#[cfg(feature = "k8s-secrets")]
if secrets.k8s_secret_name.is_some() {
return false;
}
if secrets.plaintext_mode {
return true;
}
#[cfg(feature = "keyring")]
{
return false;
}
#[allow(unreachable_code)]
true
}
#[allow(unused_variables)]
pub fn create_secret_store(
secrets: &SecretsConfig,
config_path: &std::path::Path,
) -> Result<Box<dyn SecretStore>, AppError> {
#[cfg(feature = "aws-secrets")]
if secrets.aws_secret_name.is_some() {
let store = AwsSecretStore::new(
secrets.aws_secret_name.clone().unwrap(),
secrets.aws_region.clone(),
);
return Ok(Box::new(store));
}
#[cfg(feature = "gcp-secrets")]
if secrets.gcp_secret_name.is_some() {
let project = secrets.gcp_project.clone().ok_or_else(|| {
AppError::Config(
"secrets.gcp_project is required when secrets.gcp_secret_name is set".into(),
)
})?;
let store = GcpSecretStore::new(project, secrets.gcp_secret_name.clone().unwrap());
return Ok(Box::new(store));
}
#[cfg(feature = "azure-secrets")]
if secrets.azure_secret_name.is_some() {
let vault_url = secrets.azure_vault_url.clone().ok_or_else(|| {
AppError::Config(
"secrets.azure_vault_url is required when secrets.azure_secret_name is set".into(),
)
})?;
let store = AzureKeyVaultStore::new(vault_url, secrets.azure_secret_name.clone().unwrap());
return Ok(Box::new(store));
}
#[cfg(feature = "vault-secrets")]
if secrets.vault_addr.is_some() {
let store = vault::from_config(secrets)?;
return Ok(Box::new(store));
}
#[cfg(feature = "k8s-secrets")]
if secrets.k8s_secret_name.is_some() {
let store = k8s::from_config(secrets)?;
return Ok(Box::new(store));
}
if secrets.plaintext_mode {
let store = plaintext::PlaintextSecretStore::new(
secrets.plaintext.as_ref(),
config_path.to_path_buf(),
);
return Ok(Box::new(store));
}
#[cfg(feature = "keyring")]
{
let store = KeyringSecretStore::try_new(&secrets.keyring_service, "server_secrets")?;
return Ok(Box::new(store));
}
#[allow(unreachable_code)]
{
let store = plaintext::PlaintextSecretStore::new(
secrets.plaintext.as_ref(),
config_path.to_path_buf(),
);
Ok(Box::new(store))
}
}
#[cfg(all(
test,
any(
feature = "aws-secrets",
feature = "gcp-secrets",
feature = "azure-secrets",
feature = "vault-secrets",
feature = "k8s-secrets"
)
))]
mod stored_secrets_tests {
use super::{ServerSecrets, StoredSecrets};
#[test]
fn parse_legacy_bare_server_secrets_blob() {
let legacy = r#"{
"signing_key": "sig",
"key_agreement_key": "ka",
"jwt_signing_key": "jwt"
}"#;
let env = StoredSecrets::parse(legacy).expect("legacy parses");
let secrets = env.secrets.expect("secrets present");
assert_eq!(secrets.signing_key, "sig");
assert_eq!(secrets.key_agreement_key, "ka");
assert_eq!(secrets.jwt_signing_key, "jwt");
assert!(env.bootstrap_seed.is_none());
}
#[test]
fn parse_envelope_with_seed_only() {
let envelope = r#"{ "bootstrap_seed": "AAAA" }"#;
let env = StoredSecrets::parse(envelope).expect("envelope parses");
assert!(env.secrets.is_none());
assert_eq!(env.bootstrap_seed.as_deref(), Some("AAAA"));
}
#[test]
fn parse_envelope_with_both_fields() {
let envelope = r#"{
"secrets": {
"signing_key": "sig",
"key_agreement_key": "ka",
"jwt_signing_key": "jwt"
},
"bootstrap_seed": "AAAA"
}"#;
let env = StoredSecrets::parse(envelope).expect("envelope parses");
assert!(env.secrets.is_some());
assert_eq!(env.bootstrap_seed.as_deref(), Some("AAAA"));
}
#[test]
fn roundtrip_encodes_and_decodes_seed() {
let seed = [42u8; 32];
let b64 = StoredSecrets::encode_seed(&seed);
let mut env = StoredSecrets::default();
env.bootstrap_seed = Some(b64);
env.secrets = Some(ServerSecrets {
signing_key: "s".into(),
key_agreement_key: "k".into(),
jwt_signing_key: "j".into(),
vta_credential: None,
retired: Vec::new(),
});
let json = env.to_json().expect("serialises");
let parsed = StoredSecrets::parse(&json).expect("re-parses");
let decoded = StoredSecrets::decode_seed(parsed.bootstrap_seed.as_ref().unwrap()).unwrap();
assert_eq!(decoded, seed);
}
#[test]
fn empty_envelope_serialises_to_empty_object() {
let env = StoredSecrets::default();
assert_eq!(env.to_json().unwrap(), "{}");
}
}