#![cfg(feature = "encryption")]
mod common;
use cachekit::config::CachekitConfigBuilder;
use cachekit::{CacheKit, CachekitConfig, CachekitError, EncryptionLayer};
use common::EnvGuard;
use serde_json::Value as Json;
use serial_test::serial;
const VECTORS_JSON: &str = include_str!("vectors/encryption.json");
const VECTORS_SHA256: &str = "1a8a3735408675bf9660ce1372f77723a9ffda8084010fa52eb756a330961589";
const OTHER_KEY_HEX: &str = "6161616161616161616161616161616161616161616161616161616161616161";
const TENANT: &str = "default";
#[test]
fn vendored_fixture_matches_the_pinned_sha256() {
use sha2::{Digest, Sha256};
assert_eq!(
hex::encode(Sha256::digest(VECTORS_JSON.as_bytes())),
VECTORS_SHA256,
"tests/vectors/encryption.json differs from the pinned protocol copy: \
re-vendor it from protocol and update VECTORS_SHA256"
);
}
fn block() -> Json {
let all: Json =
serde_json::from_str(VECTORS_JSON).expect("vendored vector file must be valid JSON");
all["master_key_input"].clone()
}
fn rows(group: &str, field: &str, count: usize) -> Vec<(String, String)> {
let rows: Vec<(String, String)> = block()[group]
.as_array()
.unwrap_or_else(|| panic!("{group} must be an array"))
.iter()
.map(|row| {
let get = |k: &str| {
row[k]
.as_str()
.unwrap_or_else(|| panic!("{group} row lacks {k}"))
.to_owned()
};
(get("name"), get(field))
})
.collect();
assert_eq!(rows.len(), count, "{group} row count");
rows
}
fn accept_rows() -> Vec<(String, String)> {
rows("accept_vectors", "master_key_hex", 2)
}
fn reject_rows() -> Vec<(String, String)> {
rows("reject_vectors", "master_key_hex", 11)
}
fn raw_reject_rows() -> Vec<(String, Vec<u8>)> {
rows("raw_reject_vectors", "raw_key_hex", 5)
.into_iter()
.map(|(name, h)| (name, hex::decode(h).expect("raw_key_hex is hex")))
.collect()
}
#[track_caller]
fn assert_config_err<T>(result: Result<T, CachekitError>, names: &str, row: &str, entry: &str) {
match result {
Err(CachekitError::Config(msg)) if msg.contains(names) => {}
Err(e) => panic!("{row} via {entry}: want a Config error naming {names:?}, got {e:?}"),
Ok(_) => panic!("{row} via {entry}: accepted a key protocol requires refused"),
}
}
#[test]
fn builder_encryption_rejects_every_hex_reject_row() {
for (row, key) in accept_rows() {
assert!(
CacheKit::builder().encryption(&key, TENANT).is_ok(),
"{row} refused"
);
}
for (row, key) in reject_rows() {
assert_config_err(
CacheKit::builder().encryption(&key, TENANT),
"master key",
&row,
"CacheKitBuilder::encryption",
);
}
}
#[test]
fn builder_encryption_with_previous_rejects_every_hex_reject_row() {
for (row, accept) in accept_rows() {
assert!(
CacheKit::builder()
.encryption_with_previous(&accept, &[OTHER_KEY_HEX], TENANT)
.is_ok(),
"{row} refused as the current key"
);
assert!(
CacheKit::builder()
.encryption_with_previous(OTHER_KEY_HEX, &[&accept], TENANT)
.is_ok(),
"{row} refused as a previous key"
);
}
for (row, key) in reject_rows() {
assert_config_err(
CacheKit::builder().encryption_with_previous(&key, &[OTHER_KEY_HEX], TENANT),
"master key",
&row,
"CacheKitBuilder::encryption_with_previous (current)",
);
assert_config_err(
CacheKit::builder().encryption_with_previous(OTHER_KEY_HEX, &[&key], TENANT),
"previous master key 0",
&row,
"CacheKitBuilder::encryption_with_previous (previous)",
);
}
}
#[test]
fn config_builder_rejects_every_hex_reject_row() {
for (row, accept) in accept_rows() {
assert!(
CachekitConfigBuilder::new()
.master_key(&accept)
.and_then(|b| b.previous_master_keys(&[OTHER_KEY_HEX]))
.is_ok(),
"{row} refused as the current key"
);
assert!(
CachekitConfigBuilder::new()
.master_key(OTHER_KEY_HEX)
.and_then(|b| b.previous_master_keys(&[&accept]))
.is_ok(),
"{row} refused as a previous key"
);
}
for (row, key) in reject_rows() {
assert_config_err(
CachekitConfigBuilder::new().master_key(&key),
"master_key",
&row,
"CachekitConfigBuilder::master_key",
);
assert_config_err(
CachekitConfigBuilder::new()
.master_key(OTHER_KEY_HEX)
.and_then(|b| b.previous_master_keys(&[&key])),
"previous_master_keys",
&row,
"CachekitConfigBuilder::previous_master_keys",
);
}
}
fn env_entry_points(
master: &str,
previous: Option<&str>,
) -> Vec<(&'static str, Result<(), CachekitError>)> {
let _env = EnvGuard::set(&[
("CACHEKIT_MASTER_KEY", Some(master)),
("CACHEKIT_PREVIOUS_MASTER_KEYS", previous),
("CACHEKIT_API_KEY", Some("ck_test_key")),
("CACHEKIT_API_URL", None),
]);
vec![
(
"CachekitConfig::from_env",
CachekitConfig::from_env().map(drop),
),
#[cfg(feature = "cachekitio")]
("CacheKit::from_env", CacheKit::from_env().map(drop)),
(
"CacheKitBuilder::encryption_from_env",
CacheKit::builder().encryption_from_env().map(drop),
),
]
}
#[test]
#[serial]
fn env_readers_reject_every_hex_reject_row() {
for (row, accept) in accept_rows() {
for (entry, result) in env_entry_points(&accept, Some(OTHER_KEY_HEX))
.into_iter()
.chain(env_entry_points(OTHER_KEY_HEX, Some(&accept)))
{
assert!(result.is_ok(), "{entry} refused {row}'s key: {result:?}");
}
}
for (row, key) in reject_rows() {
for (entry, result) in env_entry_points(&key, None) {
assert_config_err(result, "CACHEKIT_MASTER_KEY", &row, entry);
}
for (entry, result) in env_entry_points(OTHER_KEY_HEX, Some(&key)) {
assert_config_err(
result,
"CACHEKIT_PREVIOUS_MASTER_KEYS",
&row,
&format!("{entry} (previous keys)"),
);
}
}
}
struct KeyringRow {
name: String,
current: String,
decrypt_only: Vec<String>,
accept: bool,
}
fn keyring_rows() -> Vec<KeyringRow> {
let all: Json =
serde_json::from_str(VECTORS_JSON).expect("vendored vector file must be valid JSON");
let rows: Vec<KeyringRow> = all["keyring"]["configuration"]["vectors"]
.as_array()
.expect("keyring.configuration.vectors must be an array")
.iter()
.map(|row| {
let name = row["name"].as_str().expect("name").to_owned();
let text = |v: &Json| {
v.as_str()
.unwrap_or_else(|| panic!("{name}: hex"))
.to_owned()
};
KeyringRow {
current: text(&row["current_master_key_hex"]),
decrypt_only: row["decrypt_only_master_keys_hex"]
.as_array()
.unwrap_or_else(|| panic!("{name}: decrypt_only_master_keys_hex"))
.iter()
.map(text)
.collect(),
accept: match row["verdict"].as_str() {
Some("accept") => true,
Some("reject") => false,
other => panic!("{name}: verdict {other:?}"),
},
name,
}
})
.collect();
assert_eq!(rows.len(), 4, "keyring.configuration row count");
rows
}
#[test]
#[serial]
fn keyring_configuration_rows_load_as_their_verdict_says() {
for row in keyring_rows() {
let decrypt_only: Vec<&str> = row.decrypt_only.iter().map(String::as_str).collect();
let mut loads = vec![
(
"CacheKitBuilder::encryption_with_previous".to_owned(),
CacheKit::builder()
.encryption_with_previous(&row.current, &decrypt_only, TENANT)
.map(drop),
),
(
"CachekitConfigBuilder::previous_master_keys".to_owned(),
CachekitConfigBuilder::new()
.master_key(&row.current)
.and_then(|b| b.previous_master_keys(&decrypt_only))
.map(drop),
),
];
loads.extend(
env_entry_points(&row.current, Some(&decrypt_only.join(",")))
.into_iter()
.map(|(entry, result)| {
(format!("{entry} (CACHEKIT_PREVIOUS_MASTER_KEYS)"), result)
}),
);
for (entry, result) in loads {
match (row.accept, result) {
(true, Ok(())) | (false, Err(CachekitError::Config(_))) => {}
(true, Err(e)) => panic!("{} via {entry}: refused an accept row: {e:?}", row.name),
(false, other) => panic!(
"{} via {entry}: want a Config error at load, got {other:?}",
row.name
),
}
}
}
}
#[cfg(feature = "redis")]
const UNREACHABLE_REDIS: &str = "redis://127.0.0.1:1";
#[cfg(feature = "redis")]
#[tokio::test]
async fn secure_rejects_every_hex_reject_row() {
for (row, key) in reject_rows() {
assert_config_err(
CacheKit::secure(UNREACHABLE_REDIS, &key).await,
"master_key_hex",
&row,
"CacheKit::secure",
);
}
}
#[cfg(feature = "redis")]
#[tokio::test]
#[serial]
async fn secure_from_env_rejects_every_hex_reject_row() {
for (row, key) in reject_rows() {
let current = {
let _env = EnvGuard::set(&[
("CACHEKIT_MASTER_KEY", Some(&key)),
("CACHEKIT_PREVIOUS_MASTER_KEYS", None),
]);
CacheKit::secure_from_env(UNREACHABLE_REDIS).await
};
assert_config_err(
current,
"CACHEKIT_MASTER_KEY",
&row,
"CacheKit::secure_from_env",
);
let previous = {
let _env = EnvGuard::set(&[
("CACHEKIT_MASTER_KEY", Some(OTHER_KEY_HEX)),
("CACHEKIT_PREVIOUS_MASTER_KEYS", Some(&key)),
]);
CacheKit::secure_from_env(UNREACHABLE_REDIS).await
};
assert_config_err(
previous,
"CACHEKIT_PREVIOUS_MASTER_KEYS",
&row,
"CacheKit::secure_from_env (previous keys)",
);
}
}
const RAW_LEN_ERR: &str = "exactly 32 bytes";
fn raw_entry_points(key: &[u8]) -> [(&'static str, Result<(), CachekitError>); 6] {
let other = hex::decode(OTHER_KEY_HEX).expect("hex");
[
(
"CacheKitBuilder::encryption_from_bytes",
CacheKit::builder()
.encryption_from_bytes(key, TENANT)
.map(drop),
),
(
"CacheKitBuilder::encryption_from_bytes_with_previous (current)",
CacheKit::builder()
.encryption_from_bytes_with_previous(key, &[&other], TENANT)
.map(drop),
),
(
"CacheKitBuilder::encryption_from_bytes_with_previous (previous)",
CacheKit::builder()
.encryption_from_bytes_with_previous(&other, &[key], TENANT)
.map(drop),
),
(
"EncryptionLayer::new",
EncryptionLayer::new(key, TENANT).map(drop),
),
(
"EncryptionLayer::with_previous_keys (current)",
EncryptionLayer::with_previous_keys(key, &[&other], TENANT).map(drop),
),
(
"EncryptionLayer::with_previous_keys (previous)",
EncryptionLayer::with_previous_keys(&other, &[key], TENANT).map(drop),
),
]
}
#[test]
fn raw_entry_points_accept_the_accept_rows_bytes() {
for (row, key) in accept_rows() {
let accept = hex::decode(key).expect("accept row is hex");
for (entry, result) in raw_entry_points(&accept) {
assert!(result.is_ok(), "{row} refused via {entry}: {result:?}");
}
}
}
#[test]
fn raw_entry_points_reject_every_raw_reject_row() {
for (row, key) in raw_reject_rows() {
for (entry, result) in raw_entry_points(&key) {
assert_config_err(result, RAW_LEN_ERR, &row, entry);
}
}
}