Skip to main content

key_vault/fetcher/
env.rs

1//! [`EnvFetch`] — environment-variable [`KeyFetch`] backend.
2//!
3//! Reads key bytes from a named process environment variable. The variable
4//! **name** is not secret and appears in error messages for diagnostics; the
5//! variable **value** is treated as secret and never appears in error output
6//! or logging produced by this module.
7//!
8//! # Threat profile
9//!
10//! `EnvFetch` is the **lowest-security** built-in fetcher. Anything in the
11//! process environment is readable by other processes with appropriate
12//! privileges (e.g. `/proc/<pid>/environ` on Linux), by debuggers, and by
13//! crash-dump tooling. Use it for development and container deployments where
14//! the orchestration layer already controls the environment securely
15//! (Kubernetes Secrets mounted as env, AWS Secrets Manager → env via Lambda,
16//! systemd `EnvironmentFile=` with restricted permissions, etc.).
17//!
18//! For higher-security deployments prefer
19//! [`KeychainFetch`](super::keychain::KeychainFetch) (when available) or a
20//! TEE-backed fetcher.
21
22use alloc::borrow::Cow;
23use alloc::format;
24use alloc::string::String;
25use std::env;
26
27use super::{FetchContext, KeyFetch, RawKey};
28use crate::Result;
29use crate::error::Error;
30
31/// `KeyFetch` implementation that reads bytes from a process environment
32/// variable.
33///
34/// The variable name is configured at construction. The variable's bytes
35/// are returned verbatim — no decoding, no trimming, no parsing.
36///
37/// # Examples
38///
39/// ```no_run
40/// use key_vault::{EnvFetch, FetchContext, KeyFetch};
41///
42/// # fn main() -> Result<(), key_vault::Error> {
43/// // SAFETY for the example: setting an env var in a single-threaded
44/// // doctest is fine. Real applications should set keys via the
45/// // orchestration layer (Kubernetes Secrets, AWS Lambda env, etc.).
46/// unsafe { std::env::set_var("MY_APP_KEY", "very-secret-value"); }
47///
48/// let fetcher = EnvFetch::new("MY_APP_KEY");
49/// let ctx = FetchContext::new("my-key");
50/// let raw = fetcher.fetch(&ctx)?;
51/// assert_eq!(raw.len(), "very-secret-value".len());
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Debug, Clone)]
56pub struct EnvFetch {
57    var_name: String,
58}
59
60impl EnvFetch {
61    /// Construct a fetcher that reads from the named environment variable.
62    ///
63    /// The name is stored verbatim. It is logged in failure messages for
64    /// diagnosability — keep that in mind if your variable names themselves
65    /// encode sensitive deployment metadata.
66    #[must_use]
67    pub fn new(var_name: impl Into<String>) -> Self {
68        Self {
69            var_name: var_name.into(),
70        }
71    }
72}
73
74impl KeyFetch for EnvFetch {
75    fn fetch(&self, _ctx: &FetchContext) -> Result<RawKey> {
76        match env::var(&self.var_name) {
77            Ok(value) => Ok(RawKey::new(value.into_bytes())),
78            Err(env::VarError::NotPresent) => Err(Error::Acquisition {
79                source: Cow::Borrowed("env"),
80                reason: format!("environment variable {} is not set", self.var_name),
81            }),
82            Err(env::VarError::NotUnicode(_)) => Err(Error::Acquisition {
83                source: Cow::Borrowed("env"),
84                // We deliberately do NOT include the OsString in the message —
85                // it could expose key bytes that happen to be near-UTF-8.
86                reason: format!(
87                    "environment variable {} contained non-UTF-8 bytes",
88                    self.var_name
89                ),
90            }),
91        }
92    }
93
94    fn describe(&self) -> Cow<'_, str> {
95        Cow::Borrowed("env")
96    }
97}
98
99#[cfg(test)]
100#[allow(clippy::unwrap_used, clippy::expect_used)]
101mod tests {
102    use super::*;
103
104    // env::set_var / env::remove_var are `unsafe` on Rust 1.85+ because
105    // mutating the process environment races with concurrent readers in
106    // other threads. Each test below uses a unique variable name, so the
107    // only writer is the test itself; cargo test's default multi-thread
108    // mode still concurrently reads `getenv`, but the variables we touch
109    // are exclusive to this test invocation. Wrapping every call site in
110    // its own `unsafe { ... }` block lets us put one SAFETY note per call
111    // satisfying `clippy::undocumented_unsafe_blocks`.
112
113    /// SAFETY: see module-level test comment — the var name is unique to
114    /// this test and no other thread reads it.
115    fn set_var_for_test(name: &str, value: &str) {
116        // SAFETY: see fn doc.
117        unsafe {
118            env::set_var(name, value);
119        }
120    }
121
122    /// SAFETY: see module-level test comment.
123    fn remove_var_for_test(name: &str) {
124        // SAFETY: see fn doc.
125        unsafe {
126            env::remove_var(name);
127        }
128    }
129
130    #[test]
131    fn fetches_existing_env_var() {
132        set_var_for_test("KEY_VAULT_TEST_ENV_FETCH_OK", "hello");
133        let f = EnvFetch::new("KEY_VAULT_TEST_ENV_FETCH_OK");
134        let raw = f.fetch(&FetchContext::new("k")).unwrap();
135        assert_eq!(raw.len(), 5);
136        remove_var_for_test("KEY_VAULT_TEST_ENV_FETCH_OK");
137    }
138
139    #[test]
140    fn missing_env_var_returns_acquisition_error() {
141        let f = EnvFetch::new("KEY_VAULT_TEST_ENV_FETCH_MISSING_VAR_42x");
142        let err = f.fetch(&FetchContext::new("k")).unwrap_err();
143        match err {
144            Error::Acquisition { source, reason } => {
145                assert_eq!(source, "env");
146                assert!(reason.contains("not set"));
147                assert!(reason.contains("KEY_VAULT_TEST_ENV_FETCH_MISSING_VAR_42x"));
148            }
149            other => panic!("expected Acquisition error, got {other:?}"),
150        }
151    }
152
153    #[test]
154    fn error_message_does_not_contain_value() {
155        set_var_for_test("KEY_VAULT_TEST_ENV_FETCH_SECRET", "do-not-log-me");
156        remove_var_for_test("KEY_VAULT_TEST_ENV_FETCH_SECRET");
157        let f = EnvFetch::new("KEY_VAULT_TEST_ENV_FETCH_SECRET");
158        let err = f.fetch(&FetchContext::new("k")).unwrap_err();
159        let rendered = format!("{err}");
160        assert!(
161            !rendered.contains("do-not-log-me"),
162            "error message must not include env value (got: {rendered})"
163        );
164    }
165
166    #[test]
167    fn describe_returns_env() {
168        assert_eq!(EnvFetch::new("VAR").describe(), "env");
169    }
170}