key_vault/codex/mod.rs
1//! Layer 5 — Codex transformation.
2//!
3//! A [`Codex`] applies a byte-wise transformation to every byte (real key
4//! material **and** decoy) before it is stored in fragments. The transformation
5//! is an involution: applying it twice returns the original byte. Encoding and
6//! decoding therefore call the same operation.
7//!
8//! # When to use
9//!
10//! The codex layer is off by default ([`IdentityCodex`]). It is feature-gated
11//! behind the `codex` Cargo feature and adds approximately 5–10 ns per byte to
12//! the access path. Enabling it raises the work required for an attacker who
13//! has already defeated layers 2–4 (mlock, fragmentation, decoy): the bytes
14//! they recover are not the bytes the application uses.
15//!
16//! # Involution requirement
17//!
18//! All implementations must satisfy `decode(encode(x)) == x` for every byte.
19//! This is verified by tests for the built-in codices and, beginning in Phase
20//! 0.6, by proptest sweeps over the full byte range.
21
22use core::marker::PhantomData;
23
24mod identity;
25
26pub use self::identity::IdentityCodex;
27
28/// Byte-wise transformation applied to all stored bytes.
29///
30/// # Implementor contract
31///
32/// - **Involution.** For every byte `b`, `self.decode(self.encode(b)) == b`.
33/// Equivalently, the transformation is its own inverse.
34/// - **Constant-time.** Implementations should be branch-free; the canonical
35/// shape is a 256-entry lookup table.
36/// - **`Send + Sync`.** Codex instances are shared across threads.
37pub trait Codex: Send + Sync {
38 /// Transform a byte on the way into storage.
39 fn encode(&self, byte: u8) -> u8;
40
41 /// Transform a byte on the way out of storage.
42 ///
43 /// For involution-based codices `decode == encode`. The two methods are
44 /// kept separate so that downstream consumers reading the code do not have
45 /// to remember the invariant.
46 fn decode(&self, byte: u8) -> u8;
47}
48
49/// Wrap a user-provided closure as a [`Codex`].
50///
51/// The closure is presumed to be an involution; nothing in the type system
52/// enforces this and **violating the property will corrupt every stored key**.
53/// Test your closure with the property test in the `codex` integration suite
54/// before using it in production.
55///
56/// # Examples
57///
58/// ```
59/// use key_vault::codex::{Codex, FnCodex};
60///
61/// // XOR with a fixed mask is an involution.
62/// let codex = FnCodex::new(|b: u8| b ^ 0x5a);
63/// assert_eq!(codex.decode(codex.encode(0x42)), 0x42);
64/// ```
65pub struct FnCodex<F> {
66 f: F,
67 // `PhantomData` keeps the type parameter bound even if `F`'s captured
68 // environment is empty — defensive against future tightening.
69 _marker: PhantomData<fn(u8) -> u8>,
70}
71
72impl<F> FnCodex<F>
73where
74 F: Fn(u8) -> u8 + Send + Sync,
75{
76 /// Wrap the given involution.
77 #[must_use]
78 pub fn new(f: F) -> Self {
79 Self {
80 f,
81 _marker: PhantomData,
82 }
83 }
84}
85
86impl<F> Codex for FnCodex<F>
87where
88 F: Fn(u8) -> u8 + Send + Sync,
89{
90 fn encode(&self, byte: u8) -> u8 {
91 (self.f)(byte)
92 }
93
94 fn decode(&self, byte: u8) -> u8 {
95 (self.f)(byte)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn fn_codex_round_trips_xor() {
105 let c = FnCodex::new(|b: u8| b ^ 0x37);
106 for b in 0u8..=255 {
107 assert_eq!(c.decode(c.encode(b)), b);
108 }
109 }
110}