Skip to main content

key_vault/fetcher/
mod.rs

1//! Layer 1 — Secure Acquisition.
2//!
3//! The [`KeyFetch`] trait abstracts the source of raw key material. It is the
4//! seam through which keys enter the vault: a TPM, an OS keychain, an encrypted
5//! file, an environment variable, or a custom user-provided source.
6//!
7//! Implementations of [`KeyFetch`] are responsible for:
8//!
9//! - returning a redaction-clean [`Error`](crate::Error) on failure (no key bytes
10//!   in error messages);
11//! - performing acquisition synchronously — the vault treats fetch as a slow
12//!   path and does not retry on its own;
13//! - emitting audit events via the configured logging facility when applicable.
14//!
15//! The trait does **not** specify caching: fetchers are called exactly once per
16//! key registration; the vault keeps the post-fragmentation representation in
17//! memory after that. Re-acquiring a key is the caller's decision.
18//!
19//! Concrete implementations land in later phases. This module currently defines
20//! only the trait surface, the [`FetchContext`] passed to it, and the
21//! [`RawKey`] container that wraps the returned bytes.
22
23use alloc::borrow::Cow;
24use alloc::string::String;
25use alloc::vec::Vec;
26use core::fmt;
27
28use crate::Result;
29
30#[cfg(feature = "fetcher-env")]
31mod env;
32#[cfg(feature = "fetcher-file")]
33mod file;
34#[cfg(feature = "fetcher-keychain")]
35mod keychain;
36#[cfg(feature = "fetcher-tpm")]
37mod tpm;
38
39#[cfg(feature = "fetcher-env")]
40pub use self::env::EnvFetch;
41#[cfg(feature = "fetcher-file")]
42pub use self::file::FileFetch;
43#[cfg(feature = "fetcher-keychain")]
44pub use self::keychain::KeychainFetch;
45#[cfg(feature = "fetcher-tpm")]
46pub use self::tpm::TpmFetch;
47
48/// Information given to a [`KeyFetch`] implementation when it is asked to
49/// produce a key.
50///
51/// The struct is `#[non_exhaustive]` — additional fields (a tracing span, a
52/// caller identifier, telemetry hooks) will be added in later phases without
53/// requiring a major version bump.
54#[non_exhaustive]
55#[derive(Debug, Clone)]
56pub struct FetchContext {
57    /// Logical name of the key being requested.
58    ///
59    /// Fetchers that talk to a named store (keychain entries, environment
60    /// variables, file paths) use this to disambiguate which key to load.
61    /// It does **not** carry any policy meaning to the vault itself.
62    pub key_name: String,
63}
64
65impl FetchContext {
66    /// Construct a context for the given logical key name.
67    #[must_use]
68    pub fn new(key_name: impl Into<String>) -> Self {
69        Self {
70            key_name: key_name.into(),
71        }
72    }
73}
74
75/// Container for raw key material returned by a [`KeyFetch`] implementation.
76///
77/// `RawKey` deliberately exposes no method that returns a borrowed `&[u8]` to
78/// outside the crate. The only consumers of the inner bytes are the
79/// fragmentation pipeline and (eventually) the zero-on-drop wrapper introduced
80/// in Phase 0.3. From outside `key-vault` you can construct a `RawKey`, hand it
81/// to the vault, and never see it again.
82///
83/// # Layout
84///
85/// In this phase `RawKey` stores the bytes in a plain [`Vec<u8>`]. Phase 0.3
86/// will swap this for `Zeroizing<Vec<u8>>` from the `zeroize` crate without a
87/// public API change.
88pub struct RawKey {
89    bytes: Vec<u8>,
90}
91
92impl RawKey {
93    /// Wrap a freshly-acquired byte buffer.
94    ///
95    /// Callers are expected to overwrite the original buffer immediately after
96    /// constructing the `RawKey` if they kept a copy (for example a stack
97    /// buffer from `read_exact`). The vault itself never holds a separate
98    /// borrow.
99    #[must_use]
100    pub fn new(bytes: Vec<u8>) -> Self {
101        Self { bytes }
102    }
103
104    /// Number of raw key bytes.
105    ///
106    /// Not redacted — the *length* of a key does not by itself compromise the
107    /// secret.
108    #[must_use]
109    pub fn len(&self) -> usize {
110        self.bytes.len()
111    }
112
113    /// Returns `true` if the key contains zero bytes. A zero-length key is
114    /// almost always a configuration error; the vault rejects such keys at
115    /// registration.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.bytes.is_empty()
119    }
120
121    /// Crate-internal access to the raw bytes for the fragmentation pipeline.
122    ///
123    /// This is `pub(crate)` and not part of the public API. The only legitimate
124    /// consumers live inside this crate.
125    #[allow(dead_code)] // wired up by FragmentStrategy implementations in Phase 0.3.
126    #[must_use]
127    pub(crate) fn as_bytes(&self) -> &[u8] {
128        &self.bytes
129    }
130}
131
132impl fmt::Debug for RawKey {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        // Never leak the raw bytes through Debug. Print only the length.
135        f.debug_struct("RawKey")
136            .field("len", &self.bytes.len())
137            .field("bytes", &"<redacted>")
138            .finish()
139    }
140}
141
142impl Drop for RawKey {
143    fn drop(&mut self) {
144        // Volatile-zero every byte before the underlying Vec frees its
145        // allocation. Without the volatile + fence pair the compiler is
146        // free to elide the writes since the buffer is about to drop.
147        if !self.bytes.is_empty() {
148            // SAFETY: `self.bytes.as_mut_ptr()` is the start of a valid
149            // `self.bytes.len()`-byte allocation we own. Writes are
150            // within the buffer's bounds and only touch initialized
151            // bytes.
152            unsafe {
153                let ptr = self.bytes.as_mut_ptr();
154                for i in 0..self.bytes.len() {
155                    core::ptr::write_volatile(ptr.add(i), 0u8);
156                }
157            }
158            core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
159        }
160    }
161}
162
163/// Pluggable source of key material.
164///
165/// Implementors describe themselves through [`KeyFetch::describe`]; that name
166/// appears in audit events and in [`Error::Acquisition`](crate::Error::Acquisition)
167/// when the fetcher fails.
168///
169/// # Implementor contract
170///
171/// - **No retries.** A failure to find a key is a configuration error from the
172///   vault's perspective; the fetcher should report it once and return.
173/// - **No caching.** The fetcher is called once per key registration. Caching
174///   inside the fetcher defeats the vault's storage discipline.
175/// - **Sanitized errors.** Returned errors must not include key material or
176///   any secret-equivalent value (passwords, tokens, file contents).
177/// - **`Send + Sync`.** The vault may invoke the fetcher from any thread.
178pub trait KeyFetch: Send + Sync {
179    /// Acquire raw key material from the underlying source.
180    ///
181    /// # Errors
182    ///
183    /// Returns [`Error::Acquisition`](crate::Error::Acquisition) when the source
184    /// is reachable but the key cannot be obtained (missing entry, permission
185    /// denied, decryption failure). The `source` field of the error must match
186    /// the value returned by [`KeyFetch::describe`].
187    fn fetch(&self, ctx: &FetchContext) -> Result<RawKey>;
188
189    /// Short, machine-friendly identifier for this fetcher (e.g. `"keychain"`,
190    /// `"file"`, `"env"`). Used for audit records and error attribution.
191    ///
192    /// The returned value should be stable across calls and free of secret
193    /// information.
194    fn describe(&self) -> Cow<'_, str>;
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use alloc::format;
201
202    #[test]
203    fn raw_key_debug_is_redacted() {
204        let key = RawKey::new(alloc::vec![0xaa, 0xbb, 0xcc, 0xdd]);
205        let rendered = format!("{key:?}");
206        assert!(rendered.contains("<redacted>"));
207        assert!(!rendered.contains("aa"));
208        assert!(!rendered.contains("bb"));
209        assert!(rendered.contains("len"));
210    }
211
212    #[test]
213    fn raw_key_len_and_empty() {
214        let empty = RawKey::new(Vec::new());
215        assert!(empty.is_empty());
216        assert_eq!(empty.len(), 0);
217
218        let one = RawKey::new(alloc::vec![1, 2, 3]);
219        assert!(!one.is_empty());
220        assert_eq!(one.len(), 3);
221    }
222
223    #[test]
224    fn fetch_context_holds_name() {
225        let ctx = FetchContext::new("db-primary");
226        assert_eq!(ctx.key_name, "db-primary");
227    }
228}