1pub mod inline;
14pub mod keys;
15pub mod local;
16pub mod onepassword;
17
18use std::collections::BTreeMap;
19use std::path::Path;
20use std::sync::Arc;
21
22use serde::{Deserialize, Serialize};
23
24use crate::error::{Error, Result};
25use crate::org::OrgId;
26
27pub use inline::{decrypt_inline, encrypt_inline};
28pub use keys::{KeyOrigin, KeySources, Keyring, Recipient, SecretsConfig};
29pub use local::LocalDriver;
30
31pub const MAX_VALUE_BYTES: usize = 1024 * 1024;
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct SecretMeta {
37 pub org: OrgId,
38 pub name: String,
39 pub driver: String,
41 pub version: u64,
43 pub created_at: u64,
45 pub updated_at: u64,
46 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
47 pub labels: BTreeMap<String, String>,
48}
49
50pub trait Driver: Send + Sync {
53 fn name(&self) -> &str;
55
56 fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)>;
58
59 fn version(&self, org: &OrgId, name: &str) -> Result<u64>;
61
62 fn versions(&self, org: &OrgId, names: &[&str]) -> Vec<Result<u64>> {
66 names.iter().map(|n| self.version(org, n)).collect()
67 }
68
69 fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta>;
70
71 fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>>;
73
74 fn create(
76 &self,
77 org: &OrgId,
78 name: &str,
79 _value: &[u8],
80 _labels: &BTreeMap<String, String>,
81 ) -> Result<SecretMeta> {
82 Err(read_only(self.name(), org, name))
83 }
84
85 fn set(&self, org: &OrgId, name: &str, _value: &[u8]) -> Result<u64> {
87 Err(read_only(self.name(), org, name))
88 }
89
90 fn delete(&self, org: &OrgId, name: &str) -> Result<()> {
91 Err(read_only(self.name(), org, name))
92 }
93
94 fn refresh(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
97 self.inspect(org, name)
98 }
99
100 fn reencrypt(&self, _org: Option<&OrgId>) -> Result<usize> {
104 Ok(0)
105 }
106}
107
108pub fn validate_name(name: &str) -> Result<()> {
111 let ok = !name.is_empty()
112 && name.len() <= 128
113 && !name.starts_with('.')
114 && name
115 .bytes()
116 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'));
117 if ok {
118 Ok(())
119 } else {
120 Err(Error::invalid(format!(
121 "secret name {name:?}: 1-128 characters of [A-Za-z0-9_.-], not starting with '.'"
122 )))
123 }
124}
125
126#[doc(hidden)]
127pub fn not_found(org: &OrgId, name: &str) -> Error {
128 Error::NotFound(format!("secret {name} in org {org}"))
129}
130
131pub(crate) fn exists(org: &OrgId, name: &str) -> Error {
132 Error::invalid(format!(
133 "secret {name} already exists in org {org}; use `isb secret set` for a new value"
134 ))
135}
136
137fn read_only(driver: &str, org: &OrgId, name: &str) -> Error {
138 Error::invalid(format!(
139 "secret {name} in org {org}: driver {driver} is read-only"
140 ))
141}
142
143fn check_value(value: &[u8]) -> Result<()> {
144 if value.len() > MAX_VALUE_BYTES {
145 return Err(Error::invalid(format!(
146 "secret value is {} bytes; the limit is {MAX_VALUE_BYTES}",
147 value.len()
148 )));
149 }
150 Ok(())
151}
152
153pub struct Secrets {
155 drivers: Vec<Arc<dyn Driver>>,
156 keyring: Arc<Keyring>,
157}
158
159pub struct Opened {
161 pub secrets: Secrets,
162 pub origin: KeyOrigin,
163 pub notes: Vec<String>,
165}
166
167impl Secrets {
168 pub fn new(local: LocalDriver) -> Secrets {
170 let keyring = local.keyring().clone();
171 Secrets {
172 drivers: vec![Arc::new(local)],
173 keyring,
174 }
175 }
176
177 pub fn open(state_dir: &Path, keys: &KeySources, config: &SecretsConfig) -> Result<Opened> {
180 let break_glass = config.parsed_recipients()?;
181 let k = keys::load_identity(keys)?;
182 let keyring = Arc::new(Keyring::new(k.identity, break_glass));
183 let mut notes = vec![format!(
184 "secrets key from {} (public key {})",
185 k.origin,
186 keyring.public_key()
187 )];
188 notes.extend(k.notes);
189 if keyring.break_glass().is_empty() {
190 notes.push(format!(
191 "WARNING: no break-glass recipients: losing the secrets key loses every secret. Add recipients = [\"age1...\" or \"ssh-ed25519 ...\"] to {}, then run `isb secret reencrypt --all`",
192 SecretsConfig::default_path().display()
193 ));
194 }
195 Ok(Opened {
196 secrets: Secrets::new(LocalDriver::new(state_dir, keyring)),
197 origin: k.origin,
198 notes,
199 })
200 }
201
202 pub fn open_existing(
205 state_dir: &Path,
206 keys: &KeySources,
207 config: &SecretsConfig,
208 ) -> Result<Secrets> {
209 let break_glass = config.parsed_recipients()?;
210 let k = keys::find_identity(keys)?;
211 let keyring = Arc::new(Keyring::new(k.identity, break_glass));
212 Ok(Secrets::new(LocalDriver::new(state_dir, keyring)))
213 }
214
215 pub fn with_driver(mut self, d: Arc<dyn Driver>) -> Result<Secrets> {
217 if self.drivers.iter().any(|x| x.name() == d.name()) {
218 return Err(Error::invalid(format!(
219 "secrets driver {} registered twice",
220 d.name()
221 )));
222 }
223 self.drivers.push(d);
224 Ok(self)
225 }
226
227 pub fn driver_names(&self) -> Vec<String> {
228 self.drivers.iter().map(|d| d.name().to_string()).collect()
229 }
230
231 pub fn keyring(&self) -> &Arc<Keyring> {
232 &self.keyring
233 }
234
235 pub fn recipients(&self) -> Vec<String> {
237 self.keyring
238 .recipients()
239 .iter()
240 .map(|r| r.to_string())
241 .collect()
242 }
243
244 fn driver(&self, name: &str) -> Result<&Arc<dyn Driver>> {
245 self.drivers
246 .iter()
247 .find(|d| d.name() == name)
248 .ok_or_else(|| {
249 Error::invalid(format!(
250 "unknown secrets driver {name:?} (have: {})",
251 self.driver_names().join(", ")
252 ))
253 })
254 }
255
256 fn holder(&self, org: &OrgId, name: &str) -> Result<(&Arc<dyn Driver>, SecretMeta)> {
258 let reference = name.contains('/');
261 if !reference {
262 validate_name(name)?;
263 }
264 for d in &self.drivers {
265 if reference && d.name() == local::DRIVER {
266 continue;
267 }
268 match d.inspect(org, name) {
269 Ok(m) => return Ok((d, m)),
270 Err(Error::NotFound(_)) => {}
271 Err(e) => return Err(e),
272 }
273 }
274 Err(not_found(org, name))
275 }
276
277 pub fn create(
280 &self,
281 org: &OrgId,
282 name: &str,
283 driver: Option<&str>,
284 value: &[u8],
285 labels: &BTreeMap<String, String>,
286 ) -> Result<SecretMeta> {
287 validate_name(name)?;
288 check_value(value)?;
289 for k in labels.keys() {
290 if k.is_empty() || k.len() > 128 {
291 return Err(Error::invalid(format!("label key {k:?}: 1-128 characters")));
292 }
293 }
294 let d = self.driver(driver.unwrap_or(local::DRIVER))?;
295 match self.holder(org, name) {
296 Ok(_) => return Err(exists(org, name)),
297 Err(Error::NotFound(_)) => {}
298 Err(e) => return Err(e),
299 }
300 d.create(org, name, value, labels)
301 }
302
303 pub fn set(&self, org: &OrgId, name: &str, value: &[u8]) -> Result<SecretMeta> {
306 check_value(value)?;
307 let d = match self.holder(org, name) {
308 Ok((d, _)) => d,
309 Err(Error::NotFound(_)) => self.driver(local::DRIVER)?,
310 Err(e) => return Err(e),
311 };
312 d.set(org, name, value)?;
313 d.inspect(org, name)
314 }
315
316 pub fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, SecretMeta)> {
317 let (d, _) = self.holder(org, name)?;
318 let (v, version) = d.get(org, name)?;
319 let mut m = d.inspect(org, name)?;
320 m.version = version;
323 Ok((v, m))
324 }
325
326 pub fn version(&self, org: &OrgId, name: &str) -> Result<u64> {
327 let (d, _) = self.holder(org, name)?;
328 d.version(org, name)
329 }
330
331 pub fn put(&self, org: &OrgId, name: &str, value: &[u8]) -> Result<SecretMeta> {
334 match self.get(org, name) {
335 Ok((v, m)) if v == value => return Ok(m),
336 Ok(_) | Err(Error::NotFound(_)) => {}
337 Err(e) => return Err(e),
338 }
339 self.set(org, name, value)
340 }
341
342 pub fn versions(&self, org: &OrgId, names: &[&str]) -> Vec<Result<u64>> {
348 let mut out: Vec<Option<Result<u64>>> = names.iter().map(|_| None).collect();
349 for (i, n) in names.iter().enumerate() {
350 if !n.contains('/') {
351 out[i] = Some(self.version(org, n));
352 }
353 }
354 for d in self.drivers.iter().filter(|d| d.name() != local::DRIVER) {
355 let todo: Vec<usize> = (0..names.len())
356 .filter(|i| !matches!(out[*i], Some(Ok(_))))
357 .collect();
358 if todo.is_empty() {
359 break;
360 }
361 let asked: Vec<&str> = todo.iter().map(|i| names[*i]).collect();
362 for (i, r) in todo.into_iter().zip(d.versions(org, &asked)) {
363 if r.is_ok() || !matches!(out[i], Some(Err(_))) {
365 out[i] = Some(r);
366 }
367 }
368 }
369 out.into_iter()
370 .zip(names)
371 .map(|(r, n)| r.unwrap_or_else(|| Err(not_found(org, n))))
372 .collect()
373 }
374
375 pub fn get_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)> {
378 self.driver(driver)?.get(org, name)
379 }
380
381 pub fn version_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<u64> {
383 self.driver(driver)?.version(org, name)
384 }
385
386 pub fn versions_in(&self, driver: &str, org: &OrgId, names: &[&str]) -> Vec<Result<u64>> {
389 match self.driver(driver) {
390 Ok(d) => d.versions(org, names),
391 Err(e) => {
392 let msg = e.to_string();
393 names
394 .iter()
395 .map(|_| Err(Error::invalid(msg.clone())))
396 .collect()
397 }
398 }
399 }
400
401 pub fn refresh_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<u64> {
403 Ok(self.driver(driver)?.refresh(org, name)?.version)
404 }
405
406 pub fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
407 self.holder(org, name).map(|(_, m)| m)
408 }
409
410 pub fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>> {
412 let mut out = Vec::new();
413 for d in &self.drivers {
414 out.extend(d.list(org)?);
415 }
416 out.sort_by(|a, b| a.name.cmp(&b.name));
417 Ok(out)
418 }
419
420 pub fn delete(&self, org: &OrgId, name: &str) -> Result<()> {
421 let (d, _) = self.holder(org, name)?;
422 d.delete(org, name)
423 }
424
425 pub fn refresh(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
426 let (d, _) = self.holder(org, name)?;
427 d.refresh(org, name)
428 }
429
430 pub fn reencrypt(&self, org: Option<&OrgId>) -> Result<usize> {
432 let mut n = 0;
433 for d in &self.drivers {
434 n += d.reencrypt(org)?;
435 }
436 Ok(n)
437 }
438
439 pub fn decrypt_inline(&self, text: &str) -> Result<Vec<u8>> {
441 inline::decrypt_inline(text, &[self.keyring.identity()])
442 }
443
444 pub fn encrypt_inline(&self, value: &[u8]) -> Result<String> {
446 inline::encrypt_inline(value, self.keyring.recipients())
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 struct Fixed;
457 impl Driver for Fixed {
458 fn name(&self) -> &str {
459 "fixed"
460 }
461 fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)> {
462 self.inspect(org, name)
463 .map(|m| (b"external".to_vec(), m.version))
464 }
465 fn version(&self, org: &OrgId, name: &str) -> Result<u64> {
466 self.inspect(org, name).map(|m| m.version)
467 }
468 fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
469 if name != "vault_token" {
470 return Err(not_found(org, name));
471 }
472 Ok(SecretMeta {
473 org: org.clone(),
474 name: name.into(),
475 driver: "fixed".into(),
476 version: 7,
477 created_at: 0,
478 updated_at: 0,
479 labels: BTreeMap::new(),
480 })
481 }
482 fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>> {
483 Ok(vec![self.inspect(org, "vault_token")?])
484 }
485 }
486
487 fn secrets(dir: &Path) -> Secrets {
488 let k = Keyring::new(age::x25519::Identity::generate(), vec![]);
489 Secrets::new(LocalDriver::new(dir, Arc::new(k)))
490 .with_driver(Arc::new(Fixed))
491 .unwrap()
492 }
493
494 #[test]
495 fn names() {
496 for ok in ["a", "DB_PASSWORD", "tls.key", "x-1", &"n".repeat(128)] {
497 assert!(validate_name(ok).is_ok(), "{ok}");
498 }
499 for bad in [
500 "",
501 ".",
502 "..",
503 ".hidden",
504 "a/b",
505 "../x",
506 "a b",
507 "ü",
508 &"n".repeat(129),
509 ] {
510 assert!(validate_name(bad).is_err(), "{bad:?}");
511 }
512 }
513
514 #[test]
515 fn facade_dispatches_per_secret() {
516 let dir = tempfile::tempdir().unwrap();
517 let s = secrets(dir.path());
518 let org = OrgId::default_org();
519 assert_eq!(s.driver_names(), ["local", "fixed"]);
520 s.create(&org, "db", None, b"pw", &BTreeMap::new()).unwrap();
521 assert!(
523 s.create(&org, "vault_token", None, b"x", &BTreeMap::new())
524 .is_err()
525 );
526 assert!(
527 s.create(&org, "new", Some("nope"), b"x", &BTreeMap::new())
528 .is_err()
529 );
530 let e = s
532 .create(&org, "other", Some("fixed"), b"x", &BTreeMap::new())
533 .unwrap_err();
534 assert!(e.to_string().contains("read-only"), "{e}");
535 assert!(s.set(&org, "vault_token", b"x").is_err());
536 assert!(s.delete(&org, "vault_token").is_err());
537 let (v, m) = s.get(&org, "vault_token").unwrap();
538 assert_eq!((v.as_slice(), m.version), (&b"external"[..], 7));
539 let (v, m) = s.get(&org, "db").unwrap();
540 assert_eq!(
541 (v.as_slice(), m.version, m.driver.as_str()),
542 (&b"pw"[..], 1, "local")
543 );
544 assert_eq!(s.set(&org, "db", b"pw2").unwrap().version, 2);
545 assert_eq!(s.version(&org, "db").unwrap(), 2);
546 assert_eq!(s.refresh(&org, "db").unwrap().version, 2);
547 assert_eq!(s.put(&org, "db", b"pw2").unwrap().version, 2);
549 assert_eq!(s.put(&org, "db", b"pw3").unwrap().version, 3);
550 assert_eq!(s.put(&org, "fresh", b"x").unwrap().version, 1);
551 s.delete(&org, "fresh").unwrap();
552 s.set(&org, "db", b"pw2").unwrap();
553 assert_eq!(s.version_in("local", &org, "db").unwrap(), 4);
554 assert_eq!(s.get_in("fixed", &org, "vault_token").unwrap().1, 7);
555 assert!(s.version_in("nope", &org, "db").is_err());
556 s.set(&org, "db", b"pw2").unwrap();
557 s.set(&org, "db", b"pw2").unwrap();
558 assert_eq!(s.version(&org, "db").unwrap(), 6);
559 s.delete(&org, "db").unwrap();
561 s.create(&org, "db", None, b"pw2", &BTreeMap::new())
562 .unwrap();
563 s.set(&org, "db", b"pw2").unwrap();
564 let names: Vec<_> = s.list(&org).unwrap().into_iter().map(|m| m.name).collect();
565 assert_eq!(names, ["db", "vault_token"]);
566 assert!(s.set(&org, "big", &vec![0; MAX_VALUE_BYTES + 1]).is_err());
567 s.delete(&org, "db").unwrap();
568 assert!(matches!(s.inspect(&org, "db"), Err(Error::NotFound(_))));
569 assert!(s.with_driver(Arc::new(Fixed)).is_err());
570 }
571
572 #[test]
573 fn open_generates_and_warns() {
574 let dir = tempfile::tempdir().unwrap();
575 let keys = KeySources {
576 default_file: dir.path().join("cfg/age.txt"),
577 ..Default::default()
578 };
579 let o = Secrets::open(&dir.path().join("state"), &keys, &SecretsConfig::default()).unwrap();
580 assert!(matches!(o.origin, KeyOrigin::Generated(_)));
581 assert!(o.notes.iter().any(|n| n.contains("no break-glass")));
582 let org = OrgId::default_org();
583 o.secrets
584 .create(&org, "k", None, b"v", &BTreeMap::new())
585 .unwrap();
586 let ssh = include_str!("testdata/break_glass_ed25519.pub")
587 .trim()
588 .to_string();
589 let cfg = SecretsConfig {
590 recipients: vec![ssh],
591 };
592 let o2 = Secrets::open(&dir.path().join("state"), &keys, &cfg).unwrap();
593 assert!(matches!(o2.origin, KeyOrigin::DefaultFile(_)));
594 assert!(!o2.notes.iter().any(|n| n.contains("no break-glass")));
595 assert_eq!(o2.secrets.recipients().len(), 2);
596 assert_eq!(o2.secrets.get(&org, "k").unwrap().0, b"v");
597 let a = o2.secrets.encrypt_inline(b"inline").unwrap();
598 assert_eq!(o2.secrets.decrypt_inline(&a).unwrap(), b"inline");
599 let bad = SecretsConfig {
600 recipients: vec!["nope".into()],
601 };
602 assert!(Secrets::open(&dir.path().join("state"), &keys, &bad).is_err());
603 }
604}