1use crate::{crypto, types};
4
5#[derive(Debug)]
7pub struct RecipientEntry {
8 pub pubkey: String,
9 pub display_name: Option<String>,
10 pub is_self: bool,
11}
12
13pub fn list_recipients(vault: &types::Vault, secret_key: Option<&str>) -> Vec<RecipientEntry> {
18 let meta_data = secret_key.filter(|k| !k.is_empty()).and_then(|sk| {
19 let identity = crypto::parse_identity(sk).ok()?;
20 let my_pubkey = identity.to_public().to_string();
21 let meta = crate::decrypt_meta(vault, &identity)?;
22 Some((meta, my_pubkey))
23 });
24
25 vault
26 .recipients
27 .iter()
28 .map(|pk| {
29 let (display_name, is_self) = match &meta_data {
30 Some((meta, my_pubkey)) => {
31 let name = meta.recipients.get(pk).filter(|n| !n.is_empty()).cloned();
32 (name, pk == my_pubkey)
33 }
34 None => (None, false),
35 };
36 RecipientEntry {
37 pubkey: pk.clone(),
38 display_name,
39 is_self,
40 }
41 })
42 .collect()
43}
44
45pub fn authorize_recipient(
47 vault: &mut types::Vault,
48 murk: &mut types::Murk,
49 pubkey: &str,
50 name: Option<&str>,
51) -> Result<(), String> {
52 if crypto::parse_recipient(pubkey).is_err() {
53 return Err(format!("invalid public key: {pubkey}"));
54 }
55
56 if vault.recipients.contains(&pubkey.to_string()) {
57 return Err(format!("{pubkey} is already a recipient"));
58 }
59
60 vault.recipients.push(pubkey.into());
61
62 if let Some(n) = name {
63 murk.recipients.insert(pubkey.into(), n.into());
64 }
65
66 Ok(())
67}
68
69#[derive(Debug)]
71pub struct RevokeResult {
72 pub display_name: Option<String>,
74 pub exposed_keys: Vec<String>,
76}
77
78pub fn revoke_recipient(
81 vault: &mut types::Vault,
82 murk: &mut types::Murk,
83 recipient: &str,
84) -> Result<RevokeResult, String> {
85 let pubkey = if vault.recipients.contains(&recipient.to_string()) {
87 recipient.to_string()
88 } else {
89 murk.recipients
90 .iter()
91 .find(|(_, name)| name.as_str() == recipient)
92 .map(|(pk, _)| pk.clone())
93 .ok_or_else(|| format!("recipient not found: {recipient}"))?
94 };
95
96 if vault.recipients.len() == 1 {
97 return Err(
98 "cannot revoke last recipient — vault would become permanently inaccessible".into(),
99 );
100 }
101
102 vault.recipients.retain(|pk| pk != &pubkey);
103
104 let display_name = murk.recipients.remove(&pubkey);
105
106 for scoped_map in murk.scoped.values_mut() {
108 scoped_map.remove(&pubkey);
109 }
110 for entry in vault.secrets.values_mut() {
111 entry.scoped.remove(&pubkey);
112 }
113
114 let exposed_keys = vault.schema.keys().cloned().collect();
115
116 Ok(RevokeResult {
117 display_name,
118 exposed_keys,
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::testutil::*;
126 use crate::types;
127 use std::collections::{BTreeMap, HashMap};
128
129 #[test]
130 fn authorize_recipient_success() {
131 let (_, pubkey) = generate_keypair();
132 let mut vault = empty_vault();
133 let mut murk = empty_murk();
134
135 let result = authorize_recipient(&mut vault, &mut murk, &pubkey, Some("alice"));
136 assert!(result.is_ok());
137 assert!(vault.recipients.contains(&pubkey));
138 assert_eq!(murk.recipients[&pubkey], "alice");
139 }
140
141 #[test]
142 fn authorize_recipient_no_name() {
143 let (_, pubkey) = generate_keypair();
144 let mut vault = empty_vault();
145 let mut murk = empty_murk();
146
147 authorize_recipient(&mut vault, &mut murk, &pubkey, None).unwrap();
148 assert!(vault.recipients.contains(&pubkey));
149 assert!(!murk.recipients.contains_key(&pubkey));
150 }
151
152 #[test]
153 fn authorize_recipient_duplicate_fails() {
154 let (_, pubkey) = generate_keypair();
155 let mut vault = empty_vault();
156 vault.recipients.push(pubkey.clone());
157 let mut murk = empty_murk();
158
159 let result = authorize_recipient(&mut vault, &mut murk, &pubkey, None);
160 assert!(result.is_err());
161 assert!(result.unwrap_err().contains("already a recipient"));
162 }
163
164 #[test]
165 fn authorize_recipient_invalid_key_fails() {
166 let mut vault = empty_vault();
167 let mut murk = empty_murk();
168
169 let result = authorize_recipient(&mut vault, &mut murk, "not-a-valid-key", None);
170 assert!(result.is_err());
171 assert!(result.unwrap_err().contains("invalid public key"));
172 }
173
174 #[test]
175 fn revoke_recipient_by_pubkey() {
176 let (_, pk1) = generate_keypair();
177 let (_, pk2) = generate_keypair();
178 let mut vault = empty_vault();
179 vault.recipients = vec![pk1.clone(), pk2.clone()];
180 vault.schema.insert(
181 "KEY".into(),
182 types::SchemaEntry {
183 description: String::new(),
184 example: None,
185 tags: vec![],
186 },
187 );
188 let mut murk = empty_murk();
189 murk.recipients.insert(pk2.clone(), "bob".into());
190
191 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
192 assert_eq!(result.display_name.as_deref(), Some("bob"));
193 assert!(!vault.recipients.contains(&pk2));
194 assert!(vault.recipients.contains(&pk1));
195 assert_eq!(result.exposed_keys, vec!["KEY"]);
196 }
197
198 #[test]
199 fn revoke_recipient_by_name() {
200 let (_, pk1) = generate_keypair();
201 let (_, pk2) = generate_keypair();
202 let mut vault = empty_vault();
203 vault.recipients = vec![pk1.clone(), pk2.clone()];
204 let mut murk = empty_murk();
205 murk.recipients.insert(pk2.clone(), "bob".into());
206
207 let result = revoke_recipient(&mut vault, &mut murk, "bob").unwrap();
208 assert_eq!(result.display_name.as_deref(), Some("bob"));
209 assert!(!vault.recipients.contains(&pk2));
210 }
211
212 #[test]
213 fn revoke_recipient_last_fails() {
214 let (_, pk) = generate_keypair();
215 let mut vault = empty_vault();
216 vault.recipients = vec![pk.clone()];
217 let mut murk = empty_murk();
218
219 let result = revoke_recipient(&mut vault, &mut murk, &pk);
220 assert!(result.is_err());
221 assert!(result.unwrap_err().contains("cannot revoke last recipient"));
222 }
223
224 #[test]
225 fn revoke_recipient_unknown_fails() {
226 let (_, pk) = generate_keypair();
227 let mut vault = empty_vault();
228 vault.recipients = vec![pk.clone()];
229 let mut murk = empty_murk();
230
231 let result = revoke_recipient(&mut vault, &mut murk, "nobody");
232 assert!(result.is_err());
233 assert!(result.unwrap_err().contains("recipient not found"));
234 }
235
236 #[test]
237 fn revoke_recipient_removes_scoped() {
238 let (_, pk1) = generate_keypair();
239 let (_, pk2) = generate_keypair();
240 let mut vault = empty_vault();
241 vault.recipients = vec![pk1.clone(), pk2.clone()];
242 vault.secrets.insert(
243 "KEY".into(),
244 types::SecretEntry {
245 shared: "ct".into(),
246 scoped: BTreeMap::from([(pk2.clone(), "scoped_ct".into())]),
247 },
248 );
249 let mut murk = empty_murk();
250 let mut scoped = HashMap::new();
251 scoped.insert(pk2.clone(), "scoped_val".into());
252 murk.scoped.insert("KEY".into(), scoped);
253
254 revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
255
256 assert!(vault.secrets["KEY"].scoped.is_empty());
257 assert!(murk.scoped["KEY"].is_empty());
258 }
259
260 #[test]
261 fn revoke_recipient_reports_exposed_keys() {
262 let (_, pk1) = generate_keypair();
263 let (_, pk2) = generate_keypair();
264 let mut vault = empty_vault();
265 vault.recipients = vec![pk1.clone(), pk2.clone()];
266 vault.schema.insert(
268 "DB_URL".into(),
269 types::SchemaEntry {
270 description: "db".into(),
271 example: None,
272 tags: vec![],
273 },
274 );
275 vault.schema.insert(
276 "API_KEY".into(),
277 types::SchemaEntry {
278 description: "api".into(),
279 example: None,
280 tags: vec![],
281 },
282 );
283 vault.secrets.insert(
284 "DB_URL".into(),
285 types::SecretEntry {
286 shared: "ct".into(),
287 scoped: BTreeMap::from([(pk2.clone(), "scoped_db".into())]),
288 },
289 );
290 vault.secrets.insert(
291 "API_KEY".into(),
292 types::SecretEntry {
293 shared: "ct2".into(),
294 scoped: BTreeMap::from([(pk2.clone(), "scoped_api".into())]),
295 },
296 );
297 let mut murk = empty_murk();
298 murk.scoped
299 .insert("DB_URL".into(), HashMap::from([(pk2.clone(), "v".into())]));
300 murk.scoped.insert(
301 "API_KEY".into(),
302 HashMap::from([(pk2.clone(), "v2".into())]),
303 );
304
305 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
306 let mut keys = result.exposed_keys.clone();
307 keys.sort();
308 assert_eq!(keys, vec!["API_KEY", "DB_URL"]);
309 assert!(vault.secrets["DB_URL"].scoped.is_empty());
310 assert!(vault.secrets["API_KEY"].scoped.is_empty());
311 }
312
313 #[test]
316 fn list_recipients_with_meta() {
317 let (secret, pubkey) = generate_keypair();
318 let (_, pk2) = generate_keypair();
319 let recipient = make_recipient(&pubkey);
320
321 let mut names = std::collections::HashMap::new();
322 names.insert(pubkey.clone(), "Alice".to_string());
323 names.insert(pk2.clone(), "Bob".to_string());
324 let meta = types::Meta {
325 recipients: names,
326 mac: String::new(),
327 };
328 let meta_json = serde_json::to_vec(&meta).unwrap();
329 let r2 = make_recipient(&pk2);
330 let meta_enc = crate::encrypt_value(&meta_json, &[recipient, r2]).unwrap();
331
332 let mut vault = empty_vault();
333 vault.recipients = vec![pubkey.clone(), pk2.clone()];
334 vault.meta = meta_enc;
335
336 let entries = list_recipients(&vault, Some(&secret));
337 assert_eq!(entries.len(), 2);
338 let me = entries.iter().find(|e| e.pubkey == pubkey).unwrap();
339 assert!(me.is_self);
340 assert_eq!(me.display_name.as_deref(), Some("Alice"));
341 let other = entries.iter().find(|e| e.pubkey == pk2).unwrap();
342 assert!(!other.is_self);
343 assert_eq!(other.display_name.as_deref(), Some("Bob"));
344 }
345
346 #[test]
347 fn list_recipients_without_key() {
348 let (_, pubkey) = generate_keypair();
349 let mut vault = empty_vault();
350 vault.recipients = vec![pubkey.clone()];
351
352 let entries = list_recipients(&vault, None);
353 assert_eq!(entries.len(), 1);
354 assert_eq!(entries[0].pubkey, pubkey);
355 assert!(entries[0].display_name.is_none());
356 assert!(!entries[0].is_self);
357 }
358
359 #[test]
360 fn list_recipients_wrong_key() {
361 let (_, pubkey) = generate_keypair();
362 let recipient = make_recipient(&pubkey);
363 let (wrong_secret, _) = generate_keypair();
364
365 let meta = types::Meta {
366 recipients: std::collections::HashMap::from([(pubkey.clone(), "Alice".into())]),
367 mac: String::new(),
368 };
369 let meta_json = serde_json::to_vec(&meta).unwrap();
370 let meta_enc = crate::encrypt_value(&meta_json, &[recipient]).unwrap();
371
372 let mut vault = empty_vault();
373 vault.recipients = vec![pubkey.clone()];
374 vault.meta = meta_enc;
375
376 let entries = list_recipients(&vault, Some(&wrong_secret));
377 assert_eq!(entries.len(), 1);
378 assert!(entries[0].display_name.is_none());
379 assert!(!entries[0].is_self);
380 }
381
382 #[test]
383 fn list_recipients_empty_vault() {
384 let vault = empty_vault();
385 let entries = list_recipients(&vault, None);
386 assert!(entries.is_empty());
387 }
388
389 #[test]
390 fn revoke_recipient_no_scoped() {
391 let (_, pk1) = generate_keypair();
392 let (_, pk2) = generate_keypair();
393 let mut vault = empty_vault();
394 vault.recipients = vec![pk1.clone(), pk2.clone()];
395 let mut murk = empty_murk();
396 murk.recipients.insert(pk2.clone(), "bob".into());
397
398 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
399 assert_eq!(result.display_name.as_deref(), Some("bob"));
400 assert!(!vault.recipients.contains(&pk2));
401 }
402}