Skip to main content

cachekit/
encryption.rs

1//! Zero-knowledge encryption layer using AES-256-GCM with AAD v0x03 format.
2//!
3//! Wraps `cachekit_core::ZeroKnowledgeEncryptor` with tenant key derivation
4//! and cache-key-bound Additional Authenticated Data (AAD). The AAD binding
5//! prevents ciphertext substitution attacks within the same tenant (CVSS 8.5).
6//!
7//! # AAD v0x03 Format
8//!
9//! ```text
10//! [version(0x03)][len(4)][tenant_id][len(4)][cache_key][len(4)][format][len(4)][compressed]
11//! ```
12//!
13//! Each component is length-prefixed with a 4-byte big-endian u32 to prevent
14//! collision attacks from boundary confusion.
15
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use zeroize::Zeroizing;
19
20use cachekit_core::{derive_domain_key, KeyDomain, Keyring, TenantKeyring, ZeroKnowledgeEncryptor};
21
22use crate::error::CachekitError;
23
24/// AAD protocol version byte.
25const AAD_VERSION: u8 = 0x03;
26
27/// Required length of a raw-bytes master key (`spec/intent-presets.md`
28/// § Master Key Input rule 4).
29const RAW_KEY_LEN: usize = 32;
30
31/// Zero-knowledge encryption layer with per-tenant key derivation.
32///
33/// Holds the derived encryption key that writes use, a [`TenantKeyring`] of
34/// the derived per-tenant keys that every read tries in order (current first,
35/// then each previous key), and the `ZeroKnowledgeEncryptor` from
36/// cachekit-core for AES-256-GCM operations. Every key is derived at
37/// construction, so no read runs HKDF; derived keys zeroize on drop, and the
38/// layer keeps no master key.
39///
40/// L1 stores **ciphertext**, not plaintext — the zero-knowledge property
41/// is preserved across all cache layers.
42///
43/// # Key rotation
44///
45/// Writes always encrypt under the current master key. Reads decrypt via the
46/// keyring: sequential attempts, current key first, then each decrypt-only
47/// previous key in order, rebuilding the identical AAD per attempt
48/// (cachekit-rs entries carry no per-entry key identity — sequential
49/// attempts are the spec-assigned branch, `protocol/spec/encryption.md` →
50/// "Key Rotation (Keyring)"). Reads served by a previous key are counted —
51/// see [`Self::previous_key_hits`] for the rotation drain workflow.
52pub struct EncryptionLayer {
53    encryptor: ZeroKnowledgeEncryptor,
54    derived_key: Zeroizing<[u8; 32]>,
55    keyring: TenantKeyring,
56    tenant_id: String,
57    /// `hits[i]` ↔ `previous_keys[i]`; see [`Self::previous_key_hits`].
58    previous_key_hits: Vec<AtomicU64>,
59}
60
61impl EncryptionLayer {
62    /// Create a new encryption layer with HKDF-derived tenant keys.
63    ///
64    /// Equivalent to [`Self::with_previous_keys`] with no previous keys.
65    ///
66    /// # Arguments
67    /// * `master_key_bytes` — Raw master key, exactly 32 bytes
68    /// * `tenant_id` — Tenant identifier for cryptographic isolation
69    ///
70    /// # Errors
71    /// - Master key not exactly 32 bytes ([`CachekitError::Config`])
72    /// - HKDF derivation failure ([`CachekitError::Config`])
73    /// - Encryptor initialization failure ([`CachekitError::Encryption`])
74    pub fn new(master_key_bytes: &[u8], tenant_id: &str) -> Result<Self, CachekitError> {
75        Self::with_previous_keys(master_key_bytes, &[], tenant_id)
76    }
77
78    /// Create an encryption layer with decrypt-only previous master keys.
79    ///
80    /// `previous_keys` are retained during a key-rotation grace window, in
81    /// attempt order. Writes always use `master_key_bytes`; reads attempt it
82    /// first, then each previous key sequentially.
83    ///
84    /// Every key, current and previous, must be **exactly 32 raw bytes** —
85    /// the only length every CacheKit SDK accepts. Pass the decoded key, never
86    /// the ASCII bytes of a hex string (64 bytes, which would derive a key no
87    /// other SDK derives); hex keys go through
88    /// [`CacheKitBuilder::encryption_with_previous`](crate::client::CacheKitBuilder::encryption_with_previous),
89    /// the hex counterpart for rotation.
90    ///
91    /// # Errors
92    /// - Any key not exactly 32 bytes ([`CachekitError::Config`])
93    /// - More than 3 previous keys ([`CachekitError::Config`] — rejected,
94    ///   never truncated)
95    /// - The current master key also present in `previous_keys`
96    ///   ([`CachekitError::Config`] — rotation is forward-only)
97    /// - HKDF derivation failure ([`CachekitError::Config`])
98    /// - Encryptor initialization failure ([`CachekitError::Encryption`])
99    pub fn with_previous_keys(
100        master_key_bytes: &[u8],
101        previous_keys: &[&[u8]],
102        tenant_id: &str,
103    ) -> Result<Self, CachekitError> {
104        // Raw bytes carry no encoding to check, so length is the only guard
105        // against a hex string passed as bytes. Hex paths are floored at
106        // `>= 32` by `decode_master_key_hex`; `from_hex_decoded_keys`
107        // re-checks that floor at the crypto layer for its internal callers.
108        for (i, key) in previous_keys.iter().enumerate() {
109            if key.len() != RAW_KEY_LEN {
110                return Err(CachekitError::Config(format!(
111                    "previous master key {i} must be exactly {RAW_KEY_LEN} bytes; got {}",
112                    key.len()
113                )));
114            }
115        }
116        if master_key_bytes.len() != RAW_KEY_LEN {
117            return Err(CachekitError::Config(format!(
118                "master key must be exactly {RAW_KEY_LEN} bytes; got {} \
119                 (pass hex-encoded keys to `.encryption()`, not their ASCII bytes)",
120                master_key_bytes.len()
121            )));
122        }
123        Self::from_hex_decoded_keys(master_key_bytes, previous_keys, tenant_id)
124    }
125
126    /// Build from hex-decoded keys, which may be longer than 32 bytes
127    /// (`spec/intent-presets.md` § Master Key Input rule 3). Crate-private:
128    /// only the hex paths reach it, after `decode_master_key_hex`.
129    pub(crate) fn from_hex_decoded_keys(
130        master_key_bytes: &[u8],
131        previous_keys: &[&[u8]],
132        tenant_id: &str,
133    ) -> Result<Self, CachekitError> {
134        for (i, key) in previous_keys.iter().enumerate() {
135            if key.len() < 32 {
136                return Err(CachekitError::Config(format!(
137                    "previous master key {i} must be at least 32 bytes; got {}",
138                    key.len()
139                )));
140            }
141        }
142        // Key-length and tenant checks are configuration errors, kept in the
143        // same class as the previous-key and Keyring construction checks
144        // below (config errors must not fold into crypto failures).
145        if master_key_bytes.len() < 32 {
146            return Err(CachekitError::Config(format!(
147                "master key must be at least 32 bytes; got {}",
148                master_key_bytes.len()
149            )));
150        }
151        if tenant_id.is_empty() {
152            return Err(CachekitError::Config(
153                "tenant_id must not be empty".to_owned(),
154            ));
155        }
156        if tenant_id.len() > 255 {
157            return Err(CachekitError::Config(format!(
158                "tenant_id must be at most 255 bytes; got {}",
159                tenant_id.len()
160            )));
161        }
162
163        // The encryption-domain key alone: writes use nothing else, and
164        // `derive_tenant_keys` would also run the authentication and
165        // cache-key HKDFs. This repeats the keyring's entry 0, which
166        // `TenantKeyring` neither exposes nor encrypts with, so construction
167        // derives the current key twice. Wrapped at once, so the error
168        // returns below wipe it too.
169        let derived_key = Zeroizing::new(
170            derive_domain_key(
171                master_key_bytes,
172                KeyDomain::Encryption.as_str(),
173                tenant_id.as_bytes(),
174            )
175            .map_err(|e| CachekitError::Config(format!("key derivation failed: {e}")))?,
176        );
177
178        let encryptor = ZeroKnowledgeEncryptor::new()
179            .map_err(|e| CachekitError::Encryption(format!("encryptor init failed: {e}")))?;
180
181        Ok(Self {
182            encryptor,
183            derived_key,
184            keyring: tenant_keyring(master_key_bytes, previous_keys, tenant_id)?,
185            tenant_id: tenant_id.to_owned(),
186            previous_key_hits: previous_keys.iter().map(|_| AtomicU64::new(0)).collect(),
187        })
188    }
189
190    /// Encrypt plaintext with AAD bound to the cache key.
191    ///
192    /// Output format: `[nonce(12)][ciphertext + auth_tag(16)]`
193    pub fn encrypt(&self, plaintext: &[u8], cache_key: &str) -> Result<Vec<u8>, CachekitError> {
194        // compressed=false is normative, not a stub — see build_aad's invariant note.
195        let aad = self.build_aad(cache_key, false);
196        self.encryptor
197            .encrypt_aes_gcm(plaintext, &*self.derived_key, &aad)
198            .map_err(|e| CachekitError::Encryption(format!("encrypt failed: {e}")))
199    }
200
201    /// Decrypt ciphertext with AAD bound to the cache key.
202    ///
203    /// Returns the original plaintext. Fails if the cache key does not match
204    /// the one used during encryption (ciphertext substitution protection).
205    ///
206    /// Decryption goes through the keyring: the current master key is
207    /// attempted first, then each decrypt-only previous key in order, with
208    /// the identical AAD per attempt. Entries written before a key rotation
209    /// stay readable as long as their key remains in the previous list.
210    ///
211    /// A read served by a previous key is counted in
212    /// [`Self::previous_key_hits`] (the rotation drain signal).
213    pub fn decrypt(&self, ciphertext: &[u8], cache_key: &str) -> Result<Vec<u8>, CachekitError> {
214        // compressed=false is normative, not a stub — see build_aad's invariant note.
215        let aad = self.build_aad(cache_key, false);
216        let (plaintext, index) = self
217            .keyring
218            .decrypt_indexed(&self.encryptor, ciphertext, &aad)
219            .map_err(|e| match e {
220                // Config-class errors stay config-class:
221                // a derivation failure or caller bug must never masquerade as
222                // a decrypt failure that fail-open callers read as a miss.
223                // The bound keyring derives nothing here, so neither arm fires
224                // today; both keep the class if core's decrypt ever reports one.
225                cachekit_core::EncryptionError::KeyDerivation(_)
226                | cachekit_core::EncryptionError::KeyringIndexOutOfRange { .. } => {
227                    CachekitError::Config(format!("keyring decrypt misconfiguration: {e}"))
228                }
229                _ => CachekitError::Encryption(format!("decrypt failed: {e}")),
230            })?;
231        // index 0 = current key (no signal); i >= 1 = previous_keys[i - 1].
232        // The keyring was built from the same slice, so the slot always
233        // exists; `get` keeps a core bug from panicking the read path.
234        debug_assert!(
235            index <= self.previous_key_hits.len(),
236            "keyring index {index} beyond previous-key slots"
237        );
238        if let Some(hits) = index
239            .checked_sub(1)
240            .and_then(|i| self.previous_key_hits.get(i))
241        {
242            hits.fetch_add(1, Ordering::Relaxed);
243        }
244        Ok(plaintext)
245    }
246
247    /// Reads decrypted by each previous key, by position in the previous-key
248    /// list (`hits[i]` ↔ `previous_keys[i]`); empty when there are none.
249    /// Reads served by the current key are not counted.
250    ///
251    /// This is the rotation **drain signal**. It confirms that a grace window
252    /// has drained; it does not shorten one. Follow the protocol's
253    /// [scheduled-rotation runbook](https://github.com/cachekit-io/protocol/blob/main/decisions/key-rotation.md#runbooks-normative-for-docs):
254    /// audit for non-expiring entries first, add the incoming key as
255    /// decrypt-only fleet-wide, then promote it. The window clock starts only
256    /// when that promotion deploy has completed on every instance — until
257    /// then a lagging instance still writes fresh ciphertext under the
258    /// retiring key and reads it silently as *its* current key (index 0).
259    /// From that point, wait at least the longest TTL in use (per-entry TTLs
260    /// passed to `set_with_ttl` count, not just the default), aggregating
261    /// counts across every instance — they are per process and reset on
262    /// restart. Only when the retiring key's count has stayed flat over that
263    /// whole window has every live entry aged out or been re-encrypted on
264    /// write, and the key can be dropped from the previous list without a
265    /// hard cut-over. Positions are comparable across instances only once
266    /// they all run the same keyring configuration. The signal carries no
267    /// key material — positions and counts only.
268    ///
269    /// ```
270    /// use cachekit::EncryptionLayer;
271    ///
272    /// let k1 = [0x11u8; 32]; // retiring master key
273    /// let k2 = [0x22u8; 32]; // current master key after rotation
274    ///
275    /// // Encrypted under k1, before the rotation...
276    /// let ciphertext = EncryptionLayer::new(&k1, "tenant-123")?.encrypt(b"cached value", "user:1")?;
277    ///
278    /// // ...a layer [current=k2, previous=[k1]] serves it from previous[0]:
279    /// // the retiring key is still draining, not yet safe to drop.
280    /// let layer = EncryptionLayer::with_previous_keys(&k2, &[&k1], "tenant-123")?;
281    /// assert_eq!(layer.previous_key_hits(), vec![0]);
282    /// assert_eq!(layer.decrypt(&ciphertext, "user:1")?, b"cached value");
283    /// assert_eq!(layer.previous_key_hits(), vec![1]);
284    /// # Ok::<(), cachekit::CachekitError>(())
285    /// ```
286    pub fn previous_key_hits(&self) -> Vec<u64> {
287        self.previous_key_hits
288            .iter()
289            .map(|h| h.load(Ordering::Relaxed))
290            .collect()
291    }
292
293    /// Whether cachekit-core detected AES hardware support on this host.
294    ///
295    /// Forwards `ZeroKnowledgeEncryptor::hardware_acceleration_enabled()`.
296    /// Informational only — `ring`/`aes-gcm` dispatch independently of it; the
297    /// per-architecture semantics are core's ([README → Zero-Knowledge
298    /// Encryption](https://github.com/cachekit-io/cachekit-rs#zero-knowledge-encryption)).
299    pub fn hardware_acceleration_enabled(&self) -> bool {
300        self.encryptor.hardware_acceleration_enabled()
301    }
302
303    /// Return the tenant ID used for key derivation.
304    pub fn tenant_id(&self) -> &str {
305        &self.tenant_id
306    }
307
308    /// Build AAD v0x03 for a given cache key and compression flag.
309    ///
310    /// Format: `[0x03][len][tenant_id][len][cache_key][len]["msgpack"][len]["True"/"False"]`
311    ///
312    /// All lengths are 4-byte big-endian u32 to prevent boundary-confusion attacks.
313    ///
314    /// # Invariant: `compressed` is always `false` in production
315    ///
316    /// `encrypt` and `decrypt` pass the literal `false`, and that is normative, not a
317    /// gap: this SDK's only cross-SDK encrypted surface is interop mode, and
318    /// `protocol/spec/interop-mode.md` ("Encryption in Interop Mode") mandates the AAD
319    /// components `format = "msgpack"`, `compressed = "False"` — there is no
320    /// compression in interop mode.
321    ///
322    /// Why a reader cannot recover from a wrong flag here: in the general flow a reader
323    /// rebuilds the AAD from the **stored cleartext metadata** written alongside the
324    /// ciphertext (`protocol/spec/encryption.md`, encrypt step 6 / decrypt step 3).
325    /// cachekit-rs stores no such per-entry metadata — it writes plain
326    /// `nonce ‖ ciphertext ‖ tag` with no header — so its readers have nothing to read
327    /// the flag from and must reconstruct the AAD from the interop-pinned constants
328    /// alone. Combined with the no-retry rule (a reader MUST NOT retry decryption with
329    /// any alternative AAD input, `protocol/spec/encryption.md`), emitting
330    /// `compressed = "True"` here would produce ciphertext that no conformant peer can
331    /// authenticate, and none may probe for.
332    ///
333    /// So do not flip this literal to thread a live compression flag through
334    /// `encrypt`/`decrypt`. If this SDK ever does compress, the flag must be *stored*
335    /// per entry and threaded from that stored value — `protocol/spec/encryption.md`
336    /// requires `compressed` to describe the actual plaintext, and claiming `"False"`
337    /// over compressed bytes is the cachekit-py#166 conformance bug (round-trips
338    /// in-process, fails authentication for every correct second reader). A compressed
339    /// cross-SDK profile is a versioned protocol change (interop/v2),
340    /// not an SDK flag.
341    ///
342    /// The `"True"`/`"False"` tokens are frozen byte-level protocol constants —
343    /// normative byte table in `protocol/spec/encryption.md`, section
344    /// "`compressed` tokens"; decided in
345    /// [protocol#12](https://github.com/cachekit-io/protocol/issues/12) (resolved
346    /// 2026-07-19: a spec correction, not a wire change). The `true` branch stays,
347    /// exercised by conformance tests, so those bytes remain pinned.
348    pub fn build_aad(&self, cache_key: &str, compressed: bool) -> Vec<u8> {
349        let format_str = b"msgpack";
350        let compressed_str = if compressed {
351            b"True" as &[u8]
352        } else {
353            b"False"
354        };
355
356        let tenant_bytes = self.tenant_id.as_bytes();
357        let key_bytes = cache_key.as_bytes();
358
359        // Pre-allocate: version(1) + 4 length fields(16) + data
360        let capacity =
361            1 + 16 + tenant_bytes.len() + key_bytes.len() + format_str.len() + compressed_str.len();
362        let mut aad = Vec::with_capacity(capacity);
363
364        aad.push(AAD_VERSION);
365
366        // All components are bounded: tenant_id <= 255 (validated in new()),
367        // cache_key <= 1024 (validated by client), format/compressed are constants.
368        // Safe to use len_u32 helper which saturates on overflow.
369
370        // tenant_id
371        aad.extend_from_slice(&len_u32(tenant_bytes.len()).to_be_bytes());
372        aad.extend_from_slice(tenant_bytes);
373
374        // cache_key
375        aad.extend_from_slice(&len_u32(key_bytes.len()).to_be_bytes());
376        aad.extend_from_slice(key_bytes);
377
378        // format
379        aad.extend_from_slice(&len_u32(format_str.len()).to_be_bytes());
380        aad.extend_from_slice(format_str);
381
382        // compressed flag
383        aad.extend_from_slice(&len_u32(compressed_str.len()).to_be_bytes());
384        aad.extend_from_slice(compressed_str);
385
386        aad
387    }
388}
389
390/// The keyring bound to `tenant_id`: cachekit-core derives every entry's
391/// per-tenant key here, once, and zeroizes the master keys before returning.
392///
393/// Core owns the keyring invariants (cap of 3, forward-only self-collision,
394/// sequential current-first attempts). Its construction and derivation
395/// failures are configuration errors, not crypto failures.
396fn tenant_keyring(
397    master_key_bytes: &[u8],
398    previous_keys: &[&[u8]],
399    tenant_id: &str,
400) -> Result<TenantKeyring, CachekitError> {
401    Keyring::new(master_key_bytes, previous_keys)
402        .and_then(|keyring| keyring.for_tenant(tenant_id))
403        .map_err(|e| CachekitError::Config(format!("keyring: {e}")))
404}
405
406/// Convert a usize length to u32 for AAD encoding, saturating on overflow.
407/// In practice all inputs are validated to fit (tenant_id <= 255, cache_key <= 1024).
408#[allow(clippy::cast_possible_truncation)]
409fn len_u32(len: usize) -> u32 {
410    u32::try_from(len).unwrap_or(u32::MAX)
411}
412
413impl std::fmt::Debug for EncryptionLayer {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        f.debug_struct("EncryptionLayer")
416            .field("tenant_id", &self.tenant_id)
417            .field("derived_key", &"[REDACTED]")
418            .finish()
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    const TEST_MASTER_KEY: &[u8] = b"test_master_key_32_bytes_long!!!";
427    const TEST_TENANT: &str = "test-tenant";
428
429    #[test]
430    fn roundtrip_encrypt_decrypt() {
431        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
432        let plaintext = b"hello, zero-knowledge world";
433
434        let ciphertext = layer.encrypt(plaintext, "my:key").unwrap();
435        let decrypted = layer.decrypt(&ciphertext, "my:key").unwrap();
436
437        assert_eq!(decrypted, plaintext);
438    }
439
440    #[test]
441    fn wrong_cache_key_fails_decryption() {
442        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
443        let ciphertext = layer.encrypt(b"secret", "key:a").unwrap();
444
445        let result = layer.decrypt(&ciphertext, "key:b");
446        assert!(result.is_err(), "decryption with wrong cache key must fail");
447    }
448
449    #[test]
450    fn different_tenants_produce_different_ciphertext() {
451        let layer_a = EncryptionLayer::new(TEST_MASTER_KEY, "tenant-a").unwrap();
452        let layer_b = EncryptionLayer::new(TEST_MASTER_KEY, "tenant-b").unwrap();
453
454        let ct_a = layer_a.encrypt(b"same data", "same:key").unwrap();
455        let ct_b = layer_b.encrypt(b"same data", "same:key").unwrap();
456
457        // Nonces differ, so ciphertext differs, but also keys differ
458        assert_ne!(ct_a, ct_b);
459
460        // Cross-tenant decryption must fail
461        assert!(layer_b.decrypt(&ct_a, "same:key").is_err());
462    }
463
464    #[test]
465    fn master_key_too_short() {
466        let result = EncryptionLayer::new(b"short", "tenant");
467        assert!(result.is_err());
468        let msg = result.unwrap_err().to_string();
469        assert!(msg.contains("exactly 32 bytes"), "got: {msg}");
470    }
471
472    fn assert_config_err(result: &Result<EncryptionLayer, CachekitError>, what: &str) {
473        assert!(
474            matches!(result, Err(CachekitError::Config(_))),
475            "{what}: expected Config error, got {result:?}"
476        );
477    }
478
479    #[test]
480    fn new_requires_exactly_32_bytes() {
481        assert_config_err(&EncryptionLayer::new(&[7; 31], TEST_TENANT), "31 bytes");
482        assert_config_err(&EncryptionLayer::new(&[7; 33], TEST_TENANT), "33 bytes");
483        assert!(EncryptionLayer::new(&[7; 32], TEST_TENANT).is_ok());
484    }
485
486    #[test]
487    fn with_previous_keys_requires_exactly_32_byte_current_key() {
488        for len in [31, 33, 48] {
489            let current = vec![7u8; len];
490            assert_config_err(
491                &EncryptionLayer::with_previous_keys(&current, &[K1], TEST_TENANT),
492                &format!("{len}-byte current key"),
493            );
494        }
495        assert!(EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).is_ok());
496    }
497
498    #[test]
499    fn with_previous_keys_requires_exactly_32_byte_previous_keys() {
500        for len in [31, 33] {
501            let previous = vec![7u8; len];
502            assert_config_err(
503                &EncryptionLayer::with_previous_keys(K2, &[K1, &previous], TEST_TENANT),
504                &format!("{len}-byte previous key"),
505            );
506        }
507    }
508
509    #[test]
510    fn hex_decoded_keys_accept_longer_than_32_bytes() {
511        // Rule 3: the hex path accepts >= 32 decoded bytes.
512        let long = [7u8; 48];
513        let layer =
514            EncryptionLayer::from_hex_decoded_keys(&long, &[&long[..33]], TEST_TENANT).unwrap();
515        let ct = layer.encrypt(b"v", "k").unwrap();
516        assert_eq!(layer.decrypt(&ct, "k").unwrap(), b"v");
517        assert_config_err(
518            &EncryptionLayer::from_hex_decoded_keys(&long[..31], &[], TEST_TENANT),
519            "hex path keeps its 32-byte floor",
520        );
521    }
522
523    #[test]
524    fn aad_v03_format() {
525        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
526        let aad = layer.build_aad("user:42", false);
527
528        // Version byte
529        assert_eq!(aad[0], 0x03);
530
531        // tenant_id length (4 bytes BE) + tenant_id
532        let tenant_len = u32::from_be_bytes(aad[1..5].try_into().unwrap()) as usize;
533        assert_eq!(tenant_len, TEST_TENANT.len());
534        assert_eq!(&aad[5..5 + tenant_len], TEST_TENANT.as_bytes());
535
536        // cache_key length + cache_key
537        let offset = 5 + tenant_len;
538        let key_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
539        assert_eq!(key_len, 7); // "user:42"
540        assert_eq!(&aad[offset + 4..offset + 4 + key_len], b"user:42");
541
542        // format length + format
543        let offset = offset + 4 + key_len;
544        let fmt_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
545        assert_eq!(&aad[offset + 4..offset + 4 + fmt_len], b"msgpack");
546
547        // compressed length + compressed
548        let offset = offset + 4 + fmt_len;
549        let comp_len = u32::from_be_bytes(aad[offset..offset + 4].try_into().unwrap()) as usize;
550        assert_eq!(&aad[offset + 4..offset + 4 + comp_len], b"False");
551    }
552
553    #[test]
554    fn aad_compressed_flag() {
555        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
556        let aad_false = layer.build_aad("k", false);
557        let aad_true = layer.build_aad("k", true);
558
559        assert_ne!(aad_false, aad_true);
560        // "True" is at the end
561        assert!(aad_true.ends_with(b"True"));
562        assert!(aad_false.ends_with(b"False"));
563    }
564
565    // ── Keyring rotation ─────────────────────────────────────────────────────
566
567    const K1: &[u8] = &[0x11; 32];
568    const K2: &[u8] = &[0x22; 32];
569
570    #[test]
571    fn previous_key_decrypts_after_rotation() {
572        // Written under k1 before the rotation...
573        let old_layer = EncryptionLayer::new(K1, TEST_TENANT).unwrap();
574        let ciphertext = old_layer.encrypt(b"pre-rotation value", "user:1").unwrap();
575
576        // ...still readable with current=k2, previous=[k1] (identical AAD)...
577        let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
578        let plaintext = rotated.decrypt(&ciphertext, "user:1").unwrap();
579        assert_eq!(plaintext, b"pre-rotation value");
580
581        // ...and unreadable after a hard cut-over (previous=[]).
582        let cut_over = EncryptionLayer::with_previous_keys(K2, &[], TEST_TENANT).unwrap();
583        assert!(cut_over.decrypt(&ciphertext, "user:1").is_err());
584    }
585
586    #[test]
587    fn rotated_layer_still_writes_under_current_key() {
588        let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
589        let ciphertext = rotated.encrypt(b"fresh write", "user:2").unwrap();
590
591        // A current-key-only layer reads it: writes never use previous keys.
592        let current_only = EncryptionLayer::new(K2, TEST_TENANT).unwrap();
593        assert_eq!(
594            current_only.decrypt(&ciphertext, "user:2").unwrap(),
595            b"fresh write"
596        );
597    }
598
599    #[test]
600    fn exactly_three_previous_keys_is_accepted() {
601        // Boundary success: a `>=` cap check instead of `>` would reject a
602        // legitimate three-key rotation window and still pass every
603        // rejecting-side test.
604        let keys: Vec<[u8; 32]> = (1..=3).map(|i| [i; 32]).collect();
605        let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
606
607        let layer = EncryptionLayer::with_previous_keys(K2, &refs, TEST_TENANT);
608        assert!(
609            layer.is_ok(),
610            "cap is 3: exactly three previous keys must build"
611        );
612    }
613
614    #[test]
615    fn more_than_three_previous_keys_is_config_error() {
616        let keys: Vec<[u8; 32]> = (1..=4).map(|i| [i; 32]).collect();
617        let refs: Vec<&[u8]> = keys.iter().map(|k| k.as_slice()).collect();
618
619        let result = EncryptionLayer::with_previous_keys(K2, &refs, TEST_TENANT);
620        assert!(
621            matches!(result, Err(CachekitError::Config(_))),
622            "cap of 3 must reject, never truncate"
623        );
624    }
625
626    #[test]
627    fn current_key_in_previous_list_is_config_error() {
628        let result = EncryptionLayer::with_previous_keys(K2, &[K1, K2], TEST_TENANT);
629        assert!(
630            matches!(result, Err(CachekitError::Config(_))),
631            "forward-only: current key must not be decrypt-only"
632        );
633    }
634
635    #[test]
636    fn short_previous_key_is_config_error() {
637        let short = [0x01u8; 16]; // core would accept 16; raw keys must be exactly 32
638        let result = EncryptionLayer::with_previous_keys(K2, &[&short], TEST_TENANT);
639        assert!(matches!(result, Err(CachekitError::Config(_))));
640    }
641
642    #[test]
643    fn keyring_derivation_failure_is_config_error() {
644        // The layer's own tenant checks run first, so only the helper reaches
645        // core's derivation with a tenant it rejects. A config error must not
646        // read as a crypto failure.
647        let err = tenant_keyring(K2, &[K1], "").err();
648        assert!(
649            matches!(&err, Some(CachekitError::Config(msg)) if msg.contains("Key derivation failed")),
650            "expected a Config error carrying core's KeyDerivation, got {err:?}"
651        );
652    }
653
654    // ── Derived keys ─────────────────────────────────────────────────────────
655
656    /// Every key the layer derives is the protocol's. Each keyring entry,
657    /// current and previous, and the write key fingerprint to
658    /// `tests/vectors/encryption.json`, and the write key is byte-identical to
659    /// core's `derive_tenant_keys` encryption key.
660    #[test]
661    fn derived_keys_match_the_protocol_vectors() {
662        use cachekit_core::encryption::key_derivation::{derive_tenant_keys, key_fingerprint};
663
664        let doc: serde_json::Value =
665            serde_json::from_str(include_str!("../tests/vectors/encryption.json")).unwrap();
666        let text = |v: &serde_json::Value| v.as_str().unwrap().to_owned();
667        let key = |v: &serde_json::Value| hex::decode(v.as_str().unwrap()).unwrap();
668        let (main, ring) = (key(&doc["master_key_hex"]), &doc["keyring"]);
669        let (k1, k2) = (&ring["entries"][0], &ring["entries"][1]);
670        let fp = |entry: &serde_json::Value| text(&entry["derived_key_fingerprint_hex"]);
671        // (current, previous, tenant, fingerprints in attempt order)
672        let cases = [
673            (
674                main.clone(),
675                vec![],
676                text(&doc["tenant_id"]),
677                vec![fp(&doc)],
678            ),
679            (
680                main,
681                vec![],
682                text(&doc["default_tenant"]["tenant_id"]),
683                vec![fp(&doc["default_tenant"])],
684            ),
685            (
686                key(&k1["master_key_hex"]),
687                vec![key(&k2["master_key_hex"])],
688                text(&ring["tenant_id"]),
689                vec![fp(k1), fp(k2)],
690            ),
691            (
692                key(&k2["master_key_hex"]),
693                vec![key(&k1["master_key_hex"])],
694                text(&ring["tenant_id"]),
695                vec![fp(k2), fp(k1)],
696            ),
697        ];
698        for (current, previous, tenant, expected) in cases {
699            let previous: Vec<&[u8]> = previous.iter().map(Vec::as_slice).collect();
700            let layer = EncryptionLayer::with_previous_keys(&current, &previous, &tenant).unwrap();
701            let entries: Vec<String> = layer
702                .keyring
703                .encryption_fingerprints()
704                .iter()
705                .map(hex::encode)
706                .collect();
707            assert_eq!(entries, expected, "keyring entries, tenant {tenant}");
708            assert_eq!(
709                hex::encode(key_fingerprint(&*layer.derived_key)),
710                expected[0],
711                "write key, tenant {tenant}"
712            );
713            assert_eq!(
714                *layer.derived_key,
715                derive_tenant_keys(&current, &tenant)
716                    .unwrap()
717                    .encryption_key,
718                "write key against core, tenant {tenant}"
719            );
720        }
721    }
722
723    // ── Rotation drain signal ────────────────────────────────────────────────
724
725    #[test]
726    fn current_key_hit_is_not_counted() {
727        // No previous keys: the drain signal has nothing to report.
728        let single = EncryptionLayer::new(K2, TEST_TENANT).unwrap();
729        let ct = single.encrypt(b"v", "user:3").unwrap();
730        single.decrypt(&ct, "user:3").unwrap();
731        assert!(single.previous_key_hits().is_empty());
732
733        // With a previous key, a current-key read (index 0) is a zero reading.
734        let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
735        let ct = rotated.encrypt(b"fresh write", "user:3").unwrap();
736        rotated.decrypt(&ct, "user:3").unwrap();
737        assert_eq!(rotated.previous_key_hits(), vec![0]);
738    }
739
740    #[test]
741    fn previous_key_hit_is_counted_at_its_position() {
742        const K3: &[u8] = &[0x33; 32];
743        let k1_ct = EncryptionLayer::new(K1, TEST_TENANT)
744            .unwrap()
745            .encrypt(b"k1 era", "user:4")
746            .unwrap();
747        let k2_ct = EncryptionLayer::new(K2, TEST_TENANT)
748            .unwrap()
749            .encrypt(b"k2 era", "user:5")
750            .unwrap();
751
752        // current=k3, previous=[k2, k1]: counts index by position in the previous list.
753        let rotated = EncryptionLayer::with_previous_keys(K3, &[K2, K1], TEST_TENANT).unwrap();
754        assert_eq!(rotated.previous_key_hits(), vec![0, 0]);
755
756        rotated.decrypt(&k1_ct, "user:4").unwrap();
757        assert_eq!(rotated.previous_key_hits(), vec![0, 1], "k1 is previous[1]");
758
759        rotated.decrypt(&k2_ct, "user:5").unwrap();
760        rotated.decrypt(&k2_ct, "user:5").unwrap();
761        assert_eq!(rotated.previous_key_hits(), vec![2, 1], "k2 is previous[0]");
762    }
763
764    #[test]
765    fn failed_decrypt_is_not_a_previous_key_hit() {
766        let k1_ct = EncryptionLayer::new(K1, TEST_TENANT)
767            .unwrap()
768            .encrypt(b"v", "key:a")
769            .unwrap();
770        let rotated = EncryptionLayer::with_previous_keys(K2, &[K1], TEST_TENANT).unwrap();
771
772        // Wrong cache key: every attempt fails authentication — no key won.
773        assert!(rotated.decrypt(&k1_ct, "key:b").is_err());
774        assert_eq!(rotated.previous_key_hits(), vec![0]);
775    }
776
777    #[test]
778    fn hardware_acceleration_reports_core_detection() {
779        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
780        // Delegation, not a constant: agrees with a fresh core encryptor...
781        let core = ZeroKnowledgeEncryptor::new().unwrap();
782        assert_eq!(
783            layer.hardware_acceleration_enabled(),
784            core.hardware_acceleration_enabled()
785        );
786        // ...and on x86_64 with the CPU itself. This pins core's runtime
787        // probe — the only executed evidence in any repo that the bool is live
788        // detection, not a constant.
789        #[cfg(target_arch = "x86_64")]
790        assert_eq!(
791            layer.hardware_acceleration_enabled(),
792            std::arch::is_x86_feature_detected!("aes")
793        );
794    }
795
796    #[test]
797    fn debug_redacts_key() {
798        let layer = EncryptionLayer::new(TEST_MASTER_KEY, TEST_TENANT).unwrap();
799        let debug = format!("{layer:?}");
800        assert!(debug.contains("[REDACTED]"));
801        assert!(!debug.contains("test_master_key"));
802    }
803}