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 inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta>;
63
64 fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>>;
66
67 fn create(
69 &self,
70 org: &OrgId,
71 name: &str,
72 _value: &[u8],
73 _labels: &BTreeMap<String, String>,
74 ) -> Result<SecretMeta> {
75 Err(read_only(self.name(), org, name))
76 }
77
78 fn set(&self, org: &OrgId, name: &str, _value: &[u8]) -> Result<u64> {
80 Err(read_only(self.name(), org, name))
81 }
82
83 fn delete(&self, org: &OrgId, name: &str) -> Result<()> {
84 Err(read_only(self.name(), org, name))
85 }
86
87 fn refresh(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
90 self.inspect(org, name)
91 }
92
93 fn reencrypt(&self, _org: Option<&OrgId>) -> Result<usize> {
97 Ok(0)
98 }
99}
100
101pub fn validate_name(name: &str) -> Result<()> {
104 let ok = !name.is_empty()
105 && name.len() <= 128
106 && !name.starts_with('.')
107 && name
108 .bytes()
109 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'));
110 if ok {
111 Ok(())
112 } else {
113 Err(Error::invalid(format!(
114 "secret name {name:?}: 1-128 characters of [A-Za-z0-9_.-], not starting with '.'"
115 )))
116 }
117}
118
119#[doc(hidden)]
120pub fn not_found(org: &OrgId, name: &str) -> Error {
121 Error::NotFound(format!("secret {name} in org {org}"))
122}
123
124pub(crate) fn exists(org: &OrgId, name: &str) -> Error {
125 Error::invalid(format!(
126 "secret {name} already exists in org {org}; use `isb secret set` for a new value"
127 ))
128}
129
130fn read_only(driver: &str, org: &OrgId, name: &str) -> Error {
131 Error::invalid(format!(
132 "secret {name} in org {org}: driver {driver} is read-only"
133 ))
134}
135
136fn check_value(value: &[u8]) -> Result<()> {
137 if value.len() > MAX_VALUE_BYTES {
138 return Err(Error::invalid(format!(
139 "secret value is {} bytes; the limit is {MAX_VALUE_BYTES}",
140 value.len()
141 )));
142 }
143 Ok(())
144}
145
146pub struct Secrets {
148 drivers: Vec<Arc<dyn Driver>>,
149 keyring: Arc<Keyring>,
150}
151
152pub struct Opened {
154 pub secrets: Secrets,
155 pub origin: KeyOrigin,
156 pub notes: Vec<String>,
158}
159
160impl Secrets {
161 pub fn new(local: LocalDriver) -> Secrets {
163 let keyring = local.keyring().clone();
164 Secrets {
165 drivers: vec![Arc::new(local)],
166 keyring,
167 }
168 }
169
170 pub fn open(state_dir: &Path, keys: &KeySources, config: &SecretsConfig) -> Result<Opened> {
173 let break_glass = config.parsed_recipients()?;
174 let k = keys::load_identity(keys)?;
175 let keyring = Arc::new(Keyring::new(k.identity, break_glass));
176 let mut notes = vec![format!(
177 "secrets key from {} (public key {})",
178 k.origin,
179 keyring.public_key()
180 )];
181 notes.extend(k.notes);
182 if keyring.break_glass().is_empty() {
183 notes.push(format!(
184 "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`",
185 SecretsConfig::default_path().display()
186 ));
187 }
188 Ok(Opened {
189 secrets: Secrets::new(LocalDriver::new(state_dir, keyring)),
190 origin: k.origin,
191 notes,
192 })
193 }
194
195 pub fn open_existing(
198 state_dir: &Path,
199 keys: &KeySources,
200 config: &SecretsConfig,
201 ) -> Result<Secrets> {
202 let break_glass = config.parsed_recipients()?;
203 let k = keys::find_identity(keys)?;
204 let keyring = Arc::new(Keyring::new(k.identity, break_glass));
205 Ok(Secrets::new(LocalDriver::new(state_dir, keyring)))
206 }
207
208 pub fn with_driver(mut self, d: Arc<dyn Driver>) -> Result<Secrets> {
210 if self.drivers.iter().any(|x| x.name() == d.name()) {
211 return Err(Error::invalid(format!(
212 "secrets driver {} registered twice",
213 d.name()
214 )));
215 }
216 self.drivers.push(d);
217 Ok(self)
218 }
219
220 pub fn driver_names(&self) -> Vec<String> {
221 self.drivers.iter().map(|d| d.name().to_string()).collect()
222 }
223
224 pub fn keyring(&self) -> &Arc<Keyring> {
225 &self.keyring
226 }
227
228 pub fn recipients(&self) -> Vec<String> {
230 self.keyring
231 .recipients()
232 .iter()
233 .map(|r| r.to_string())
234 .collect()
235 }
236
237 fn driver(&self, name: &str) -> Result<&Arc<dyn Driver>> {
238 self.drivers
239 .iter()
240 .find(|d| d.name() == name)
241 .ok_or_else(|| {
242 Error::invalid(format!(
243 "unknown secrets driver {name:?} (have: {})",
244 self.driver_names().join(", ")
245 ))
246 })
247 }
248
249 fn holder(&self, org: &OrgId, name: &str) -> Result<(&Arc<dyn Driver>, SecretMeta)> {
251 let reference = name.contains('/');
254 if !reference {
255 validate_name(name)?;
256 }
257 for d in &self.drivers {
258 if reference && d.name() == local::DRIVER {
259 continue;
260 }
261 match d.inspect(org, name) {
262 Ok(m) => return Ok((d, m)),
263 Err(Error::NotFound(_)) => {}
264 Err(e) => return Err(e),
265 }
266 }
267 Err(not_found(org, name))
268 }
269
270 pub fn create(
273 &self,
274 org: &OrgId,
275 name: &str,
276 driver: Option<&str>,
277 value: &[u8],
278 labels: &BTreeMap<String, String>,
279 ) -> Result<SecretMeta> {
280 validate_name(name)?;
281 check_value(value)?;
282 for k in labels.keys() {
283 if k.is_empty() || k.len() > 128 {
284 return Err(Error::invalid(format!("label key {k:?}: 1-128 characters")));
285 }
286 }
287 let d = self.driver(driver.unwrap_or(local::DRIVER))?;
288 match self.holder(org, name) {
289 Ok(_) => return Err(exists(org, name)),
290 Err(Error::NotFound(_)) => {}
291 Err(e) => return Err(e),
292 }
293 d.create(org, name, value, labels)
294 }
295
296 pub fn set(&self, org: &OrgId, name: &str, value: &[u8]) -> Result<SecretMeta> {
299 check_value(value)?;
300 let d = match self.holder(org, name) {
301 Ok((d, _)) => d,
302 Err(Error::NotFound(_)) => self.driver(local::DRIVER)?,
303 Err(e) => return Err(e),
304 };
305 d.set(org, name, value)?;
306 d.inspect(org, name)
307 }
308
309 pub fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, SecretMeta)> {
310 let (d, _) = self.holder(org, name)?;
311 let (v, version) = d.get(org, name)?;
312 let mut m = d.inspect(org, name)?;
313 m.version = version;
316 Ok((v, m))
317 }
318
319 pub fn version(&self, org: &OrgId, name: &str) -> Result<u64> {
320 let (d, _) = self.holder(org, name)?;
321 d.version(org, name)
322 }
323
324 pub fn put(&self, org: &OrgId, name: &str, value: &[u8]) -> Result<SecretMeta> {
327 match self.get(org, name) {
328 Ok((v, m)) if v == value => return Ok(m),
329 Ok(_) | Err(Error::NotFound(_)) => {}
330 Err(e) => return Err(e),
331 }
332 self.set(org, name, value)
333 }
334
335 pub fn get_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)> {
338 self.driver(driver)?.get(org, name)
339 }
340
341 pub fn version_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<u64> {
343 self.driver(driver)?.version(org, name)
344 }
345
346 pub fn refresh_in(&self, driver: &str, org: &OrgId, name: &str) -> Result<u64> {
348 let d = self.driver(driver)?;
349 d.refresh(org, name)?;
350 d.version(org, name)
351 }
352
353 pub fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
354 self.holder(org, name).map(|(_, m)| m)
355 }
356
357 pub fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>> {
359 let mut out = Vec::new();
360 for d in &self.drivers {
361 out.extend(d.list(org)?);
362 }
363 out.sort_by(|a, b| a.name.cmp(&b.name));
364 Ok(out)
365 }
366
367 pub fn delete(&self, org: &OrgId, name: &str) -> Result<()> {
368 let (d, _) = self.holder(org, name)?;
369 d.delete(org, name)
370 }
371
372 pub fn refresh(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
373 let (d, _) = self.holder(org, name)?;
374 d.refresh(org, name)
375 }
376
377 pub fn reencrypt(&self, org: Option<&OrgId>) -> Result<usize> {
379 let mut n = 0;
380 for d in &self.drivers {
381 n += d.reencrypt(org)?;
382 }
383 Ok(n)
384 }
385
386 pub fn decrypt_inline(&self, text: &str) -> Result<Vec<u8>> {
388 inline::decrypt_inline(text, &[self.keyring.identity()])
389 }
390
391 pub fn encrypt_inline(&self, value: &[u8]) -> Result<String> {
393 inline::encrypt_inline(value, self.keyring.recipients())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 struct Fixed;
404 impl Driver for Fixed {
405 fn name(&self) -> &str {
406 "fixed"
407 }
408 fn get(&self, org: &OrgId, name: &str) -> Result<(Vec<u8>, u64)> {
409 self.inspect(org, name)
410 .map(|m| (b"external".to_vec(), m.version))
411 }
412 fn version(&self, org: &OrgId, name: &str) -> Result<u64> {
413 self.inspect(org, name).map(|m| m.version)
414 }
415 fn inspect(&self, org: &OrgId, name: &str) -> Result<SecretMeta> {
416 if name != "vault_token" {
417 return Err(not_found(org, name));
418 }
419 Ok(SecretMeta {
420 org: org.clone(),
421 name: name.into(),
422 driver: "fixed".into(),
423 version: 7,
424 created_at: 0,
425 updated_at: 0,
426 labels: BTreeMap::new(),
427 })
428 }
429 fn list(&self, org: &OrgId) -> Result<Vec<SecretMeta>> {
430 Ok(vec![self.inspect(org, "vault_token")?])
431 }
432 }
433
434 fn secrets(dir: &Path) -> Secrets {
435 let k = Keyring::new(age::x25519::Identity::generate(), vec![]);
436 Secrets::new(LocalDriver::new(dir, Arc::new(k)))
437 .with_driver(Arc::new(Fixed))
438 .unwrap()
439 }
440
441 #[test]
442 fn names() {
443 for ok in ["a", "DB_PASSWORD", "tls.key", "x-1", &"n".repeat(128)] {
444 assert!(validate_name(ok).is_ok(), "{ok}");
445 }
446 for bad in [
447 "",
448 ".",
449 "..",
450 ".hidden",
451 "a/b",
452 "../x",
453 "a b",
454 "ü",
455 &"n".repeat(129),
456 ] {
457 assert!(validate_name(bad).is_err(), "{bad:?}");
458 }
459 }
460
461 #[test]
462 fn facade_dispatches_per_secret() {
463 let dir = tempfile::tempdir().unwrap();
464 let s = secrets(dir.path());
465 let org = OrgId::default_org();
466 assert_eq!(s.driver_names(), ["local", "fixed"]);
467 s.create(&org, "db", None, b"pw", &BTreeMap::new()).unwrap();
468 assert!(
470 s.create(&org, "vault_token", None, b"x", &BTreeMap::new())
471 .is_err()
472 );
473 assert!(
474 s.create(&org, "new", Some("nope"), b"x", &BTreeMap::new())
475 .is_err()
476 );
477 let e = s
479 .create(&org, "other", Some("fixed"), b"x", &BTreeMap::new())
480 .unwrap_err();
481 assert!(e.to_string().contains("read-only"), "{e}");
482 assert!(s.set(&org, "vault_token", b"x").is_err());
483 assert!(s.delete(&org, "vault_token").is_err());
484 let (v, m) = s.get(&org, "vault_token").unwrap();
485 assert_eq!((v.as_slice(), m.version), (&b"external"[..], 7));
486 let (v, m) = s.get(&org, "db").unwrap();
487 assert_eq!(
488 (v.as_slice(), m.version, m.driver.as_str()),
489 (&b"pw"[..], 1, "local")
490 );
491 assert_eq!(s.set(&org, "db", b"pw2").unwrap().version, 2);
492 assert_eq!(s.version(&org, "db").unwrap(), 2);
493 assert_eq!(s.refresh(&org, "db").unwrap().version, 2);
494 assert_eq!(s.put(&org, "db", b"pw2").unwrap().version, 2);
496 assert_eq!(s.put(&org, "db", b"pw3").unwrap().version, 3);
497 assert_eq!(s.put(&org, "fresh", b"x").unwrap().version, 1);
498 s.delete(&org, "fresh").unwrap();
499 s.set(&org, "db", b"pw2").unwrap();
500 assert_eq!(s.version_in("local", &org, "db").unwrap(), 4);
501 assert_eq!(s.get_in("fixed", &org, "vault_token").unwrap().1, 7);
502 assert!(s.version_in("nope", &org, "db").is_err());
503 s.set(&org, "db", b"pw2").unwrap();
504 s.set(&org, "db", b"pw2").unwrap();
505 assert_eq!(s.version(&org, "db").unwrap(), 6);
506 s.delete(&org, "db").unwrap();
508 s.create(&org, "db", None, b"pw2", &BTreeMap::new())
509 .unwrap();
510 s.set(&org, "db", b"pw2").unwrap();
511 let names: Vec<_> = s.list(&org).unwrap().into_iter().map(|m| m.name).collect();
512 assert_eq!(names, ["db", "vault_token"]);
513 assert!(s.set(&org, "big", &vec![0; MAX_VALUE_BYTES + 1]).is_err());
514 s.delete(&org, "db").unwrap();
515 assert!(matches!(s.inspect(&org, "db"), Err(Error::NotFound(_))));
516 assert!(s.with_driver(Arc::new(Fixed)).is_err());
517 }
518
519 #[test]
520 fn open_generates_and_warns() {
521 let dir = tempfile::tempdir().unwrap();
522 let keys = KeySources {
523 default_file: dir.path().join("cfg/age.txt"),
524 ..Default::default()
525 };
526 let o = Secrets::open(&dir.path().join("state"), &keys, &SecretsConfig::default()).unwrap();
527 assert!(matches!(o.origin, KeyOrigin::Generated(_)));
528 assert!(o.notes.iter().any(|n| n.contains("no break-glass")));
529 let org = OrgId::default_org();
530 o.secrets
531 .create(&org, "k", None, b"v", &BTreeMap::new())
532 .unwrap();
533 let ssh = include_str!("testdata/break_glass_ed25519.pub")
534 .trim()
535 .to_string();
536 let cfg = SecretsConfig {
537 recipients: vec![ssh],
538 };
539 let o2 = Secrets::open(&dir.path().join("state"), &keys, &cfg).unwrap();
540 assert!(matches!(o2.origin, KeyOrigin::DefaultFile(_)));
541 assert!(!o2.notes.iter().any(|n| n.contains("no break-glass")));
542 assert_eq!(o2.secrets.recipients().len(), 2);
543 assert_eq!(o2.secrets.get(&org, "k").unwrap().0, b"v");
544 let a = o2.secrets.encrypt_inline(b"inline").unwrap();
545 assert_eq!(o2.secrets.decrypt_inline(&a).unwrap(), b"inline");
546 let bad = SecretsConfig {
547 recipients: vec!["nope".into()],
548 };
549 assert!(Secrets::open(&dir.path().join("state"), &keys, &bad).is_err());
550 }
551}