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
142/// Pluggable source of key material.
143///
144/// Implementors describe themselves through [`KeyFetch::describe`]; that name
145/// appears in audit events and in [`Error::Acquisition`](crate::Error::Acquisition)
146/// when the fetcher fails.
147///
148/// # Implementor contract
149///
150/// - **No retries.** A failure to find a key is a configuration error from the
151///   vault's perspective; the fetcher should report it once and return.
152/// - **No caching.** The fetcher is called once per key registration. Caching
153///   inside the fetcher defeats the vault's storage discipline.
154/// - **Sanitized errors.** Returned errors must not include key material or
155///   any secret-equivalent value (passwords, tokens, file contents).
156/// - **`Send + Sync`.** The vault may invoke the fetcher from any thread.
157pub trait KeyFetch: Send + Sync {
158    /// Acquire raw key material from the underlying source.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`Error::Acquisition`](crate::Error::Acquisition) when the source
163    /// is reachable but the key cannot be obtained (missing entry, permission
164    /// denied, decryption failure). The `source` field of the error must match
165    /// the value returned by [`KeyFetch::describe`].
166    fn fetch(&self, ctx: &FetchContext) -> Result<RawKey>;
167
168    /// Short, machine-friendly identifier for this fetcher (e.g. `"keychain"`,
169    /// `"file"`, `"env"`). Used for audit records and error attribution.
170    ///
171    /// The returned value should be stable across calls and free of secret
172    /// information.
173    fn describe(&self) -> Cow<'_, str>;
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use alloc::format;
180
181    #[test]
182    fn raw_key_debug_is_redacted() {
183        let key = RawKey::new(alloc::vec![0xaa, 0xbb, 0xcc, 0xdd]);
184        let rendered = format!("{key:?}");
185        assert!(rendered.contains("<redacted>"));
186        assert!(!rendered.contains("aa"));
187        assert!(!rendered.contains("bb"));
188        assert!(rendered.contains("len"));
189    }
190
191    #[test]
192    fn raw_key_len_and_empty() {
193        let empty = RawKey::new(Vec::new());
194        assert!(empty.is_empty());
195        assert_eq!(empty.len(), 0);
196
197        let one = RawKey::new(alloc::vec![1, 2, 3]);
198        assert!(!one.is_empty());
199        assert_eq!(one.len(), 3);
200    }
201
202    #[test]
203    fn fetch_context_holds_name() {
204        let ctx = FetchContext::new("db-primary");
205        assert_eq!(ctx.key_name, "db-primary");
206    }
207}