#![cfg(feature = "encryption")]
mod common;
use std::collections::BTreeMap;
use cachekit::{CacheKit, CachekitError, EncryptionLayer};
use cachekit_core::encryption::key_derivation::derive_tenant_keys;
use cachekit_core::ZeroKnowledgeEncryptor;
use common::{
encrypting_client_holding, every_read, interop_reads, runtime, EnvelopedValue, AUTH_FAILURE,
};
use serde::de::IgnoredAny;
use serde_json::Value as Json;
const VECTORS_JSON: &str = include_str!("vectors/encryption.json");
const INTEROP_JSON: &str = include_str!("vectors/interop-mode.json");
const PREFIX_NAMESPACE: &str = "app";
fn vectors() -> Json {
serde_json::from_str(VECTORS_JSON).expect("vendored vector file must be valid JSON")
}
fn field<'a>(row: &'a Json, k: &str) -> &'a str {
row[k]
.as_str()
.unwrap_or_else(|| panic!("row {} lacks {k}", row["name"]))
}
fn hex_field(row: &Json, k: &str) -> Vec<u8> {
hex::decode(field(row, k)).unwrap_or_else(|e| panic!("row {} {k}: {e}", row["name"]))
}
fn layer() -> EncryptionLayer {
let doc = vectors();
EncryptionLayer::new(&hex_field(&doc, "master_key_hex"), field(&doc, "tenant_id"))
.expect("main vector key")
}
fn binding_rows(group: &[Json]) -> Vec<Json> {
let layer = layer();
group
.iter()
.filter(|row| layer.build_aad(field(row, "cache_key"), false) == hex_field(row, "aad_hex"))
.cloned()
.collect()
}
fn names(rows: &[Json]) -> Vec<&str> {
rows.iter().map(|row| field(row, "name")).collect()
}
fn client_holding(namespace: Option<&str>, stored_key: &str, ciphertext: Vec<u8>) -> CacheKit {
let doc = vectors();
encrypting_client_holding(
field(&doc, "master_key_hex"),
field(&doc, "tenant_id"),
namespace,
stored_key,
ciphertext,
)
}
#[test]
fn keyring_rows_decrypt_under_their_key_as_current_and_as_previous() {
let doc = vectors();
let ring = &doc["keyring"];
let entries = ring["entries"].as_array().expect("keyring.entries");
let master = |id: &str| {
let entry = entries
.iter()
.find(|e| e["id"] == id)
.unwrap_or_else(|| panic!("no keyring entry {id}"));
hex_field(entry, "master_key_hex")
};
let (k1, k2) = (master("k1"), master("k2"));
let rows = ring["vectors"].as_array().expect("keyring.vectors");
assert_eq!(names(rows), ["encrypted_with_k1", "encrypted_with_k2"]);
for (current, previous) in [(&k1, &k2), (&k2, &k1)] {
let layer =
EncryptionLayer::with_previous_keys(current, &[previous], field(ring, "tenant_id"))
.expect("keyring layer");
for row in rows {
let (name, cache_key) = (field(row, "name"), field(row, "cache_key"));
assert_eq!(
layer.build_aad(cache_key, false),
hex_field(row, "aad_hex"),
"{name}: AAD"
);
let plaintext = layer
.decrypt(&hex_field(row, "ciphertext_hex"), cache_key)
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(plaintext, hex_field(row, "plaintext_hex"), "{name}");
}
assert_eq!(
layer.previous_key_hits(),
vec![1],
"one row is sealed under the previous key"
);
}
}
fn assert_authenticates_as_sealed(doc: &Json, row: &Json) {
let name = field(row, "name");
let sealed_as = field(row, "sealed_as");
let sealed = doc["vectors"]
.as_array()
.expect("vectors")
.iter()
.find(|v| v["name"] == sealed_as)
.unwrap_or_else(|| panic!("{name}: sealed_as {sealed_as} is not in vectors"));
let key = derive_tenant_keys(&hex_field(doc, "master_key_hex"), field(doc, "tenant_id"))
.expect("main vector key")
.encryption_key;
let plaintext = ZeroKnowledgeEncryptor::new()
.expect("encryptor")
.decrypt_aes_gcm(
&hex_field(row, "ciphertext_hex"),
&key,
&hex_field(sealed, "aad_hex"),
)
.unwrap_or_else(|e| panic!("{name}: does not authenticate as {sealed_as}: {e:?}"));
assert_eq!(
plaintext,
hex_field(sealed, "plaintext_hex"),
"{name}: decrypts to {sealed_as}'s plaintext"
);
}
#[test]
fn aad_reject_rows_this_reader_builds_fail_authentication() {
let doc = vectors();
let rows = binding_rows(
doc["aad_reject_vectors"]
.as_array()
.expect("aad_reject_vectors"),
);
assert_eq!(
names(&rows),
[
"aad_compressed_false_sealed_true",
"aad_without_original_type_sealed_with",
"aad_key_with_prefix_sealed_without",
],
"aad_reject_vectors rows whose AAD this reader builds"
);
let rt = runtime();
for row in &rows {
assert_authenticates_as_sealed(&doc, row);
let name = field(row, "name");
let cache_key = field(row, "cache_key");
let ciphertext = hex_field(row, "ciphertext_hex");
let namespace = (name == "aad_key_with_prefix_sealed_without").then_some(PREFIX_NAMESPACE);
let key = match namespace {
Some(ns) => cache_key
.strip_prefix(&format!("{ns}:"))
.expect("the prefix row's cache_key carries the namespace"),
None => cache_key,
};
let client = client_holding(namespace, cache_key, ciphertext);
let mut reads = vec![("CacheKit::get", rt.block_on(client.get::<IgnoredAny>(key)))];
if namespace.is_none() {
reads.extend(rt.block_on(interop_reads::<IgnoredAny>(&client, key)));
}
for (path, result) in reads {
match result {
Err(CachekitError::Encryption(msg)) if msg == AUTH_FAILURE => {}
other => panic!("{name}: {path} must fail authentication, got {other:?}"),
}
}
}
}
fn container_rows() -> Vec<Json> {
let doc = vectors();
let rows = binding_rows(
doc["decrypted_container"]["vectors"]
.as_array()
.expect("decrypted_container.vectors"),
);
assert_eq!(
names(&rows),
[
"container_envelope_to_plain_reader",
"container_trailing_byte_to_interop_reader",
"container_incomplete_tail_to_interop_reader",
],
"decrypted_container rows whose AAD this reader builds"
);
for row in &rows {
assert_eq!(
layer()
.decrypt(&hex_field(row, "ciphertext_hex"), field(row, "cache_key"))
.unwrap_or_else(|e| panic!("{}: does not decrypt: {e:?}", row["name"])),
hex_field(row, "plaintext_hex"),
"{}",
row["name"]
);
}
rows
}
fn container_row(name: &str) -> Json {
container_rows()
.into_iter()
.find(|row| row["name"] == name)
.unwrap_or_else(|| panic!("{name}"))
}
#[test]
fn envelope_plaintext_is_not_unwrapped() {
let row = container_row("container_envelope_to_plain_reader");
assert_eq!(row["reader"], "plain_msgpack");
assert_eq!(row["outcome"], "not_unwrapped");
let key = field(&row, "cache_key");
let client = client_holding(None, key, hex_field(&row, "ciphertext_hex"));
let rt = runtime();
let (values, maps, documents) = rt.block_on(async {
(
every_read::<EnvelopedValue>(&client, key).await,
every_read::<BTreeMap<String, IgnoredAny>>(&client, key).await,
every_read::<Vec<IgnoredAny>>(&client, key).await,
)
});
for (path, result) in values {
assert!(
matches!(result, Err(CachekitError::Serialization(_))),
"{path}: the envelope's inner value must not decode, got {result:?}"
);
}
for (path, result) in maps {
assert!(
matches!(result, Err(CachekitError::Serialization(_))),
"{path}: no map must decode from the envelope, got {result:?}"
);
}
for (path, result) in documents {
assert_eq!(
result
.unwrap_or_else(|e| panic!("{path}: one MessagePack document: {e:?}"))
.map(|v| v.len()),
Some(4),
"{path}: the plaintext decodes as the envelope's 4-element array"
);
}
}
#[test]
fn interop_plaintext_with_trailing_bytes_is_refused() {
let interop: Json = serde_json::from_str(INTEROP_JSON).expect("interop-mode.json");
let expected_key = |name: &str| {
interop["key_vectors"]
.as_array()
.expect("key_vectors")
.iter()
.find(|v| v["name"] == name)
.and_then(|v| v["expected_key"].as_str())
.unwrap_or_else(|| panic!("interop-mode.json key {name}"))
.to_owned()
};
let rt = runtime();
for (name, interop_key) in [
("container_trailing_byte_to_interop_reader", "bool_null"),
(
"container_incomplete_tail_to_interop_reader",
"issue_example_mixed",
),
] {
let row = container_row(name);
assert_eq!(row["reader"], "interop");
assert_eq!(row["outcome"], "error");
let key = field(&row, "cache_key");
assert_eq!(key, expected_key(interop_key), "{name}'s interop key");
let client = client_holding(None, key, hex_field(&row, "ciphertext_hex"));
for (path, result) in rt.block_on(interop_reads::<IgnoredAny>(&client, key)) {
assert!(
matches!(result, Err(CachekitError::Serialization(_))),
"{name}: {path} must refuse the bytes after the first document, got {result:?}"
);
}
}
}