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}