use std::time::Duration;
use zeroize::Zeroizing;
use crate::error::CachekitError;
pub const MAX_PREVIOUS_MASTER_KEYS: usize = 3;
pub struct CachekitConfig {
pub api_key: Option<Zeroizing<String>>,
pub api_url: String,
pub master_key: Option<Zeroizing<Vec<u8>>>,
pub previous_master_keys: Vec<Zeroizing<Vec<u8>>>,
pub default_ttl: Duration,
pub namespace: Option<String>,
pub l1_capacity: usize,
pub max_payload_bytes: usize,
}
impl std::fmt::Debug for CachekitConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let api_key_repr = if self.api_key.is_some() {
"[REDACTED]"
} else {
"None"
};
let master_key_repr = if self.master_key.is_some() {
"[REDACTED]"
} else {
"None"
};
f.debug_struct("CachekitConfig")
.field("api_key", &api_key_repr)
.field("api_url", &self.api_url)
.field("master_key", &master_key_repr)
.field(
"previous_master_keys",
&format_args!("[REDACTED; {}]", self.previous_master_keys.len()),
)
.field("default_ttl", &self.default_ttl)
.field("namespace", &self.namespace)
.field("l1_capacity", &self.l1_capacity)
.field("max_payload_bytes", &self.max_payload_bytes)
.finish()
}
}
impl Default for CachekitConfig {
fn default() -> Self {
Self {
api_key: None,
api_url: "https://api.cachekit.io".to_owned(),
master_key: None,
previous_master_keys: Vec::new(),
default_ttl: Duration::from_secs(300),
namespace: None,
l1_capacity: 1000,
max_payload_bytes: 5 * 1024 * 1024, }
}
}
impl CachekitConfig {
pub fn from_env() -> Result<Self, CachekitError> {
let mut config = Self::default();
if let Ok(val) = std::env::var("CACHEKIT_API_KEY") {
config.api_key = Some(Zeroizing::new(val));
}
if let Ok(val) = std::env::var("CACHEKIT_API_URL") {
validate_https(&val)?;
config.api_url = val;
}
config.master_key = master_key_from_env()?;
config.previous_master_keys =
previous_master_keys_from_env(config.master_key.as_deref().map(Vec::as_slice))?;
Ok(config)
}
}
#[derive(Default)]
#[must_use]
pub struct CachekitConfigBuilder {
inner: CachekitConfig,
}
impl CachekitConfigBuilder {
pub fn new() -> Self {
Self {
inner: CachekitConfig::default(),
}
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.inner.api_key = Some(Zeroizing::new(key.into()));
self
}
pub fn api_url(mut self, url: impl Into<String>) -> Result<Self, CachekitError> {
let url = url.into();
validate_https(&url)?;
self.inner.api_url = url;
Ok(self)
}
pub fn master_key(mut self, hex_key: &str) -> Result<Self, CachekitError> {
let bytes = decode_master_key_hex(hex_key, "master_key")?;
validate_previous_master_keys(
Some(bytes.as_slice()),
&self.inner.previous_master_keys,
"previous_master_keys",
)?;
self.inner.master_key = Some(bytes);
Ok(self)
}
pub fn previous_master_keys(mut self, hex_keys: &[&str]) -> Result<Self, CachekitError> {
let mut previous = Vec::with_capacity(hex_keys.len());
for hex_key in hex_keys {
previous.push(decode_master_key_hex(
hex_key,
"previous_master_keys entry",
)?);
}
validate_previous_master_keys(
self.inner.master_key.as_deref().map(Vec::as_slice),
&previous,
"previous_master_keys",
)?;
self.inner.previous_master_keys = previous;
Ok(self)
}
pub fn default_ttl(mut self, ttl: Duration) -> Result<Self, CachekitError> {
if ttl.is_zero() {
return Err(CachekitError::Config(
"default_ttl must be greater than zero".to_owned(),
));
}
self.inner.default_ttl = ttl;
Ok(self)
}
pub fn namespace(mut self, ns: impl Into<String>) -> Self {
self.inner.namespace = Some(ns.into());
self
}
pub fn l1_capacity(mut self, capacity: usize) -> Self {
self.inner.l1_capacity = capacity;
self
}
pub fn build(self) -> CachekitConfig {
self.inner
}
}
pub(crate) fn decode_master_key_hex(
hex_key: &str,
what: &str,
) -> Result<Zeroizing<Vec<u8>>, CachekitError> {
let mut bytes = Zeroizing::new(vec![0u8; hex_key.len() / 2]);
hex::decode_to_slice(hex_key, &mut bytes).map_err(|e| {
let reason = match e {
hex::FromHexError::InvalidHexCharacter { .. } => "contains a non-hex character",
hex::FromHexError::OddLength => "odd number of hex digits",
hex::FromHexError::InvalidStringLength => "invalid length",
};
CachekitError::Config(format!("{what} is not valid hex: {reason}"))
})?;
if bytes.len() < 32 {
return Err(CachekitError::Config(format!(
"{what} must be at least 32 bytes (64 hex chars); got {} bytes",
bytes.len()
)));
}
Ok(bytes)
}
fn master_key_hex_from_env() -> Result<Option<Zeroizing<String>>, CachekitError> {
match std::env::var("CACHEKIT_MASTER_KEY") {
Ok(val) => Ok(Some(Zeroizing::new(val))),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(raw)) => {
wipe(raw);
Err(CachekitError::Config(
"CACHEKIT_MASTER_KEY is not valid UTF-8: set it to the key's 64 hex chars"
.to_owned(),
))
}
}
}
fn wipe(raw: std::ffi::OsString) {
drop(Zeroizing::new(raw.into_encoded_bytes()));
}
fn master_key_from_env() -> Result<Option<Zeroizing<Vec<u8>>>, CachekitError> {
let Some(val) = master_key_hex_from_env()? else {
return Ok(None);
};
if val.is_empty() {
return Err(CachekitError::Config(
"CACHEKIT_MASTER_KEY is set but empty: set it to a 64-hex-char key".to_owned(),
));
}
decode_master_key_hex(&val, "CACHEKIT_MASTER_KEY").map(Some)
}
#[cfg(feature = "encryption")]
pub(crate) type EnvKeys = (Zeroizing<Vec<u8>>, Vec<Zeroizing<Vec<u8>>>);
#[cfg(feature = "encryption")]
pub(crate) fn master_keys_from_env(explicit_key_call: &str) -> Result<EnvKeys, CachekitError> {
let hex_key = master_key_hex_from_env()?
.filter(|key| !key.is_empty())
.ok_or_else(|| {
CachekitError::Config(format!(
"CACHEKIT_MASTER_KEY is unset or empty: set it to a 64-hex-char key or pass \
the key to {explicit_key_call}"
))
})?;
let master_key = decode_master_key_hex(&hex_key, "CACHEKIT_MASTER_KEY")?;
let previous = previous_master_keys_from_env(Some(&master_key))?;
Ok((master_key, previous))
}
pub(crate) fn previous_master_keys_from_env(
master_key: Option<&[u8]>,
) -> Result<Vec<Zeroizing<Vec<u8>>>, CachekitError> {
let val = match std::env::var("CACHEKIT_PREVIOUS_MASTER_KEYS") {
Ok(val) => Zeroizing::new(val),
Err(std::env::VarError::NotPresent) => return Ok(Vec::new()),
Err(std::env::VarError::NotUnicode(raw)) => {
wipe(raw);
return Err(CachekitError::Config(
"CACHEKIT_PREVIOUS_MASTER_KEYS is not valid UTF-8".to_owned(),
));
}
};
if val.trim().is_empty() {
return Ok(Vec::new());
}
let mut previous = Vec::new();
for entry in val.split(',') {
let entry = entry.trim();
if entry.is_empty() {
return Err(CachekitError::Config(
"CACHEKIT_PREVIOUS_MASTER_KEYS contains an empty entry".to_owned(),
));
}
previous.push(decode_master_key_hex(
entry,
"CACHEKIT_PREVIOUS_MASTER_KEYS entry",
)?);
}
let Some(master_key) = master_key else {
return Err(CachekitError::Config(
"CACHEKIT_PREVIOUS_MASTER_KEYS requires CACHEKIT_MASTER_KEY to be set".to_owned(),
));
};
validate_previous_master_keys(Some(master_key), &previous, "CACHEKIT_PREVIOUS_MASTER_KEYS")?;
Ok(previous)
}
fn validate_previous_master_keys(
master_key: Option<&[u8]>,
previous: &[Zeroizing<Vec<u8>>],
source: &str,
) -> Result<(), CachekitError> {
if previous.len() > MAX_PREVIOUS_MASTER_KEYS {
return Err(CachekitError::Config(format!(
"{source} accepts at most {MAX_PREVIOUS_MASTER_KEYS} entries; got {}",
previous.len()
)));
}
if let Some(master) = master_key {
if previous.iter().any(|key| key.as_slice() == master) {
return Err(CachekitError::Config(format!(
"the current master key must not appear in {source} \
(rotation is forward-only; retired keys are never re-promoted)"
)));
}
}
Ok(())
}
fn validate_https(url: &str) -> Result<(), CachekitError> {
if !url.starts_with("https://") {
return Err(CachekitError::Config(format!(
"API URL must use HTTPS; got: {url}"
)));
}
Ok(())
}