Skip to main content

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 dynamic;
25mod identity;
26mod static_codex;
27
28pub use self::dynamic::DynamicCodex;
29pub use self::identity::IdentityCodex;
30pub use self::static_codex::StaticCodex;
31
32/// Byte-wise transformation applied to all stored bytes.
33///
34/// # Implementor contract
35///
36/// - **Involution.** For every byte `b`, `self.decode(self.encode(b)) == b`.
37///   Equivalently, the transformation is its own inverse.
38/// - **Constant-time.** Implementations should be branch-free; the canonical
39///   shape is a 256-entry lookup table.
40/// - **`Send + Sync`.** Codex instances are shared across threads.
41pub trait Codex: Send + Sync {
42    /// Transform a byte on the way into storage.
43    fn encode(&self, byte: u8) -> u8;
44
45    /// Transform a byte on the way out of storage.
46    ///
47    /// For involution-based codices `decode == encode`. The two methods are
48    /// kept separate so that downstream consumers reading the code do not have
49    /// to remember the invariant.
50    fn decode(&self, byte: u8) -> u8;
51}
52
53/// Wrap a user-provided closure as a [`Codex`].
54///
55/// The closure is presumed to be an involution; nothing in the type system
56/// enforces this and **violating the property will corrupt every stored key**.
57/// Test your closure with the property test in the `codex` integration suite
58/// before using it in production.
59///
60/// # Examples
61///
62/// ```
63/// use key_vault::codex::{Codex, FnCodex};
64///
65/// // XOR with a fixed mask is an involution.
66/// let codex = FnCodex::new(|b: u8| b ^ 0x5a);
67/// assert_eq!(codex.decode(codex.encode(0x42)), 0x42);
68/// ```
69pub struct FnCodex<F> {
70    f: F,
71    // `PhantomData` keeps the type parameter bound even if `F`'s captured
72    // environment is empty — defensive against future tightening.
73    _marker: PhantomData<fn(u8) -> u8>,
74}
75
76impl<F> FnCodex<F>
77where
78    F: Fn(u8) -> u8 + Send + Sync,
79{
80    /// Wrap the given involution.
81    #[must_use]
82    pub fn new(f: F) -> Self {
83        Self {
84            f,
85            _marker: PhantomData,
86        }
87    }
88}
89
90impl<F> Codex for FnCodex<F>
91where
92    F: Fn(u8) -> u8 + Send + Sync,
93{
94    fn encode(&self, byte: u8) -> u8 {
95        (self.f)(byte)
96    }
97
98    fn decode(&self, byte: u8) -> u8 {
99        (self.f)(byte)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn fn_codex_round_trips_xor() {
109        let c = FnCodex::new(|b: u8| b ^ 0x37);
110        for b in 0u8..=255 {
111            assert_eq!(c.decode(c.encode(b)), b);
112        }
113    }
114}