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 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}