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("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(vault_name: &str, pubkey: &str, name: &str) -> Result<types::Vault, String> {
77 let mut recipient_names = HashMap::new();
79 recipient_names.insert(pubkey.to_string(), name.to_string());
80
81 let recipient = crypto::parse_recipient(pubkey).map_err(|e| e.to_string())?;
82
83 let repo = Command::new("git")
85 .args(["remote", "get-url", "origin"])
86 .output()
87 .ok()
88 .filter(|o| o.status.success())
89 .and_then(|o| String::from_utf8(o.stdout).ok())
90 .map(|s| s.trim().to_string())
91 .unwrap_or_default();
92
93 let mut vault = types::Vault {
95 version: types::VAULT_VERSION.into(),
96 created: now_utc(),
97 vault_name: vault_name.into(),
98 repo,
99 recipients: vec![pubkey.to_string()],
100 schema: BTreeMap::new(),
101 secrets: BTreeMap::new(),
102 meta: String::new(),
103 };
104
105 let mac = crate::compute_mac(&vault);
106 let meta = types::Meta {
107 recipients: recipient_names,
108 mac,
109 };
110 let meta_json = serde_json::to_vec(&meta).map_err(|e| e.to_string())?;
111 vault.meta = encrypt_value(&meta_json, &[recipient])?;
112
113 Ok(vault)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::testutil::*;
120 use std::sync::Mutex;
121
122 static ENV_LOCK: Mutex<()> = Mutex::new(());
124
125 #[test]
128 fn discover_existing_key_from_env() {
129 let _lock = ENV_LOCK.lock().unwrap();
130 let (secret, pubkey) = generate_keypair();
131 unsafe { env::set_var("MURK_KEY", &secret) };
132 let result = discover_existing_key();
133 unsafe { env::remove_var("MURK_KEY") };
134
135 let dk = result.unwrap().unwrap();
136 assert_eq!(dk.secret_key, secret);
137 assert_eq!(dk.pubkey, pubkey);
138 }
139
140 #[test]
141 fn discover_existing_key_from_dotenv() {
142 let _lock = ENV_LOCK.lock().unwrap();
143 unsafe { env::remove_var("MURK_KEY") };
144
145 let dir = std::env::temp_dir().join("murk_test_discover_dotenv");
147 std::fs::create_dir_all(&dir).unwrap();
148 let (secret, pubkey) = generate_keypair();
149 std::fs::write(dir.join(".env"), format!("MURK_KEY={secret}\n")).unwrap();
150
151 let orig_dir = std::env::current_dir().unwrap();
152 std::env::set_current_dir(&dir).unwrap();
153 let result = discover_existing_key();
154 std::env::set_current_dir(&orig_dir).unwrap();
155 std::fs::remove_dir_all(&dir).unwrap();
156
157 let dk = result.unwrap().unwrap();
158 assert_eq!(dk.secret_key, secret);
159 assert_eq!(dk.pubkey, pubkey);
160 }
161
162 #[test]
163 fn discover_existing_key_neither_set() {
164 let _lock = ENV_LOCK.lock().unwrap();
165 unsafe { env::remove_var("MURK_KEY") };
166
167 let dir = std::env::temp_dir().join("murk_test_discover_none");
169 std::fs::create_dir_all(&dir).unwrap();
170 let orig_dir = std::env::current_dir().unwrap();
171 std::env::set_current_dir(&dir).unwrap();
172 let result = discover_existing_key();
173 std::env::set_current_dir(&orig_dir).unwrap();
174 std::fs::remove_dir_all(&dir).unwrap();
175
176 assert!(result.unwrap().is_none());
177 }
178
179 #[test]
180 fn discover_existing_key_invalid_key() {
181 let _lock = ENV_LOCK.lock().unwrap();
182 unsafe { env::set_var("MURK_KEY", "not-a-valid-age-key") };
183 let result = discover_existing_key();
184 unsafe { env::remove_var("MURK_KEY") };
185
186 assert!(result.is_err());
187 }
188
189 #[test]
192 fn check_init_status_authorized() {
193 let (secret, pubkey) = generate_keypair();
194 let recipient = make_recipient(&pubkey);
195
196 let mut names = HashMap::new();
198 names.insert(pubkey.clone(), "Alice".to_string());
199 let meta = types::Meta {
200 recipients: names,
201 mac: String::new(),
202 };
203 let meta_json = serde_json::to_vec(&meta).unwrap();
204 let meta_enc = encrypt_value(&meta_json, &[recipient]).unwrap();
205
206 let vault = types::Vault {
207 version: "2.0".into(),
208 created: "2026-01-01T00:00:00Z".into(),
209 vault_name: ".murk".into(),
210 repo: String::new(),
211 recipients: vec![pubkey.clone()],
212 schema: std::collections::BTreeMap::new(),
213 secrets: std::collections::BTreeMap::new(),
214 meta: meta_enc,
215 };
216
217 let status = check_init_status(&vault, &secret).unwrap();
218 assert!(status.authorized);
219 assert_eq!(status.pubkey, pubkey);
220 assert_eq!(status.display_name.as_deref(), Some("Alice"));
221 }
222
223 #[test]
224 fn check_init_status_not_authorized() {
225 let (secret, pubkey) = generate_keypair();
226 let (_, other_pubkey) = generate_keypair();
227
228 let vault = types::Vault {
229 version: "2.0".into(),
230 created: "2026-01-01T00:00:00Z".into(),
231 vault_name: ".murk".into(),
232 repo: String::new(),
233 recipients: vec![other_pubkey],
234 schema: std::collections::BTreeMap::new(),
235 secrets: std::collections::BTreeMap::new(),
236 meta: String::new(),
237 };
238
239 let status = check_init_status(&vault, &secret).unwrap();
240 assert!(!status.authorized);
241 assert_eq!(status.pubkey, pubkey);
242 assert!(status.display_name.is_none());
243 }
244
245 #[test]
246 fn create_vault_basic() {
247 let (_, pubkey) = generate_keypair();
248
249 let vault = create_vault(".murk", &pubkey, "Bob").unwrap();
250 assert_eq!(vault.version, types::VAULT_VERSION);
251 assert_eq!(vault.vault_name, ".murk");
252 assert_eq!(vault.recipients, vec![pubkey]);
253 assert!(vault.schema.is_empty());
254 assert!(vault.secrets.is_empty());
255 assert!(!vault.meta.is_empty());
256 }
257}