1use std::collections::{BTreeMap, HashMap};
4use std::env;
5use std::process::Command;
6
7use crate::{crypto, encrypt_value, now_utc, types};
8
9#[derive(Debug)]
11pub struct DiscoveredKey {
12 pub secret_key: String,
13 pub pubkey: String,
14}
15
16pub fn discover_existing_key() -> Result<Option<DiscoveredKey>, String> {
19 let raw = env::var(crate::env::ENV_MURK_KEY)
20 .ok()
21 .filter(|k| !k.is_empty())
22 .or_else(crate::read_key_from_dotenv);
23
24 match raw {
25 Some(key) => {
26 let identity = crypto::parse_identity(&key).map_err(|e| e.to_string())?;
27 let pubkey = identity.pubkey_string().map_err(|e| e.to_string())?;
28 Ok(Some(DiscoveredKey {
29 secret_key: key,
30 pubkey,
31 }))
32 }
33 None => Ok(None),
34 }
35}
36
37#[derive(Debug)]
39pub struct InitStatus {
40 pub authorized: bool,
42 pub pubkey: String,
44 pub display_name: Option<String>,
46}
47
48pub fn check_init_status(vault: &types::Vault, secret_key: &str) -> Result<InitStatus, String> {
53 let identity = crypto::parse_identity(secret_key).map_err(|e| e.to_string())?;
54 let pubkey = identity.pubkey_string().map_err(|e| e.to_string())?;
55 let authorized = vault.recipients.contains(&pubkey);
56
57 let display_name = if authorized {
58 crate::decrypt_meta(vault, &identity)
59 .and_then(|meta| meta.recipients.get(&pubkey).cloned())
60 .filter(|name| !name.is_empty())
61 } else {
62 None
63 };
64
65 Ok(InitStatus {
66 authorized,
67 pubkey,
68 display_name,
69 })
70}
71
72pub fn create_vault(
77 vault_name: &str,
78 pubkey: &str,
79 name: &str,
80) -> Result<types::Vault, crate::error::MurkError> {
81 use crate::error::MurkError;
82
83 let mut recipient_names = HashMap::new();
84 recipient_names.insert(pubkey.to_string(), name.to_string());
85
86 let recipient = crypto::parse_recipient(pubkey)?;
87
88 let repo = Command::new("git")
90 .args(["remote", "get-url", "origin"])
91 .output()
92 .ok()
93 .filter(|o| o.status.success())
94 .and_then(|o| String::from_utf8(o.stdout).ok())
95 .map(|s| s.trim().to_string())
96 .unwrap_or_default();
97
98 let mut vault = types::Vault {
99 version: types::VAULT_VERSION.into(),
100 created: now_utc(),
101 vault_name: vault_name.into(),
102 repo,
103 recipients: vec![pubkey.to_string()],
104 schema: BTreeMap::new(),
105 secrets: BTreeMap::new(),
106 meta: String::new(),
107 };
108
109 let mac_key_hex = crate::generate_mac_key();
110 let mac_key = crate::decode_mac_key(&mac_key_hex).unwrap();
111 let mac = crate::compute_mac(&vault, Some(&mac_key));
112 let meta = types::Meta {
113 recipients: recipient_names,
114 mac,
115 mac_key: Some(mac_key_hex),
116 github_pins: HashMap::new(),
117 };
118 let meta_json =
119 serde_json::to_vec(&meta).map_err(|e| MurkError::Secret(format!("meta serialize: {e}")))?;
120 vault.meta = encrypt_value(&meta_json, &[recipient])?;
121
122 Ok(vault)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::testutil::*;
129 use crate::testutil::{CWD_LOCK, ENV_LOCK};
130
131 #[test]
134 fn discover_existing_key_from_env() {
135 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
136 let (secret, pubkey) = generate_keypair();
137 unsafe { env::set_var("MURK_KEY", &secret) };
138 let result = discover_existing_key();
139 unsafe { env::remove_var("MURK_KEY") };
140
141 let dk = result.unwrap().unwrap();
142 assert_eq!(dk.secret_key, secret);
143 assert_eq!(dk.pubkey, pubkey);
144 }
145
146 #[test]
147 fn discover_existing_key_from_dotenv() {
148 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
149 let _cwd = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
150 unsafe { env::remove_var("MURK_KEY") };
151
152 let dir = std::env::temp_dir().join("murk_test_discover_dotenv");
154 std::fs::create_dir_all(&dir).unwrap();
155 let (secret, pubkey) = generate_keypair();
156 std::fs::write(dir.join(".env"), format!("MURK_KEY={secret}\n")).unwrap();
157
158 let orig_dir = std::env::current_dir().unwrap();
159 std::env::set_current_dir(&dir).unwrap();
160 let result = discover_existing_key();
161 std::env::set_current_dir(&orig_dir).unwrap();
162 std::fs::remove_dir_all(&dir).unwrap();
163
164 let dk = result.unwrap().unwrap();
165 assert_eq!(dk.secret_key, secret);
166 assert_eq!(dk.pubkey, pubkey);
167 }
168
169 #[test]
170 fn discover_existing_key_neither_set() {
171 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
172 let _cwd = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
173 unsafe { env::remove_var("MURK_KEY") };
174
175 let dir = std::env::temp_dir().join("murk_test_discover_none");
177 std::fs::create_dir_all(&dir).unwrap();
178 let orig_dir = std::env::current_dir().unwrap();
179 std::env::set_current_dir(&dir).unwrap();
180 let result = discover_existing_key();
181 std::env::set_current_dir(&orig_dir).unwrap();
182 std::fs::remove_dir_all(&dir).unwrap();
183
184 assert!(result.unwrap().is_none());
185 }
186
187 #[test]
188 fn discover_existing_key_invalid_key() {
189 let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
190 unsafe { env::set_var("MURK_KEY", "not-a-valid-age-key") };
191 let result = discover_existing_key();
192 unsafe { env::remove_var("MURK_KEY") };
193
194 assert!(result.is_err());
195 }
196
197 #[test]
200 fn check_init_status_authorized() {
201 let (secret, pubkey) = generate_keypair();
202 let recipient = make_recipient(&pubkey);
203
204 let mut names = HashMap::new();
206 names.insert(pubkey.clone(), "Alice".to_string());
207 let meta = types::Meta {
208 recipients: names,
209 mac: String::new(),
210 mac_key: None,
211 github_pins: HashMap::new(),
212 };
213 let meta_json = serde_json::to_vec(&meta).unwrap();
214 let meta_enc = encrypt_value(&meta_json, &[recipient]).unwrap();
215
216 let vault = types::Vault {
217 version: "2.0".into(),
218 created: "2026-01-01T00:00:00Z".into(),
219 vault_name: ".murk".into(),
220 repo: String::new(),
221 recipients: vec![pubkey.clone()],
222 schema: std::collections::BTreeMap::new(),
223 secrets: std::collections::BTreeMap::new(),
224 meta: meta_enc,
225 };
226
227 let status = check_init_status(&vault, &secret).unwrap();
228 assert!(status.authorized);
229 assert_eq!(status.pubkey, pubkey);
230 assert_eq!(status.display_name.as_deref(), Some("Alice"));
231 }
232
233 #[test]
234 fn check_init_status_not_authorized() {
235 let (secret, pubkey) = generate_keypair();
236 let (_, other_pubkey) = generate_keypair();
237
238 let vault = types::Vault {
239 version: "2.0".into(),
240 created: "2026-01-01T00:00:00Z".into(),
241 vault_name: ".murk".into(),
242 repo: String::new(),
243 recipients: vec![other_pubkey],
244 schema: std::collections::BTreeMap::new(),
245 secrets: std::collections::BTreeMap::new(),
246 meta: String::new(),
247 };
248
249 let status = check_init_status(&vault, &secret).unwrap();
250 assert!(!status.authorized);
251 assert_eq!(status.pubkey, pubkey);
252 assert!(status.display_name.is_none());
253 }
254
255 #[test]
256 fn create_vault_basic() {
257 let (_, pubkey) = generate_keypair();
258
259 let vault = create_vault(".murk", &pubkey, "Bob").unwrap();
260 assert_eq!(vault.version, types::VAULT_VERSION);
261 assert_eq!(vault.vault_name, ".murk");
262 assert_eq!(vault.recipients, vec![pubkey]);
263 assert!(vault.schema.is_empty());
264 assert!(vault.secrets.is_empty());
265 assert!(!vault.meta.is_empty());
266 }
267}