1use crate::error::ServerError;
10use crate::subservice::{State, Status, Subservice};
11use m4a_agent::vault::EnvMasterKey;
12use m4a_agent::{FileVault, KeyVault};
13use std::path::{Path, PathBuf};
14
15pub struct M4aSubservice {
16 store_root: PathBuf,
17 master_key_var: Option<String>,
18 vault_kind: Option<&'static str>,
19 failed: Option<String>,
20}
21
22impl M4aSubservice {
23 pub fn new(data_dir: &Path, master_key_var: Option<String>) -> Self {
24 Self {
25 store_root: data_dir.join("m4a"),
26 master_key_var,
27 vault_kind: None,
28 failed: None,
29 }
30 }
31
32 pub fn vault_dir(&self) -> PathBuf {
33 self.store_root.join(".m4a-agent")
34 }
35
36 fn open(&self) -> Result<&'static str, ServerError> {
37 let var = self
38 .master_key_var
39 .as_deref()
40 .filter(|v| !v.is_empty())
41 .ok_or_else(|| {
42 ServerError::sub("m4a", "refusing to start: no master key variable configured")
43 })?;
44 let dir = self.vault_dir();
45 std::fs::create_dir_all(&dir)?;
46 let marker = dir.join("vault.home");
47 if !marker.exists() {
48 write_private(&marker, b"envkey")?;
49 }
50 let vault = FileVault::open(dir.join("vault.enc"), Box::new(EnvMasterKey { var: var.to_owned() }))
51 .map_err(|e| ServerError::sub("m4a", format!("key vault: {e}")))?;
52 Ok(vault.kind())
53 }
54}
55
56fn write_private(path: &Path, data: &[u8]) -> std::io::Result<()> {
57 use std::io::Write;
58 #[cfg(unix)]
59 use std::os::unix::fs::OpenOptionsExt;
60 let mut options = std::fs::OpenOptions::new();
61 options.write(true).create_new(true);
62 #[cfg(unix)]
63 options.mode(0o600);
64 options.open(path)?.write_all(data)
65}
66
67impl Subservice for M4aSubservice {
68 fn name(&self) -> &'static str {
69 "m4a"
70 }
71
72 fn start(&mut self) -> Result<(), ServerError> {
73 match self.open() {
74 Ok(kind) => {
75 self.vault_kind = Some(kind);
76 self.failed = None;
77 Ok(())
78 }
79 Err(error) => {
80 self.vault_kind = None;
81 self.failed = Some(error.to_string());
82 Err(error)
83 }
84 }
85 }
86
87 fn stop(&mut self) {
88 self.vault_kind = None;
89 }
90
91 fn status(&mut self) -> Status {
92 let state = if self.vault_kind.is_some() {
93 State::Running
94 } else if self.failed.is_some() {
95 State::Failed
96 } else {
97 State::Stopped
98 };
99 let detail = self.vault_kind.map(str::to_owned).or_else(|| self.failed.clone());
101 Status { name: "m4a", state, port: None, detail }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn refuses_without_master_key_variable() {
111 let dir = tempfile::tempdir().unwrap();
112 let mut service = M4aSubservice::new(dir.path(), None);
113 assert!(service.start().is_err());
114 assert_eq!(service.status().state, State::Failed);
115 }
116
117 #[test]
118 fn refuses_when_variable_is_unset_or_short() {
119 let dir = tempfile::tempdir().unwrap();
120 let mut unset = M4aSubservice::new(dir.path(), Some("HATCHERY_TEST_M4A_UNSET_KEY".into()));
121 assert!(unset.start().is_err());
122 assert!(!dir.path().join("m4a/.m4a-agent/vault.enc").exists(), "no vault under a missing key");
123 }
124
125 #[test]
126 fn opens_vault_with_env_master_key_and_never_reports_it() {
127 let dir = tempfile::tempdir().unwrap();
128 let var = "HATCHERY_TEST_M4A_MASTER_OK";
129 std::env::set_var(var, "test-master-secret-0123456789");
131 let mut service = M4aSubservice::new(dir.path(), Some(var.into()));
132 service.start().unwrap();
133 let status = service.status();
134 assert_eq!(status.state, State::Running);
135 let json = serde_json::to_string(&status).unwrap();
136 assert!(!json.contains("test-master-secret"), "{json}");
137 assert!(dir.path().join("m4a/.m4a-agent/vault.home").exists());
138 let vault = FileVault::open(
140 service.vault_dir().join("vault.enc"),
141 Box::new(EnvMasterKey { var: var.to_owned() }),
142 )
143 .unwrap();
144 vault.put("test-label", b"value").unwrap();
145 service.stop();
146 service.start().unwrap();
147 let other = "HATCHERY_TEST_M4A_MASTER_OTHER";
149 std::env::set_var(other, "another-master-secret-9876543210");
150 let mut wrong = M4aSubservice::new(dir.path(), Some(other.into()));
151 assert!(wrong.start().is_err(), "a wrong master key must not open the vault");
152 std::env::remove_var(var);
153 std::env::remove_var(other);
154 }
155}