1use std::collections::{BTreeMap, HashMap};
10use std::path::{Path, PathBuf};
11use std::sync::Mutex;
12
13use aes_gcm::aead::{Aead, KeyInit, Payload};
14use aes_gcm::{Aes256Gcm, Key, Nonce};
15use base64::engine::general_purpose::STANDARD;
16use base64::Engine;
17use rand::RngCore;
18use zeroize::Zeroizing;
19
20use crate::error::{AgentError, Result};
21
22pub fn random_bytes(n: usize) -> Zeroizing<Vec<u8>> {
23 let mut b = Zeroizing::new(vec![0u8; n]);
24 rand::thread_rng().fill_bytes(&mut b);
25 b
26}
27
28pub trait KeyVault: Send + Sync {
29 fn get(&self, label: &str) -> Result<Option<Zeroizing<Vec<u8>>>>;
30 fn put(&self, label: &str, value: &[u8]) -> Result<()>;
31 fn delete(&self, label: &str) -> Result<bool>;
32 fn labels(&self) -> Result<Vec<String>> {
34 Err(AgentError::Vault("this vault cannot list its labels".into()))
35 }
36 fn kind(&self) -> &'static str;
38 fn external_identity(&self, _session_id: &str) -> Option<[u8; 32]> {
41 None
42 }
43 fn sign_external(&self, _session_id: &str, _msg: &[u8]) -> Result<[u8; 64]> {
45 Err(AgentError::Vault("this vault holds no external identity".into()))
46 }
47}
48
49pub struct StaticMasterKey(pub Zeroizing<Vec<u8>>);
52
53impl MasterKeySource for StaticMasterKey {
54 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
55 if self.0.len() != 32 {
56 return Err(AgentError::Vault("master key must be 32 bytes".into()));
57 }
58 Ok(self.0.clone())
59 }
60 fn name(&self) -> &'static str {
61 "hostkey"
62 }
63}
64
65#[derive(Default)]
67pub struct MemoryVault(Mutex<HashMap<String, Vec<u8>>>);
68
69impl MemoryVault {
70 pub fn new() -> Self {
71 Self::default()
72 }
73}
74
75impl KeyVault for MemoryVault {
76 fn get(&self, label: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
77 Ok(self.0.lock().unwrap_or_else(|e| e.into_inner()).get(label).map(|v| Zeroizing::new(v.clone())))
78 }
79 fn put(&self, label: &str, value: &[u8]) -> Result<()> {
80 self.0.lock().unwrap_or_else(|e| e.into_inner()).insert(label.into(), value.to_vec());
81 Ok(())
82 }
83 fn delete(&self, label: &str) -> Result<bool> {
84 Ok(self.0.lock().unwrap_or_else(|e| e.into_inner()).remove(label).is_some())
85 }
86 fn labels(&self) -> Result<Vec<String>> {
87 let mut v: Vec<String> = self.0.lock().unwrap_or_else(|e| e.into_inner()).keys().cloned().collect();
88 v.sort();
89 Ok(v)
90 }
91 fn kind(&self) -> &'static str {
92 "memory"
93 }
94}
95
96pub trait MasterKeySource: Send + Sync {
98 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>>;
100 fn name(&self) -> &'static str;
101}
102
103pub struct FileMasterKey(pub PathBuf);
105
106impl MasterKeySource for FileMasterKey {
107 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
108 match std::fs::read(&self.0) {
109 Ok(b) if b.len() == 32 => Ok(Zeroizing::new(b)),
110 Ok(_) => Err(AgentError::Vault("master key file has the wrong size".into())),
111 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
112 let k = random_bytes(32);
113 write_private(&self.0, &k)?;
114 Ok(k)
115 }
116 Err(e) => Err(AgentError::Vault(format!("master key file: {e}"))),
117 }
118 }
119 fn name(&self) -> &'static str {
120 "keyfile"
121 }
122}
123
124pub const VAULT_KEY_ENV: &str = "M4A_VAULT_KEY_ENV";
129
130pub struct EnvMasterKey {
133 pub var: String,
134}
135
136impl MasterKeySource for EnvMasterKey {
137 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
138 use sha2::{Digest, Sha256};
139 let value = Zeroizing::new(std::env::var(&self.var).map_err(|_| AgentError::Vault(format!("the vault key variable {} is not set", self.var)))?);
140 if value.trim().len() < 16 {
141 return Err(AgentError::Vault(format!("the vault key variable {} must hold at least 16 characters", self.var)));
142 }
143 let mut h = Sha256::new();
144 h.update(b"m4a-vault-master-v1\0");
145 h.update(value.trim().as_bytes());
146 Ok(Zeroizing::new(h.finalize().to_vec()))
147 }
148 fn name(&self) -> &'static str {
149 "envkey"
150 }
151}
152
153#[cfg(feature = "vault-keychain")]
155pub struct KeychainMasterKey {
156 pub service: String,
157 pub account: String,
158}
159
160#[cfg(feature = "vault-keychain")]
161impl MasterKeySource for KeychainMasterKey {
162 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
163 let e = |x: keyring::Error| AgentError::Vault(format!("keychain: {x}"));
164 let entry = keyring::Entry::new(&self.service, &self.account).map_err(e)?;
165 match entry.get_secret() {
166 Ok(b) if b.len() == 32 => Ok(Zeroizing::new(b)),
167 Ok(_) => Err(AgentError::Vault("keychain entry has the wrong size".into())),
168 Err(keyring::Error::NoEntry) => {
169 let k = random_bytes(32);
170 entry.set_secret(&k).map_err(e)?;
171 match entry.get_secret() {
173 Ok(b) if b[..] == k[..] => Ok(k),
174 _ => Err(AgentError::Vault("keychain did not keep the key".into())),
175 }
176 }
177 Err(x) => Err(e(x)),
178 }
179 }
180 fn name(&self) -> &'static str {
181 "keychain"
182 }
183}
184
185fn write_private(path: &Path, data: &[u8]) -> Result<()> {
186 let io = |e: std::io::Error| AgentError::Vault(format!("{}: {e}", path.display()));
187 if let Some(d) = path.parent() {
188 std::fs::create_dir_all(d).map_err(io)?;
189 }
190 let tmp = path.with_extension("tmp");
191 {
192 use std::io::Write;
193 let mut opts = std::fs::OpenOptions::new();
194 opts.write(true).create(true).truncate(true);
195 #[cfg(unix)]
196 {
197 use std::os::unix::fs::OpenOptionsExt;
198 opts.mode(0o600);
199 }
200 let mut f = opts.open(&tmp).map_err(io)?;
201 f.write_all(data).map_err(io)?;
202 f.sync_all().map_err(io)?;
203 }
204 std::fs::rename(&tmp, path).map_err(io)
205}
206
207const AAD: &[u8] = b"m4a-vault-v1";
208
209pub struct FileVault {
211 path: PathBuf,
212 master: Box<dyn MasterKeySource>,
213 kind: &'static str,
214 lock: Mutex<()>,
215}
216
217impl FileVault {
218 pub fn open(path: impl Into<PathBuf>, master: Box<dyn MasterKeySource>) -> Result<Self> {
219 let kind = match master.name() {
220 "keychain" => "file+keychain",
221 "envkey" => "file+envkey",
222 _ => "file+keyfile",
223 };
224 let v = Self { path: path.into(), master, kind, lock: Mutex::new(()) };
225 v.load()?; Ok(v)
227 }
228
229 pub fn open_default(dir: &Path, service: &str) -> Result<Self> {
232 let marker = dir.join("vault.home");
233 let want = std::fs::read_to_string(&marker).ok().map(|s| s.trim().to_string());
234 let file = || -> Box<dyn MasterKeySource> { Box::new(FileMasterKey(dir.join("vault.key"))) };
235 #[cfg(feature = "vault-keychain")]
236 let keychain = || -> Box<dyn MasterKeySource> { Box::new(KeychainMasterKey { service: service.to_string(), account: format!("vault:{}", dir.display()) }) };
237 let path = dir.join("vault.enc");
238 let env_var = std::env::var(VAULT_KEY_ENV).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
240 if want.as_deref() == Some("envkey") && env_var.is_none() {
241 return Err(AgentError::Vault(format!("this vault's key comes from the environment: set {VAULT_KEY_ENV} to the name of the secret that holds it")));
242 }
243 if let Some(var) = env_var {
244 match want.as_deref() {
245 None | Some("envkey") => {
246 let v = Self::open(&path, Box::new(EnvMasterKey { var }))?;
247 if want.is_none() {
248 write_private(&marker, b"envkey")?;
249 }
250 return Ok(v);
251 }
252 Some(other) => return Err(AgentError::Vault(format!("this vault lives under {other}; to move it under {VAULT_KEY_ENV} make a backup, start a new vault and restore the backup"))),
253 }
254 }
255 let (v, home) = match want.as_deref() {
256 Some("keyfile") => (Self::open(&path, file())?, "keyfile"),
257 #[cfg(feature = "vault-keychain")]
258 Some("keychain") => (Self::open(&path, keychain())?, "keychain"),
259 Some(other) => return Err(AgentError::Vault(format!("vault home {other:?} is not available in this build"))),
260 None => {
261 #[cfg(feature = "vault-keychain")]
262 {
263 match Self::open(&path, keychain()) {
264 Ok(v) => (v, "keychain"),
265 Err(_) => (Self::open(&path, file())?, "keyfile"),
266 }
267 }
268 #[cfg(not(feature = "vault-keychain"))]
269 {
270 let _ = service;
271 (Self::open(&path, file())?, "keyfile")
272 }
273 }
274 };
275 if want.is_none() {
276 write_private(&marker, home.as_bytes())?;
277 }
278 Ok(v)
279 }
280
281 fn cipher(&self) -> Result<Aes256Gcm> {
282 let k = self.master.load_or_create()?;
283 Ok(Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&k)))
284 }
285
286 fn load(&self) -> Result<BTreeMap<String, String>> {
287 let raw = match std::fs::read(&self.path) {
288 Ok(r) => r,
289 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
290 self.cipher()?; return Ok(BTreeMap::new());
292 }
293 Err(e) => return Err(AgentError::Vault(format!("{}: {e}", self.path.display()))),
294 };
295 if raw.len() < 13 {
296 return Err(AgentError::Vault("vault file is truncated".into()));
297 }
298 let (nonce, ct) = raw.split_at(12);
299 let plain = Zeroizing::new(self.cipher()?.decrypt(Nonce::from_slice(nonce), Payload { msg: ct, aad: AAD }).map_err(|_| AgentError::Vault("vault does not open with its master key".into()))?);
300 serde_json::from_slice(&plain).map_err(|e| AgentError::Vault(format!("vault content: {e}")))
301 }
302
303 fn store(&self, map: &BTreeMap<String, String>) -> Result<()> {
304 let plain = Zeroizing::new(serde_json::to_vec(map).map_err(|e| AgentError::Vault(e.to_string()))?);
305 let mut nonce = [0u8; 12];
306 rand::thread_rng().fill_bytes(&mut nonce);
307 let ct = self.cipher()?.encrypt(Nonce::from_slice(&nonce), Payload { msg: &plain, aad: AAD }).map_err(|_| AgentError::Vault("encrypt failed".into()))?;
308 let mut out = nonce.to_vec();
309 out.extend(ct);
310 write_private(&self.path, &out)
311 }
312}
313
314impl KeyVault for FileVault {
315 fn get(&self, label: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
316 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
317 Ok(self.load()?.get(label).and_then(|v| STANDARD.decode(v).ok()).map(Zeroizing::new))
318 }
319 fn put(&self, label: &str, value: &[u8]) -> Result<()> {
320 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
321 let mut m = self.load()?;
322 m.insert(label.into(), STANDARD.encode(value));
323 self.store(&m)
324 }
325 fn delete(&self, label: &str) -> Result<bool> {
326 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
327 let mut m = self.load()?;
328 let had = m.remove(label).is_some();
329 if had {
330 self.store(&m)?;
331 }
332 Ok(had)
333 }
334 fn labels(&self) -> Result<Vec<String>> {
335 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
336 Ok(self.load()?.keys().cloned().collect())
337 }
338 fn kind(&self) -> &'static str {
339 self.kind
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 static ENV_LOCK: Mutex<()> = Mutex::new(());
349
350 #[test]
351 fn file_vault_round_trips_and_never_writes_plaintext() {
352 let d = tempfile::tempdir().unwrap();
353 let v = FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join("v.key")))).unwrap();
354 assert!(v.get("a").unwrap().is_none());
355 v.put("a", b"super-secret-seed").unwrap();
356 v.put("b", &[1, 2, 3]).unwrap();
357 assert_eq!(&v.get("a").unwrap().unwrap()[..], b"super-secret-seed");
358 let raw = std::fs::read(d.path().join("v.enc")).unwrap();
359 assert!(!raw.windows(12).any(|w| w == b"super-secret"));
360 let v2 = FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join("v.key")))).unwrap();
362 assert_eq!(&v2.get("b").unwrap().unwrap()[..], &[1, 2, 3]);
363 assert!(v2.delete("a").unwrap() && !v2.delete("a").unwrap());
364 assert!(v2.get("a").unwrap().is_none());
365 #[cfg(unix)]
366 {
367 use std::os::unix::fs::PermissionsExt;
368 assert_eq!(std::fs::metadata(d.path().join("v.key")).unwrap().permissions().mode() & 0o777, 0o600);
369 assert_eq!(std::fs::metadata(d.path().join("v.enc")).unwrap().permissions().mode() & 0o777, 0o600);
370 }
371 }
372
373 #[test]
374 fn a_wrong_or_lost_master_key_is_an_error_not_an_empty_vault() {
375 let d = tempfile::tempdir().unwrap();
376 let open = |k: &str| FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join(k))));
377 open("k1").unwrap().put("a", b"x").unwrap();
378 assert!(open("k2").is_err());
379 let mut raw = std::fs::read(d.path().join("v.enc")).unwrap();
381 let n = raw.len() - 1;
382 raw[n] ^= 1;
383 std::fs::write(d.path().join("v.enc"), raw).unwrap();
384 assert!(open("k1").is_err());
385 }
386
387 #[test]
388 fn open_default_remembers_where_the_master_key_lives() {
389 let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
390 let d = tempfile::tempdir().unwrap();
391 let v = FileVault::open_default(d.path(), "m4a-agent-test").unwrap();
394 v.put("a", b"1").unwrap();
395 let kind = v.kind();
396 let v = FileVault::open_default(d.path(), "m4a-agent-test").unwrap();
397 assert_eq!(v.kind(), kind);
398 assert_eq!(&v.get("a").unwrap().unwrap()[..], b"1");
399 }
400
401 #[test]
402 fn the_master_key_can_come_from_the_environment_and_never_from_a_file_beside_the_vault() {
403 let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
404 let d = tempfile::tempdir().unwrap();
405 std::env::set_var("M4A_TEST_VAULT_SECRET_A", "a-long-enough-secret-value-1");
407 std::env::set_var(VAULT_KEY_ENV, "M4A_TEST_VAULT_SECRET_A");
408 let v = FileVault::open_default(d.path(), "svc").unwrap();
409 v.put("x", b"1").unwrap();
410 assert_eq!(v.kind(), "file+envkey");
411 assert!(!d.path().join("vault.key").exists(), "no key file");
412 drop(v);
413 assert!(FileVault::open_default(d.path(), "svc").unwrap().get("x").unwrap().is_some());
415 std::env::set_var("M4A_TEST_VAULT_SECRET_B", "another-long-secret-value-22");
416 std::env::set_var(VAULT_KEY_ENV, "M4A_TEST_VAULT_SECRET_B");
417 assert!(FileVault::open_default(d.path(), "svc").is_err());
418 std::env::remove_var(VAULT_KEY_ENV);
419 let e = FileVault::open_default(d.path(), "svc").err().unwrap().to_string();
420 assert!(e.contains(VAULT_KEY_ENV), "{e}");
421 let d2 = tempfile::tempdir().unwrap();
423 FileVault::open_default(d2.path(), "svc").unwrap().put("y", b"2").unwrap();
424 std::env::set_var(VAULT_KEY_ENV, "M4A_TEST_VAULT_SECRET_A");
425 assert!(FileVault::open_default(d2.path(), "svc").is_err());
426 std::env::remove_var(VAULT_KEY_ENV);
427 std::env::set_var("M4A_TEST_VAULT_SHORT", "short");
429 std::env::set_var(VAULT_KEY_ENV, "M4A_TEST_VAULT_SHORT");
430 let d3 = tempfile::tempdir().unwrap();
431 assert!(FileVault::open_default(d3.path(), "svc").is_err());
432 std::env::remove_var(VAULT_KEY_ENV);
433 }
434}