key_vault/fetcher/
file.rs1use alloc::borrow::Cow;
24use alloc::format;
25use alloc::string::String;
26use std::path::{Path, PathBuf};
27
28use super::{FetchContext, KeyFetch, RawKey};
29use crate::Result;
30use crate::error::Error;
31
32#[derive(Debug, Clone)]
52pub struct FileFetch {
53 path: PathBuf,
54 strict_perms: bool,
55}
56
57impl FileFetch {
58 #[must_use]
61 pub fn new(path: impl Into<PathBuf>) -> Self {
62 Self {
63 path: path.into(),
64 strict_perms: true,
65 }
66 }
67
68 #[must_use]
75 pub fn allow_loose_perms(mut self) -> Self {
76 self.strict_perms = false;
77 self
78 }
79
80 #[must_use]
82 pub fn path(&self) -> &Path {
83 &self.path
84 }
85}
86
87impl KeyFetch for FileFetch {
88 fn fetch(&self, _ctx: &FetchContext) -> Result<RawKey> {
89 if self.strict_perms {
90 check_perms(&self.path)?;
91 }
92 std::fs::read(&self.path).map_or_else(
93 |e| {
94 Err(Error::Acquisition {
95 source: Cow::Borrowed("file"),
96 reason: io_failure_message(&self.path, &e),
97 })
98 },
99 |bytes| Ok(RawKey::new(bytes)),
100 )
101 }
102
103 fn describe(&self) -> Cow<'_, str> {
104 Cow::Borrowed("file")
105 }
106}
107
108#[cfg(unix)]
109fn check_perms(path: &Path) -> Result<()> {
110 use std::os::unix::fs::PermissionsExt;
111 let meta = std::fs::metadata(path).map_err(|e| Error::Acquisition {
112 source: Cow::Borrowed("file"),
113 reason: io_failure_message(path, &e),
114 })?;
115 let mode = meta.permissions().mode();
116 if (mode & 0o077) != 0 {
117 return Err(Error::Acquisition {
118 source: Cow::Borrowed("file"),
119 reason: format!(
120 "{} is too permissive (mode {:o}); expected 0600 or stricter",
121 path.display(),
122 mode & 0o777
123 ),
124 });
125 }
126 Ok(())
127}
128
129#[cfg(not(unix))]
130#[allow(clippy::unnecessary_wraps)] fn check_perms(_path: &Path) -> Result<()> {
132 Ok(())
136}
137
138fn io_failure_message(path: &Path, e: &std::io::Error) -> String {
140 format!("failed to read {}: {:?}", path.display(), e.kind())
144}
145
146#[cfg(test)]
147#[allow(clippy::unwrap_used, clippy::expect_used)]
148mod tests {
149 use super::*;
150 use std::io::Write;
151
152 struct TempFile {
156 path: PathBuf,
157 }
158
159 impl TempFile {
160 fn new(prefix: &str, contents: &[u8]) -> Self {
161 let mut path = std::env::temp_dir();
162 let suffix = format!("{}_{}", std::process::id(), prefix);
165 path.push(format!("kv_test_{suffix}.bin"));
166 let mut f = std::fs::File::create(&path).unwrap();
167 f.write_all(contents).unwrap();
168 drop(f);
169 Self { path }
170 }
171
172 fn path(&self) -> &Path {
173 &self.path
174 }
175 }
176
177 impl Drop for TempFile {
178 fn drop(&mut self) {
179 let _ = std::fs::remove_file(&self.path);
180 }
181 }
182
183 #[test]
184 fn reads_file_contents() {
185 let f = TempFile::new("read_ok", b"hello, world!");
186 let fetcher = FileFetch::new(f.path()).allow_loose_perms();
187 let raw = fetcher.fetch(&FetchContext::new("k")).unwrap();
188 assert_eq!(raw.len(), 13);
189 }
190
191 #[test]
192 fn missing_file_returns_acquisition_error() {
193 let fetcher =
194 FileFetch::new("/nonexistent/path/key-vault-test-missing.bin").allow_loose_perms();
195 let err = fetcher.fetch(&FetchContext::new("k")).unwrap_err();
196 match err {
197 Error::Acquisition { source, reason } => {
198 assert_eq!(source, "file");
199 assert!(reason.contains("failed to read"));
200 }
201 other => panic!("expected Acquisition, got {other:?}"),
202 }
203 }
204
205 #[cfg(unix)]
206 #[test]
207 fn strict_perms_rejects_world_readable_file() {
208 use std::os::unix::fs::PermissionsExt;
209 let f = TempFile::new("strict_perm", b"key");
210 std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o644)).unwrap();
211 let fetcher = FileFetch::new(f.path());
212 let err = fetcher.fetch(&FetchContext::new("k")).unwrap_err();
213 match err {
214 Error::Acquisition { reason, .. } => {
215 assert!(reason.contains("too permissive"));
216 }
217 other => panic!("expected Acquisition, got {other:?}"),
218 }
219 }
220
221 #[cfg(unix)]
222 #[test]
223 fn strict_perms_accepts_0600() {
224 use std::os::unix::fs::PermissionsExt;
225 let f = TempFile::new("strict_0600", b"key");
226 std::fs::set_permissions(f.path(), std::fs::Permissions::from_mode(0o600)).unwrap();
227 let fetcher = FileFetch::new(f.path());
228 let raw = fetcher.fetch(&FetchContext::new("k")).unwrap();
229 assert_eq!(raw.len(), 3);
230 }
231
232 #[test]
233 fn describe_returns_file() {
234 assert_eq!(FileFetch::new("/dev/null").describe(), "file");
235 }
236}