aven_core/sync/client/keys/
storage.rs1use std::collections::HashSet;
3use std::fs::{self, File, OpenOptions};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7use zeroize::Zeroizing;
8
9use super::{ProtectedLocalKeyStoreErrorKind, StoreResult, error};
10
11pub const KEYRING_ITEM: &str = "keyring";
14
15pub struct ProtectedStorageLock {
17 _guard: Box<dyn Send + Sync>,
18}
19
20impl ProtectedStorageLock {
21 pub fn new(guard: impl Send + Sync + 'static) -> Self {
22 Self {
23 _guard: Box::new(guard),
24 }
25 }
26}
27
28pub trait ProtectedStorage: Send + Sync {
42 fn database_location(&self, database_path: &Path) -> StoreResult<Vec<u8>> {
47 canonical_location(database_path)
48 }
49
50 fn prepare(&self, namespace: &str) -> StoreResult<()>;
52
53 fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock>;
56
57 fn load_secret(
59 &self,
60 namespace: &str,
61 item: &str,
62 expected_len: usize,
63 ) -> StoreResult<Option<Zeroizing<Vec<u8>>>>;
64
65 fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()>;
67
68 fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()>;
70
71 fn read_record(
73 &self,
74 namespace: &str,
75 name: &str,
76 expected_len: usize,
77 ) -> StoreResult<Option<Vec<u8>>>;
78
79 fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()>;
81
82 fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()>;
84
85 fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>>;
89
90 fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>>;
93}
94
95pub fn canonical_location(database_path: &Path) -> StoreResult<Vec<u8>> {
97 let canonical = database_path
98 .canonicalize()
99 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
100 #[cfg(unix)]
101 {
102 use std::os::unix::ffi::OsStrExt;
103 Ok(canonical.as_os_str().as_bytes().to_vec())
104 }
105 #[cfg(not(unix))]
106 {
107 Ok(canonical.to_string_lossy().as_bytes().to_vec())
108 }
109}
110
111pub struct FileProtectedStorage {
115 directory: PathBuf,
116}
117
118impl FileProtectedStorage {
119 pub fn new(directory: PathBuf) -> Self {
120 Self { directory }
121 }
122
123 pub fn directory(&self) -> &Path {
124 &self.directory
125 }
126
127 pub fn path(&self, namespace: &str, name: &str) -> PathBuf {
128 self.directory.join(format!("{namespace}.{name}"))
129 }
130}
131
132impl ProtectedStorage for FileProtectedStorage {
133 fn prepare(&self, _namespace: &str) -> StoreResult<()> {
134 prepare_directory(&self.directory)
135 }
136
137 fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock> {
138 let path = self.path(namespace, "lock");
139 let mut options = OpenOptions::new();
140 options.read(true).write(true).create(true).truncate(false);
141 #[cfg(unix)]
142 {
143 use std::os::unix::fs::OpenOptionsExt;
144 options.mode(0o600);
145 }
146 let file = options
147 .open(path)
148 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
149 file.lock()
150 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
151 Ok(ProtectedStorageLock::new(file))
152 }
153
154 fn load_secret(
155 &self,
156 namespace: &str,
157 item: &str,
158 expected_len: usize,
159 ) -> StoreResult<Option<Zeroizing<Vec<u8>>>> {
160 Ok(read_restricted_file(&self.path(namespace, item), expected_len)?.map(Zeroizing::new))
161 }
162
163 fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()> {
164 write_restricted_new(&self.path(namespace, item), bytes)
165 }
166
167 fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()> {
168 remove_restricted_file(&self.path(namespace, item))
169 }
170
171 fn read_record(
172 &self,
173 namespace: &str,
174 name: &str,
175 expected_len: usize,
176 ) -> StoreResult<Option<Vec<u8>>> {
177 read_restricted_file(&self.path(namespace, name), expected_len)
178 }
179
180 fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()> {
181 write_restricted_new(&self.path(namespace, name), bytes)
182 }
183
184 fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()> {
185 remove_restricted_file(&self.path(namespace, name))
186 }
187
188 fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>> {
191 self.list(namespace)
192 }
193
194 fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>> {
195 self.list(namespace)
196 }
197}
198
199impl FileProtectedStorage {
200 fn list(&self, namespace: &str) -> StoreResult<HashSet<String>> {
201 let prefix = format!("{namespace}.");
202 let entries = match fs::read_dir(&self.directory) {
203 Ok(entries) => entries,
204 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
205 return Ok(HashSet::new());
206 }
207 Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
208 };
209 let mut names = HashSet::new();
210 for entry in entries {
211 let entry = entry.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
212 if let Some(name) = entry
213 .file_name()
214 .to_str()
215 .and_then(|name| name.strip_prefix(&prefix))
216 .filter(|name| *name != "lock")
217 {
218 names.insert(name.to_string());
219 }
220 }
221 Ok(names)
222 }
223}
224
225fn prepare_directory(path: &Path) -> StoreResult<()> {
226 fs::create_dir_all(path).map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
227 #[cfg(unix)]
228 {
229 use std::os::unix::fs::PermissionsExt;
230 fs::set_permissions(path, fs::Permissions::from_mode(0o700))
231 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
232 }
233 Ok(())
234}
235
236fn read_restricted_file(path: &Path, expected_len: usize) -> StoreResult<Option<Vec<u8>>> {
237 let mut file = match File::open(path) {
238 Ok(file) => file,
239 Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
240 Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
241 };
242 let metadata = file
243 .metadata()
244 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
245 if !metadata.is_file() || metadata.len() != expected_len as u64 {
246 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
247 }
248 #[cfg(unix)]
249 {
250 use std::os::unix::fs::PermissionsExt;
251 if metadata.permissions().mode() & 0o077 != 0 {
252 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
253 }
254 }
255 let mut bytes = vec![0_u8; expected_len];
256 file.read_exact(&mut bytes)
257 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))?;
258 Ok(Some(bytes))
259}
260
261fn sync_parent(path: &Path) -> StoreResult<()> {
262 let parent = path
263 .parent()
264 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
265 File::open(parent)
266 .and_then(|directory| directory.sync_all())
267 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))
268}
269
270fn remove_restricted_file(path: &Path) -> StoreResult<()> {
271 match fs::remove_file(path) {
272 Ok(()) => sync_parent(path),
273 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
274 Err(_) => Err(super::error(ProtectedLocalKeyStoreErrorKind::WriteFailed)),
275 }
276}
277
278fn write_failure(
279 phase: &'static str,
280 source: std::io::Error,
281) -> super::ProtectedLocalKeyStoreError {
282 tracing::warn!(
283 phase,
284 errno = source.raw_os_error(),
285 "protected storage write failed"
286 );
287 error(ProtectedLocalKeyStoreErrorKind::WriteFailed)
288}
289
290fn write_restricted_new(path: &Path, bytes: &[u8]) -> StoreResult<()> {
291 let parent = path
292 .parent()
293 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
294 let mut staged = tempfile::NamedTempFile::new_in(parent)
295 .map_err(|source| write_failure("stage_create", source))?;
296 #[cfg(unix)]
297 {
298 use std::os::unix::fs::PermissionsExt;
299 staged
300 .as_file()
301 .set_permissions(fs::Permissions::from_mode(0o600))
302 .map_err(|source| write_failure("stage_permissions", source))?;
303 }
304 staged
305 .as_file_mut()
306 .write_all(bytes)
307 .map_err(|source| write_failure("stage_write", source))?;
308 staged
309 .as_file()
310 .sync_all()
311 .map_err(|source| write_failure("stage_sync", source))?;
312 staged
313 .persist_noclobber(path)
314 .map_err(|source| write_failure("stage_persist", source.error))?;
315 File::open(parent)
316 .map_err(|source| write_failure("parent_open", source))?
317 .sync_all()
318 .map_err(|source| write_failure("parent_sync", source))
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn failed_create_preserves_existing_bytes_and_error_contract() {
327 let directory = tempfile::tempdir().unwrap();
328 let path = directory.path().join("record");
329 write_restricted_new(&path, b"first").unwrap();
330 assert_eq!(
331 write_restricted_new(&path, b"other").unwrap_err().kind(),
332 ProtectedLocalKeyStoreErrorKind::WriteFailed
333 );
334 assert_eq!(read_restricted_file(&path, 5).unwrap().unwrap(), b"first");
335 assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 1);
336 }
337
338 #[test]
339 fn failed_staging_keeps_stable_private_error() {
340 let directory = tempfile::tempdir().unwrap();
341 let path = directory.path().join("missing/record");
342 let failure = write_restricted_new(&path, b"private").unwrap_err();
343 assert_eq!(failure.kind(), ProtectedLocalKeyStoreErrorKind::WriteFailed);
344 assert!(!failure.to_string().contains("missing"));
345 assert!(!failure.to_string().contains("private"));
346 }
347}