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/// Information given to a [`KeyFetch`] implementation when it is asked to
31/// produce a key.
32///
33/// The struct is `#[non_exhaustive]` — additional fields (a tracing span, a
34/// caller identifier, telemetry hooks) will be added in later phases without
35/// requiring a major version bump.
36#[non_exhaustive]
37#[derive(Debug, Clone)]
38pub struct FetchContext {
39    /// Logical name of the key being requested.
40    ///
41    /// Fetchers that talk to a named store (keychain entries, environment
42    /// variables, file paths) use this to disambiguate which key to load.
43    /// It does **not** carry any policy meaning to the vault itself.
44    pub key_name: String,
45}
46
47impl FetchContext {
48    /// Construct a context for the given logical key name.
49    #[must_use]
50    pub fn new(key_name: impl Into<String>) -> Self {
51        Self {
52            key_name: key_name.into(),
53        }
54    }
55}
56
57/// Container for raw key material returned by a [`KeyFetch`] implementation.
58///
59/// `RawKey` deliberately exposes no method that returns a borrowed `&[u8]` to
60/// outside the crate. The only consumers of the inner bytes are the
61/// fragmentation pipeline and (eventually) the zero-on-drop wrapper introduced
62/// in Phase 0.3. From outside `key-vault` you can construct a `RawKey`, hand it
63/// to the vault, and never see it again.
64///
65/// # Layout
66///
67/// In this phase `RawKey` stores the bytes in a plain [`Vec<u8>`]. Phase 0.3
68/// will swap this for `Zeroizing<Vec<u8>>` from the `zeroize` crate without a
69/// public API change.
70pub struct RawKey {
71    bytes: Vec<u8>,
72}
73
74impl RawKey {
75    /// Wrap a freshly-acquired byte buffer.
76    ///
77    /// Callers are expected to overwrite the original buffer immediately after
78    /// constructing the `RawKey` if they kept a copy (for example a stack
79    /// buffer from `read_exact`). The vault itself never holds a separate
80    /// borrow.
81    #[must_use]
82    pub fn new(bytes: Vec<u8>) -> Self {
83        Self { bytes }
84    }
85
86    /// Number of raw key bytes.
87    ///
88    /// Not redacted — the *length* of a key does not by itself compromise the
89    /// secret.
90    #[must_use]
91    pub fn len(&self) -> usize {
92        self.bytes.len()
93    }
94
95    /// Returns `true` if the key contains zero bytes. A zero-length key is
96    /// almost always a configuration error; the vault rejects such keys at
97    /// registration.
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        self.bytes.is_empty()
101    }
102
103    /// Crate-internal access to the raw bytes for the fragmentation pipeline.
104    ///
105    /// This is `pub(crate)` and not part of the public API. The only legitimate
106    /// consumers live inside this crate.
107    #[allow(dead_code)] // wired up by FragmentStrategy implementations in Phase 0.3.
108    #[must_use]
109    pub(crate) fn as_bytes(&self) -> &[u8] {
110        &self.bytes
111    }
112}
113
114impl fmt::Debug for RawKey {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        // Never leak the raw bytes through Debug. Print only the length.
117        f.debug_struct("RawKey")
118            .field("len", &self.bytes.len())
119            .field("bytes", &"<redacted>")
120            .finish()
121    }
122}
123
124/// Pluggable source of key material.
125///
126/// Implementors describe themselves through [`KeyFetch::describe`]; that name
127/// appears in audit events and in [`Error::Acquisition`](crate::Error::Acquisition)
128/// when the fetcher fails.
129///
130/// # Implementor contract
131///
132/// - **No retries.** A failure to find a key is a configuration error from the
133///   vault's perspective; the fetcher should report it once and return.
134/// - **No caching.** The fetcher is called once per key registration. Caching
135///   inside the fetcher defeats the vault's storage discipline.
136/// - **Sanitized errors.** Returned errors must not include key material or
137///   any secret-equivalent value (passwords, tokens, file contents).
138/// - **`Send + Sync`.** The vault may invoke the fetcher from any thread.
139pub trait KeyFetch: Send + Sync {
140    /// Acquire raw key material from the underlying source.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`Error::Acquisition`](crate::Error::Acquisition) when the source
145    /// is reachable but the key cannot be obtained (missing entry, permission
146    /// denied, decryption failure). The `source` field of the error must match
147    /// the value returned by [`KeyFetch::describe`].
148    fn fetch(&self, ctx: &FetchContext) -> Result<RawKey>;
149
150    /// Short, machine-friendly identifier for this fetcher (e.g. `"keychain"`,
151    /// `"file"`, `"env"`). Used for audit records and error attribution.
152    ///
153    /// The returned value should be stable across calls and free of secret
154    /// information.
155    fn describe(&self) -> Cow<'_, str>;
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use alloc::format;
162
163    #[test]
164    fn raw_key_debug_is_redacted() {
165        let key = RawKey::new(alloc::vec![0xaa, 0xbb, 0xcc, 0xdd]);
166        let rendered = format!("{key:?}");
167        assert!(rendered.contains("<redacted>"));
168        assert!(!rendered.contains("aa"));
169        assert!(!rendered.contains("bb"));
170        assert!(rendered.contains("len"));
171    }
172
173    #[test]
174    fn raw_key_len_and_empty() {
175        let empty = RawKey::new(Vec::new());
176        assert!(empty.is_empty());
177        assert_eq!(empty.len(), 0);
178
179        let one = RawKey::new(alloc::vec![1, 2, 3]);
180        assert!(!one.is_empty());
181        assert_eq!(one.len(), 3);
182    }
183
184    #[test]
185    fn fetch_context_holds_name() {
186        let ctx = FetchContext::new("db-primary");
187        assert_eq!(ctx.key_name, "db-primary");
188    }
189}