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 kind(&self) -> &'static str;
34}
35
36#[derive(Default)]
38pub struct MemoryVault(Mutex<HashMap<String, Vec<u8>>>);
39
40impl MemoryVault {
41 pub fn new() -> Self {
42 Self::default()
43 }
44}
45
46impl KeyVault for MemoryVault {
47 fn get(&self, label: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
48 Ok(self.0.lock().unwrap_or_else(|e| e.into_inner()).get(label).map(|v| Zeroizing::new(v.clone())))
49 }
50 fn put(&self, label: &str, value: &[u8]) -> Result<()> {
51 self.0.lock().unwrap_or_else(|e| e.into_inner()).insert(label.into(), value.to_vec());
52 Ok(())
53 }
54 fn delete(&self, label: &str) -> Result<bool> {
55 Ok(self.0.lock().unwrap_or_else(|e| e.into_inner()).remove(label).is_some())
56 }
57 fn kind(&self) -> &'static str {
58 "memory"
59 }
60}
61
62pub trait MasterKeySource: Send + Sync {
64 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>>;
66 fn name(&self) -> &'static str;
67}
68
69pub struct FileMasterKey(pub PathBuf);
71
72impl MasterKeySource for FileMasterKey {
73 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
74 match std::fs::read(&self.0) {
75 Ok(b) if b.len() == 32 => Ok(Zeroizing::new(b)),
76 Ok(_) => Err(AgentError::Vault("master key file has the wrong size".into())),
77 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
78 let k = random_bytes(32);
79 write_private(&self.0, &k)?;
80 Ok(k)
81 }
82 Err(e) => Err(AgentError::Vault(format!("master key file: {e}"))),
83 }
84 }
85 fn name(&self) -> &'static str {
86 "keyfile"
87 }
88}
89
90#[cfg(feature = "vault-keychain")]
92pub struct KeychainMasterKey {
93 pub service: String,
94 pub account: String,
95}
96
97#[cfg(feature = "vault-keychain")]
98impl MasterKeySource for KeychainMasterKey {
99 fn load_or_create(&self) -> Result<Zeroizing<Vec<u8>>> {
100 let e = |x: keyring::Error| AgentError::Vault(format!("keychain: {x}"));
101 let entry = keyring::Entry::new(&self.service, &self.account).map_err(e)?;
102 match entry.get_secret() {
103 Ok(b) if b.len() == 32 => Ok(Zeroizing::new(b)),
104 Ok(_) => Err(AgentError::Vault("keychain entry has the wrong size".into())),
105 Err(keyring::Error::NoEntry) => {
106 let k = random_bytes(32);
107 entry.set_secret(&k).map_err(e)?;
108 match entry.get_secret() {
110 Ok(b) if b[..] == k[..] => Ok(k),
111 _ => Err(AgentError::Vault("keychain did not keep the key".into())),
112 }
113 }
114 Err(x) => Err(e(x)),
115 }
116 }
117 fn name(&self) -> &'static str {
118 "keychain"
119 }
120}
121
122fn write_private(path: &Path, data: &[u8]) -> Result<()> {
123 let io = |e: std::io::Error| AgentError::Vault(format!("{}: {e}", path.display()));
124 if let Some(d) = path.parent() {
125 std::fs::create_dir_all(d).map_err(io)?;
126 }
127 let tmp = path.with_extension("tmp");
128 {
129 use std::io::Write;
130 let mut opts = std::fs::OpenOptions::new();
131 opts.write(true).create(true).truncate(true);
132 #[cfg(unix)]
133 {
134 use std::os::unix::fs::OpenOptionsExt;
135 opts.mode(0o600);
136 }
137 let mut f = opts.open(&tmp).map_err(io)?;
138 f.write_all(data).map_err(io)?;
139 f.sync_all().map_err(io)?;
140 }
141 std::fs::rename(&tmp, path).map_err(io)
142}
143
144const AAD: &[u8] = b"m4a-vault-v1";
145
146pub struct FileVault {
148 path: PathBuf,
149 master: Box<dyn MasterKeySource>,
150 kind: &'static str,
151 lock: Mutex<()>,
152}
153
154impl FileVault {
155 pub fn open(path: impl Into<PathBuf>, master: Box<dyn MasterKeySource>) -> Result<Self> {
156 let kind = if master.name() == "keychain" { "file+keychain" } else { "file+keyfile" };
157 let v = Self { path: path.into(), master, kind, lock: Mutex::new(()) };
158 v.load()?; Ok(v)
160 }
161
162 pub fn open_default(dir: &Path, service: &str) -> Result<Self> {
165 let marker = dir.join("vault.home");
166 let want = std::fs::read_to_string(&marker).ok().map(|s| s.trim().to_string());
167 let file = || -> Box<dyn MasterKeySource> { Box::new(FileMasterKey(dir.join("vault.key"))) };
168 #[cfg(feature = "vault-keychain")]
169 let keychain = || -> Box<dyn MasterKeySource> { Box::new(KeychainMasterKey { service: service.to_string(), account: format!("vault:{}", dir.display()) }) };
170 let path = dir.join("vault.enc");
171 let (v, home) = match want.as_deref() {
172 Some("keyfile") => (Self::open(&path, file())?, "keyfile"),
173 #[cfg(feature = "vault-keychain")]
174 Some("keychain") => (Self::open(&path, keychain())?, "keychain"),
175 Some(other) => return Err(AgentError::Vault(format!("vault home {other:?} is not available in this build"))),
176 None => {
177 #[cfg(feature = "vault-keychain")]
178 {
179 match Self::open(&path, keychain()) {
180 Ok(v) => (v, "keychain"),
181 Err(_) => (Self::open(&path, file())?, "keyfile"),
182 }
183 }
184 #[cfg(not(feature = "vault-keychain"))]
185 {
186 let _ = service;
187 (Self::open(&path, file())?, "keyfile")
188 }
189 }
190 };
191 if want.is_none() {
192 write_private(&marker, home.as_bytes())?;
193 }
194 Ok(v)
195 }
196
197 fn cipher(&self) -> Result<Aes256Gcm> {
198 let k = self.master.load_or_create()?;
199 Ok(Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&k)))
200 }
201
202 fn load(&self) -> Result<BTreeMap<String, String>> {
203 let raw = match std::fs::read(&self.path) {
204 Ok(r) => r,
205 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
206 self.cipher()?; return Ok(BTreeMap::new());
208 }
209 Err(e) => return Err(AgentError::Vault(format!("{}: {e}", self.path.display()))),
210 };
211 if raw.len() < 13 {
212 return Err(AgentError::Vault("vault file is truncated".into()));
213 }
214 let (nonce, ct) = raw.split_at(12);
215 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()))?);
216 serde_json::from_slice(&plain).map_err(|e| AgentError::Vault(format!("vault content: {e}")))
217 }
218
219 fn store(&self, map: &BTreeMap<String, String>) -> Result<()> {
220 let plain = Zeroizing::new(serde_json::to_vec(map).map_err(|e| AgentError::Vault(e.to_string()))?);
221 let mut nonce = [0u8; 12];
222 rand::thread_rng().fill_bytes(&mut nonce);
223 let ct = self.cipher()?.encrypt(Nonce::from_slice(&nonce), Payload { msg: &plain, aad: AAD }).map_err(|_| AgentError::Vault("encrypt failed".into()))?;
224 let mut out = nonce.to_vec();
225 out.extend(ct);
226 write_private(&self.path, &out)
227 }
228}
229
230impl KeyVault for FileVault {
231 fn get(&self, label: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
232 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
233 Ok(self.load()?.get(label).and_then(|v| STANDARD.decode(v).ok()).map(Zeroizing::new))
234 }
235 fn put(&self, label: &str, value: &[u8]) -> Result<()> {
236 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
237 let mut m = self.load()?;
238 m.insert(label.into(), STANDARD.encode(value));
239 self.store(&m)
240 }
241 fn delete(&self, label: &str) -> Result<bool> {
242 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
243 let mut m = self.load()?;
244 let had = m.remove(label).is_some();
245 if had {
246 self.store(&m)?;
247 }
248 Ok(had)
249 }
250 fn kind(&self) -> &'static str {
251 self.kind
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn file_vault_round_trips_and_never_writes_plaintext() {
261 let d = tempfile::tempdir().unwrap();
262 let v = FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join("v.key")))).unwrap();
263 assert!(v.get("a").unwrap().is_none());
264 v.put("a", b"super-secret-seed").unwrap();
265 v.put("b", &[1, 2, 3]).unwrap();
266 assert_eq!(&v.get("a").unwrap().unwrap()[..], b"super-secret-seed");
267 let raw = std::fs::read(d.path().join("v.enc")).unwrap();
268 assert!(!raw.windows(12).any(|w| w == b"super-secret"));
269 let v2 = FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join("v.key")))).unwrap();
271 assert_eq!(&v2.get("b").unwrap().unwrap()[..], &[1, 2, 3]);
272 assert!(v2.delete("a").unwrap() && !v2.delete("a").unwrap());
273 assert!(v2.get("a").unwrap().is_none());
274 #[cfg(unix)]
275 {
276 use std::os::unix::fs::PermissionsExt;
277 assert_eq!(std::fs::metadata(d.path().join("v.key")).unwrap().permissions().mode() & 0o777, 0o600);
278 assert_eq!(std::fs::metadata(d.path().join("v.enc")).unwrap().permissions().mode() & 0o777, 0o600);
279 }
280 }
281
282 #[test]
283 fn a_wrong_or_lost_master_key_is_an_error_not_an_empty_vault() {
284 let d = tempfile::tempdir().unwrap();
285 let open = |k: &str| FileVault::open(d.path().join("v.enc"), Box::new(FileMasterKey(d.path().join(k))));
286 open("k1").unwrap().put("a", b"x").unwrap();
287 assert!(open("k2").is_err());
288 let mut raw = std::fs::read(d.path().join("v.enc")).unwrap();
290 let n = raw.len() - 1;
291 raw[n] ^= 1;
292 std::fs::write(d.path().join("v.enc"), raw).unwrap();
293 assert!(open("k1").is_err());
294 }
295
296 #[test]
297 fn open_default_remembers_where_the_master_key_lives() {
298 let d = tempfile::tempdir().unwrap();
299 let v = FileVault::open_default(d.path(), "m4a-agent-test").unwrap();
302 v.put("a", b"1").unwrap();
303 let kind = v.kind();
304 let v = FileVault::open_default(d.path(), "m4a-agent-test").unwrap();
305 assert_eq!(v.kind(), kind);
306 assert_eq!(&v.get("a").unwrap().unwrap()[..], b"1");
307 }
308}