use std::time::Duration;
use crate::client::{CacheKit, CacheKitBuilder, SharedBackend};
use crate::error::CachekitError;
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
fn wrap(b: impl crate::backend::Backend + 'static) -> SharedBackend {
std::sync::Arc::new(b)
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
fn wrap(b: impl crate::backend::Backend + 'static) -> SharedBackend {
std::rc::Rc::new(b)
}
#[cfg(feature = "redis")]
fn minimal_defaults(backend: SharedBackend) -> CacheKitBuilder {
let builder = CacheKitBuilder::default()
.backend(backend)
.default_ttl(Duration::from_secs(300))
.l1_capacity(1000);
#[cfg(all(feature = "l1", not(feature = "unsync"), not(target_arch = "wasm32")))]
let builder = builder.swr_enabled(false);
builder
}
#[cfg(all(feature = "redis", feature = "encryption"))]
fn secure_defaults(
master_key: &[u8],
previous_keys: &[zeroize::Zeroizing<Vec<u8>>],
) -> Result<CacheKitBuilder, CachekitError> {
let previous: Vec<&[u8]> = previous_keys.iter().map(|k| k.as_slice()).collect();
let builder = CacheKitBuilder::default()
.default_ttl(Duration::from_secs(600))
.l1_capacity(1000)
.encryption_from_hex_decoded(master_key, &previous, "default")?;
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
let builder = builder.reliability(crate::reliability::ReliabilityConfig::default());
Ok(builder)
}
#[cfg(all(feature = "redis", feature = "encryption"))]
fn master_key_hex_from_env() -> Result<zeroize::Zeroizing<String>, CachekitError> {
std::env::var("CACHEKIT_MASTER_KEY")
.ok()
.map(zeroize::Zeroizing::new)
.filter(|k| !k.is_empty())
.ok_or_else(|| {
CachekitError::Config(
"CACHEKIT_MASTER_KEY is unset or empty: set it to a 64-hex-char key or pass \
the key to CacheKit::secure(url, master_key_hex)"
.to_owned(),
)
})
}
#[cfg(all(feature = "redis", feature = "encryption"))]
fn secure_env_defaults() -> Result<CacheKitBuilder, CachekitError> {
let master_key_hex = master_key_hex_from_env()?;
let master_key = crate::config::decode_master_key_hex(&master_key_hex, "CACHEKIT_MASTER_KEY")?;
let previous = crate::config::previous_master_keys_from_env(Some(&master_key))?;
secure_defaults(&master_key, &previous)
}
#[cfg(all(feature = "redis", feature = "encryption"))]
async fn connect_secure(
redis_url: &str,
builder: CacheKitBuilder,
) -> Result<CacheKitBuilder, CachekitError> {
let backend = crate::backend::redis::RedisBackend::builder()
.url(redis_url)
.auto_reconnect()
.build()?;
drop(backend.connect().await?);
Ok(builder.backend(wrap(backend)))
}
impl CacheKit {
#[cfg(feature = "redis")]
pub async fn minimal(redis_url: &str) -> Result<CacheKitBuilder, CachekitError> {
let backend = crate::backend::redis::RedisBackend::builder()
.url(redis_url)
.build()?;
drop(backend.connect().await?);
Ok(minimal_defaults(wrap(backend)))
}
#[cfg(feature = "redis")]
pub async fn production(redis_url: &str) -> Result<CacheKitBuilder, CachekitError> {
let backend = crate::backend::redis::RedisBackend::builder()
.url(redis_url)
.auto_reconnect()
.build()?;
drop(backend.connect().await?);
let builder = CacheKitBuilder::default()
.backend(wrap(backend))
.default_ttl(Duration::from_secs(600))
.l1_capacity(1000);
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
let builder = builder.reliability(crate::reliability::ReliabilityConfig::default());
Ok(builder)
}
#[cfg(all(feature = "redis", feature = "encryption"))]
pub async fn secure(
redis_url: &str,
master_key_hex: &str,
) -> Result<CacheKitBuilder, CachekitError> {
let master_key = crate::config::decode_master_key_hex(master_key_hex, "master_key_hex")?;
connect_secure(redis_url, secure_defaults(&master_key, &[])?).await
}
#[cfg(all(feature = "redis", feature = "encryption"))]
pub async fn secure_from_env(redis_url: &str) -> Result<CacheKitBuilder, CachekitError> {
connect_secure(redis_url, secure_env_defaults()?).await
}
#[cfg(all(feature = "cachekitio", not(target_arch = "wasm32")))]
pub fn io(api_key: &str) -> Result<CacheKitBuilder, CachekitError> {
let backend = crate::backend::cachekitio::CachekitIO::builder()
.api_key(api_key)
.build()?;
let builder = CacheKitBuilder::default()
.backend(wrap(backend))
.default_ttl(Duration::from_secs(3600))
.l1_capacity(1000);
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
let builder = builder.reliability(crate::reliability::ReliabilityConfig::default());
Ok(builder)
}
#[cfg(all(feature = "cachekitio", not(target_arch = "wasm32")))]
pub fn io_from_env() -> Result<CacheKitBuilder, CachekitError> {
let api_key = std::env::var("CACHEKIT_API_KEY")
.ok()
.map(zeroize::Zeroizing::new)
.filter(|k| !k.is_empty())
.ok_or_else(|| {
CachekitError::Config(
"CACHEKIT_API_KEY is unset or empty: set it or pass the key to CacheKit::io(api_key)"
.to_owned(),
)
})?;
Self::io(&api_key)
}
}
#[cfg(all(test, feature = "redis", feature = "l1"))]
#[allow(clippy::expect_used)] mod tests {
#[test]
fn minimal_enables_l1_without_swr() {
let backend = crate::backend::redis::RedisBackend::builder()
.url("redis://localhost:6379")
.build()
.expect("valid URL");
let cache = super::minimal_defaults(super::wrap(backend))
.build()
.expect("minimal defaults must build");
assert!(
cache.l1.is_some(),
"spec/intent-presets.md § L1 Posture: minimal MUST enable L1"
);
#[cfg(all(not(feature = "unsync"), not(target_arch = "wasm32")))]
assert!(
!cache.swr_enabled,
"spec/intent-presets.md § L1 Posture: minimal MUST NOT enable SWR"
);
}
}
#[cfg(all(test, feature = "redis", feature = "encryption"))]
#[allow(clippy::expect_used)] mod secure_tests {
const MASTER_KEY_HEX: &str = "6161616161616161616161616161616161616161616161616161616161616161";
const CACHE_KEY: &str =
"users:get_user:61598716255080080f6456eb065c2e51badfaa4320b0efe97469c29cffee8875";
const CIPHERTEXT_HEX: &str =
"0d0e0f1011121314151617183c29af318238925ee76d081934adce133c0b4a7c5eb5704102b04582dcbf278ffd"; const PLAINTEXT_HEX: &str = "82a36167651ea46e616d65a5616c696365";
fn assert_decrypts_default_tenant_vector(builder: crate::CacheKitBuilder) {
let layer = builder
.encryption
.expect("secure preset must configure encryption");
assert_eq!(layer.tenant_id(), "default");
let ciphertext = hex::decode(CIPHERTEXT_HEX).expect("vector hex");
let plaintext = layer
.decrypt(&ciphertext, CACHE_KEY)
.expect("default-tenant vector must decrypt");
assert_eq!(hex::encode(plaintext), PLAINTEXT_HEX);
}
#[test]
fn hex_path_decrypts_default_tenant_vector() {
assert_decrypts_default_tenant_vector(
super::secure_defaults(&hex::decode(MASTER_KEY_HEX).expect("vector hex"), &[])
.expect("valid key"),
);
}
#[test]
fn hex_path_accepts_48_byte_key() {
let long = [0x61u8; 48];
assert!(super::secure_defaults(&long, &[]).is_ok());
let previous = [zeroize::Zeroizing::new(vec![0x62u8; 48])];
assert!(super::secure_defaults(&long, &previous).is_ok());
}
#[test]
#[serial_test::serial]
fn env_path_accepts_48_byte_keys() {
let (k2, k1) = ("22".repeat(48), "11".repeat(48));
assert!(secure_env_defaults_with(Some(&k2), Some(&k1)).is_ok());
assert!(secure_env_defaults_with(Some(&"22".repeat(32)), None).is_ok());
}
fn secure_env_defaults_with(
master: Option<&str>,
previous: Option<&str>,
) -> Result<crate::CacheKitBuilder, crate::CachekitError> {
const VARS: [&str; 2] = ["CACHEKIT_MASTER_KEY", "CACHEKIT_PREVIOUS_MASTER_KEYS"];
let saved = VARS.map(std::env::var_os);
for (var, val) in VARS.iter().zip([master, previous]) {
match val {
Some(v) => std::env::set_var(var, v),
None => std::env::remove_var(var),
}
}
let resolved = super::secure_env_defaults();
for (var, val) in VARS.iter().zip(saved) {
match val {
Some(v) => std::env::set_var(var, v),
None => std::env::remove_var(var),
}
}
resolved
}
#[test]
#[serial_test::serial]
fn env_path_decrypts_default_tenant_vector() {
assert_decrypts_default_tenant_vector(
secure_env_defaults_with(Some(MASTER_KEY_HEX), None).expect("valid key"),
);
}
#[test]
#[serial_test::serial]
fn env_path_decrypts_under_previous_key_after_rotation() {
let k2 = "22".repeat(32);
let rotated = secure_env_defaults_with(Some(&k2), Some(MASTER_KEY_HEX))
.expect("valid rotation config");
assert_decrypts_default_tenant_vector(rotated);
let layer = secure_env_defaults_with(Some(&k2), None)
.expect("valid key")
.encryption
.expect("secure preset must configure encryption");
let ciphertext = hex::decode(CIPHERTEXT_HEX).expect("vector hex");
assert!(layer.decrypt(&ciphertext, CACHE_KEY).is_err());
}
#[test]
#[serial_test::serial]
fn env_path_blank_previous_keys_is_unset() {
assert_decrypts_default_tenant_vector(
secure_env_defaults_with(Some(MASTER_KEY_HEX), Some(" ")).expect("blank is unset"),
);
}
#[cfg(unix)]
#[test]
#[serial_test::serial]
fn env_path_rejects_non_utf8_previous_keys() {
use std::os::unix::ffi::OsStringExt;
let saved = std::env::var_os("CACHEKIT_PREVIOUS_MASTER_KEYS");
std::env::set_var(
"CACHEKIT_PREVIOUS_MASTER_KEYS",
std::ffi::OsString::from_vec(vec![0x31, 0xff, 0x31]),
);
let saved_master = std::env::var_os("CACHEKIT_MASTER_KEY");
std::env::set_var("CACHEKIT_MASTER_KEY", MASTER_KEY_HEX);
let resolved = super::secure_env_defaults();
for (var, val) in [
("CACHEKIT_PREVIOUS_MASTER_KEYS", saved),
("CACHEKIT_MASTER_KEY", saved_master),
] {
match val {
Some(v) => std::env::set_var(var, v),
None => std::env::remove_var(var),
}
}
assert!(
matches!(
&resolved,
Err(crate::CachekitError::Config(msg)) if msg.contains("CACHEKIT_PREVIOUS_MASTER_KEYS")
),
"non-UTF-8 previous keys must be a Config error naming the variable; got {:?}",
resolved.err()
);
}
#[test]
#[serial_test::serial]
fn env_path_rejects_invalid_previous_keys() {
let k2 = "22".repeat(32);
let four = ["11", "33", "44", "55"].map(|b| b.repeat(32)).join(",");
for (previous, why) in [
("not-hex", "invalid hex"),
(&*format!("{MASTER_KEY_HEX},"), "empty entry"),
(&*"11".repeat(31), "short key"),
(&*four, "more than 3"),
(&*k2, "current key repeated"),
] {
assert!(
matches!(
secure_env_defaults_with(Some(&k2), Some(previous)),
Err(crate::CachekitError::Config(_))
),
"{why} must be a Config error"
);
}
}
}