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}