1use std::collections::BTreeMap;
9use std::io::Write;
10use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, RwLock};
13
14use super::{Driver, Keyring, SecretMeta, not_found, validate_name};
15use crate::error::{Error, Result};
16use crate::org::OrgId;
17use crate::stack::now_secs;
18
19pub const DRIVER: &str = "local";
20
21pub struct LocalDriver {
22 state: PathBuf,
23 keyring: Arc<Keyring>,
24 lock: RwLock<()>,
27}
28
29impl LocalDriver {
30 pub fn new(state_dir: impl Into<PathBuf>, keyring: Arc<Keyring>) -> LocalDriver {
31 LocalDriver {
32 state: state_dir.into(),
33 keyring,
34 lock: RwLock::new(()),
35 }
36 }
37
38 pub fn keyring(&self) -> &Arc<Keyring> {
39 &self.keyring
40 }
41
42 pub fn dir(&self, org: &OrgId) -> PathBuf {
44 org.dir(&self.state).join("secrets")
45 }
46
47 fn paths(&self, org: &OrgId, name: &str) -> Result<(PathBuf, PathBuf)> {
48 validate_name(name)?;
49 let d = self.dir(org);
50 Ok((
51 d.join(format!("{name}.age")),
52 d.join(format!("{name}.json")),
53 ))
54 }
55
56 fn ensure_dir(&self, org: &OrgId) -> Result<PathBuf> {
57 let d = self.dir(org);
58 std::fs::DirBuilder::new()
59 .recursive(true)
60 .mode(0o700)
61 .create(&d)?;
62 std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700))?;
63 Ok(d)
64 }
65
66 fn read_meta(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
67 let (_, meta) = self.paths(org, name)?;
68 match std::fs::read(&meta) {
69 Ok(b) => serde_json::from_slice(&b)
70 .map_err(|e| Error::invalid(format!("{}: {e}", meta.display()))),
71 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(not_found(org, name)),
72 Err(e) => Err(e.into()),
73 }
74 }
75
76 fn write(&self, org: &OrgId, meta: &SecretMeta, value: &[u8]) -> Result<()> {
79 let dir = self.ensure_dir(org)?;
80 let (age, json) = self.paths(org, &meta.name)?;
81 write_atomic(&age, &self.keyring.encrypt(value)?)?;
82 write_atomic(&json, &serde_json::to_vec_pretty(meta)?)?;
83 fsync_dir(&dir);
84 Ok(())
85 }
86
87 fn decrypt(&self, org: &OrgId, name: &str) -> Result<Vec<u8>> {
88 let (age, _) = self.paths(org, name)?;
89 let ct = match std::fs::read(&age) {
90 Ok(b) => b,
91 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
92 return Err(Error::invalid(format!(
93 "secret {name} in org {org}: metadata without a value ({} is missing); set it again",
94 age.display()
95 )));
96 }
97 Err(e) => return Err(e.into()),
98 };
99 self.keyring.decrypt(&ct).map_err(|e| {
100 Error::invalid(format!(
101 "secret {name} in org {org}: cannot decrypt with this daemon's key ({e}); was it encrypted to another key?"
102 ))
103 })
104 }
105
106 fn orgs(&self) -> Result<Vec<OrgId>> {
107 let dir = self.state.join("orgs");
108 let rd = match std::fs::read_dir(&dir) {
109 Ok(rd) => rd,
110 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
111 Err(e) => return Err(e.into()),
112 };
113 let mut out = Vec::new();
114 for e in rd {
115 let e = e?;
116 if let Some(o) = e.file_name().to_str().and_then(|n| OrgId::new(n).ok()) {
117 if e.path().join("secrets").is_dir() {
118 out.push(o);
119 }
120 }
121 }
122 out.sort();
123 Ok(out)
124 }
125
126 fn read_guard(&self) -> std::sync::RwLockReadGuard<'_, ()> {
127 self.lock.read().unwrap_or_else(|p| p.into_inner())
128 }
129
130 fn write_guard(&self) -> std::sync::RwLockWriteGuard<'_, ()> {
131 self.lock.write().unwrap_or_else(|p| p.into_inner())
132 }
133}
134
135impl Driver for LocalDriver {
136 fn name(&self) -> &str {
137 DRIVER
138 }
139
140 fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)> {
141 let _g = self.read_guard();
142 let meta = self.read_meta(org, name)?;
143 Ok((self.decrypt(org, name)?, meta.version))
144 }
145
146 fn version(&self, org: &OrgId, name: &str) -> Result<u64> {
147 let _g = self.read_guard();
148 Ok(self.read_meta(org, name)?.version)
149 }
150
151 fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
152 let _g = self.read_guard();
153 self.read_meta(org, name)
154 }
155
156 fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>> {
157 let _g = self.read_guard();
158 let rd = match std::fs::read_dir(self.dir(org)) {
159 Ok(rd) => rd,
160 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
161 Err(e) => return Err(e.into()),
162 };
163 let mut out = Vec::new();
164 for e in rd {
165 let p = e?.path();
166 if p.extension().is_none_or(|x| x != "json") {
167 continue;
168 }
169 match std::fs::read(&p)
170 .map_err(Error::from)
171 .and_then(|b| serde_json::from_slice::<SecretMeta>(&b).map_err(Error::from))
172 {
173 Ok(m) => out.push(m),
174 Err(e) => eprintln!("isb secrets: skipping {}: {e}", p.display()),
175 }
176 }
177 out.sort_by(|a, b| a.name.cmp(&b.name));
178 Ok(out)
179 }
180
181 fn create(
182 &self,
183 org: &OrgId,
184 name: &str,
185 value: &[u8],
186 labels: &BTreeMap<String, String>,
187 ) -> Result<SecretMeta> {
188 let _g = self.write_guard();
189 match self.read_meta(org, name) {
190 Ok(_) => return Err(super::exists(org, name)),
191 Err(Error::NotFound(_)) => {}
192 Err(e) => return Err(e),
193 }
194 let now = now_secs();
195 let meta = SecretMeta {
196 org: org.clone(),
197 name: name.to_string(),
198 driver: DRIVER.into(),
199 version: 1,
200 created_at: now,
201 updated_at: now,
202 labels: labels.clone(),
203 };
204 self.write(org, &meta, value)?;
205 Ok(meta)
206 }
207
208 fn set(&self, org: &OrgId, name: &str, value: &[u8]) -> Result<u64> {
209 let _g = self.write_guard();
210 let now = now_secs();
211 let meta = match self.read_meta(org, name) {
212 Ok(mut m) => {
213 m.version += 1;
214 m.updated_at = now;
215 m
216 }
217 Err(Error::NotFound(_)) => SecretMeta {
218 org: org.clone(),
219 name: name.to_string(),
220 driver: DRIVER.into(),
221 version: 1,
222 created_at: now,
223 updated_at: now,
224 labels: BTreeMap::new(),
225 },
226 Err(e) => return Err(e),
227 };
228 self.write(org, &meta, value)?;
229 Ok(meta.version)
230 }
231
232 fn delete(&self, org: &OrgId, name: &str) -> Result<()> {
233 let _g = self.write_guard();
234 self.read_meta(org, name)?;
235 let (age, json) = self.paths(org, name)?;
236 std::fs::remove_file(&json)?;
239 match std::fs::remove_file(&age) {
240 Err(e) if e.kind() != std::io::ErrorKind::NotFound => return Err(e.into()),
241 _ => {}
242 }
243 fsync_dir(&self.dir(org));
244 Ok(())
245 }
246
247 fn reencrypt(&self, org: Option<&OrgId>) -> Result<usize> {
248 let _g = self.write_guard();
249 let orgs = match org {
250 Some(o) => vec![o.clone()],
251 None => self.orgs()?,
252 };
253 let mut n = 0;
254 for o in &orgs {
255 let rd = match std::fs::read_dir(self.dir(o)) {
256 Ok(rd) => rd,
257 Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
258 Err(e) => return Err(e.into()),
259 };
260 let mut names = Vec::new();
261 for e in rd {
262 let p = e?.path();
263 if p.extension().is_some_and(|x| x == "json") {
264 let b = std::fs::read(&p)?;
265 if let Ok(m) = serde_json::from_slice::<SecretMeta>(&b) {
266 names.push(m.name);
267 }
268 }
269 }
270 names.sort();
271 for name in names {
272 let value = self.decrypt(o, &name)?;
273 let (age, _) = self.paths(o, &name)?;
274 write_atomic(&age, &self.keyring.encrypt(&value)?)?;
275 n += 1;
276 }
277 fsync_dir(&self.dir(o));
278 }
279 Ok(n)
280 }
281}
282
283pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
285 let mut tmp = path.as_os_str().to_owned();
286 tmp.push(".tmp");
287 let tmp = PathBuf::from(tmp);
288 let r = (|| -> Result<()> {
289 let mut f = std::fs::OpenOptions::new()
290 .write(true)
291 .create(true)
292 .truncate(true)
293 .mode(0o600)
294 .open(&tmp)?;
295 f.set_permissions(std::fs::Permissions::from_mode(0o600))?;
297 f.write_all(bytes)?;
298 f.sync_all()?;
299 std::fs::rename(&tmp, path)?;
300 Ok(())
301 })();
302 if r.is_err() {
303 let _ = std::fs::remove_file(&tmp);
304 }
305 r
306}
307
308pub fn fsync_dir(dir: &Path) {
310 if let Ok(d) = std::fs::File::open(dir) {
311 let _ = d.sync_all();
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::secrets::Recipient;
319
320 fn driver(dir: &Path) -> (LocalDriver, OrgId) {
321 let k = Keyring::new(age::x25519::Identity::generate(), vec![]);
322 (
323 LocalDriver::new(dir, Arc::new(k)),
324 OrgId::new("ocai").unwrap(),
325 )
326 }
327
328 fn mode(p: &Path) -> u32 {
329 std::fs::metadata(p).unwrap().permissions().mode() & 0o777
330 }
331
332 #[test]
333 fn round_trip_and_versions() {
334 let dir = tempfile::tempdir().unwrap();
335 let (d, org) = driver(dir.path());
336 let labels = BTreeMap::from([("team".to_string(), "web".to_string())]);
337 let m = d.create(&org, "db_password", b"one", &labels).unwrap();
338 assert_eq!((m.version, m.driver.as_str()), (1, "local"));
339 assert!(d.create(&org, "db_password", b"again", &labels).is_err());
340 assert_eq!(d.get(&org, "db_password").unwrap(), (b"one".to_vec(), 1));
341 assert_eq!(d.set(&org, "db_password", b"two").unwrap(), 2);
342 assert_eq!(d.set(&org, "db_password", b"three").unwrap(), 3);
343 assert_eq!(d.version(&org, "db_password").unwrap(), 3);
344 let (v, ver) = d.get(&org, "db_password").unwrap();
345 assert_eq!((v.as_slice(), ver), (&b"three"[..], 3));
346 let m = d.inspect(&org, "db_password").unwrap();
347 assert_eq!(m.labels, labels, "set keeps labels");
348 assert!(m.created_at <= m.updated_at);
349 assert_eq!(d.set(&org, "api_key", b"k").unwrap(), 1);
351 let other = OrgId::new("norm").unwrap();
353 assert!(matches!(
354 d.get(&other, "db_password"),
355 Err(Error::NotFound(_))
356 ));
357 assert!(d.list(&other).unwrap().is_empty());
358 d.delete(&org, "db_password").unwrap();
359 assert!(matches!(
360 d.get(&org, "db_password"),
361 Err(Error::NotFound(_))
362 ));
363 assert!(matches!(
364 d.delete(&org, "db_password"),
365 Err(Error::NotFound(_))
366 ));
367 let sec = d.dir(&org);
368 assert!(!sec.join("db_password.age").exists());
369 assert!(!sec.join("db_password.json").exists());
370 }
371
372 #[test]
373 fn list_has_metadata_never_values() {
374 let dir = tempfile::tempdir().unwrap();
375 let (d, org) = driver(dir.path());
376 d.create(&org, "b", b"value-b-SENTINEL", &BTreeMap::new())
377 .unwrap();
378 d.create(&org, "a", b"value-a-SENTINEL", &BTreeMap::new())
379 .unwrap();
380 let l = d.list(&org).unwrap();
381 assert_eq!(
382 l.iter().map(|m| m.name.as_str()).collect::<Vec<_>>(),
383 ["a", "b"]
384 );
385 let json = serde_json::to_string(&l).unwrap();
386 assert!(!json.contains("SENTINEL"));
387 for e in std::fs::read_dir(d.dir(&org)).unwrap() {
389 let b = std::fs::read(e.unwrap().path()).unwrap();
390 assert!(!String::from_utf8_lossy(&b).contains("SENTINEL"));
391 }
392 }
393
394 #[test]
395 fn files_and_modes() {
396 let dir = tempfile::tempdir().unwrap();
397 let (d, org) = driver(dir.path());
398 d.create(&org, "k", b"v", &BTreeMap::new()).unwrap();
399 d.set(&org, "k", b"v2").unwrap();
400 let sec = dir.path().join("orgs/ocai/secrets");
401 assert_eq!(sec, d.dir(&org));
402 assert_eq!(mode(&sec), 0o700);
403 assert_eq!(mode(&sec.join("k.age")), 0o600);
404 assert_eq!(mode(&sec.join("k.json")), 0o600);
405 let mut names: Vec<String> = std::fs::read_dir(&sec)
407 .unwrap()
408 .map(|e| e.unwrap().file_name().into_string().unwrap())
409 .collect();
410 names.sort();
411 assert_eq!(names, ["k.age", "k.json"]);
412 let ct = std::fs::read(sec.join("k.age")).unwrap();
414 assert!(ct.starts_with(b"age-encryption.org/v1\n"));
415 let tmp = sec.join("k.age.tmp");
417 std::fs::write(&tmp, b"junk").unwrap();
418 std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o644)).unwrap();
419 d.set(&org, "k", b"v3").unwrap();
420 assert!(!tmp.exists());
421 assert_eq!(mode(&sec.join("k.age")), 0o600);
422 std::fs::set_permissions(&sec, std::fs::Permissions::from_mode(0o755)).unwrap();
424 d.set(&org, "k", b"v4").unwrap();
425 assert_eq!(mode(&sec), 0o700);
426 }
427
428 #[test]
429 fn reencrypt_to_an_added_recipient() {
430 let dir = tempfile::tempdir().unwrap();
431 let daemon = age::x25519::Identity::generate();
432 let daemon_text = crate::secrets::keys::identity_file_text(&daemon);
433 let k1 = Keyring::new(daemon, vec![]);
434 let d1 = LocalDriver::new(dir.path(), Arc::new(k1));
435 let org = OrgId::default_org();
436 let org2 = OrgId::new("norm").unwrap();
437 d1.create(&org, "a", b"alpha", &BTreeMap::new()).unwrap();
438 d1.create(&org2, "b", b"beta", &BTreeMap::new()).unwrap();
439 let glass = age::x25519::Identity::generate();
440 let read_with = |id: &dyn age::Identity, org: &OrgId, n: &str| {
441 let ct = std::fs::read(d1.dir(org).join(format!("{n}.age"))).unwrap();
442 crate::secrets::inline::decrypt(&ct, &[id])
443 };
444 assert!(read_with(&glass, &org, "a").is_err());
445
446 let daemon = crate::secrets::keys::parse_identity(&daemon_text).unwrap();
448 let k2 = Keyring::new(daemon, vec![Recipient::X25519(glass.to_public())]);
449 let d2 = LocalDriver::new(dir.path(), Arc::new(k2));
450 assert_eq!(d2.reencrypt(Some(&org)).unwrap(), 1);
451 assert_eq!(read_with(&glass, &org, "a").unwrap(), b"alpha");
452 assert!(read_with(&glass, &org2, "b").is_err());
453 assert_eq!(d2.reencrypt(None).unwrap(), 2);
454 assert_eq!(read_with(&glass, &org2, "b").unwrap(), b"beta");
455 assert_eq!(d2.get(&org, "a").unwrap(), (b"alpha".to_vec(), 1));
457 assert_eq!(d2.get(&org2, "b").unwrap(), (b"beta".to_vec(), 1));
458 }
459
460 #[test]
461 fn wrong_key_is_reported() {
462 let dir = tempfile::tempdir().unwrap();
463 let (d, org) = driver(dir.path());
464 d.create(&org, "k", b"v", &BTreeMap::new()).unwrap();
465 let (d2, _) = driver(dir.path());
466 let e = d2.get(&org, "k").unwrap_err().to_string();
467 assert!(e.contains("cannot decrypt"), "{e}");
468 assert_eq!(d2.list(&org).unwrap().len(), 1);
470 }
471}