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.pubkey_string().ok()?;
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(
84 vault: &mut types::Vault,
85 murk: &mut types::Murk,
86 recipient: &str,
87) -> Result<RevokeResult, String> {
88 let pubkeys: Vec<String> = if vault.recipients.contains(&recipient.to_string()) {
90 vec![recipient.to_string()]
92 } else {
93 let matched: Vec<String> = murk
95 .recipients
96 .iter()
97 .filter(|(_, name)| name.as_str() == recipient)
98 .map(|(pk, _)| pk.clone())
99 .collect();
100 if matched.is_empty() {
101 return Err(format!("recipient not found: {recipient}"));
102 }
103 matched
104 };
105
106 if vault.recipients.len() <= pubkeys.len() {
107 return Err(
108 "cannot revoke last recipient — vault would become permanently inaccessible".into(),
109 );
110 }
111
112 let mut display_name = None;
113 for pubkey in &pubkeys {
114 vault.recipients.retain(|pk| pk != pubkey);
115
116 if let Some(name) = murk.recipients.remove(pubkey) {
117 display_name = Some(name);
118 }
119
120 for scoped_map in murk.scoped.values_mut() {
122 scoped_map.remove(pubkey);
123 }
124 for entry in vault.secrets.values_mut() {
125 entry.scoped.remove(pubkey);
126 }
127 }
128
129 let exposed_keys = vault.schema.keys().cloned().collect();
130
131 Ok(RevokeResult {
132 display_name,
133 exposed_keys,
134 })
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::testutil::*;
141 use crate::types;
142 use std::collections::{BTreeMap, HashMap};
143
144 #[test]
145 fn authorize_recipient_success() {
146 let (_, pubkey) = generate_keypair();
147 let mut vault = empty_vault();
148 let mut murk = empty_murk();
149
150 let result = authorize_recipient(&mut vault, &mut murk, &pubkey, Some("alice"));
151 assert!(result.is_ok());
152 assert!(vault.recipients.contains(&pubkey));
153 assert_eq!(murk.recipients[&pubkey], "alice");
154 }
155
156 #[test]
157 fn authorize_recipient_no_name() {
158 let (_, pubkey) = generate_keypair();
159 let mut vault = empty_vault();
160 let mut murk = empty_murk();
161
162 authorize_recipient(&mut vault, &mut murk, &pubkey, None).unwrap();
163 assert!(vault.recipients.contains(&pubkey));
164 assert!(!murk.recipients.contains_key(&pubkey));
165 }
166
167 #[test]
168 fn authorize_recipient_duplicate_fails() {
169 let (_, pubkey) = generate_keypair();
170 let mut vault = empty_vault();
171 vault.recipients.push(pubkey.clone());
172 let mut murk = empty_murk();
173
174 let result = authorize_recipient(&mut vault, &mut murk, &pubkey, None);
175 assert!(result.is_err());
176 assert!(result.unwrap_err().contains("already a recipient"));
177 }
178
179 #[test]
180 fn authorize_recipient_invalid_key_fails() {
181 let mut vault = empty_vault();
182 let mut murk = empty_murk();
183
184 let result = authorize_recipient(&mut vault, &mut murk, "not-a-valid-key", None);
185 assert!(result.is_err());
186 assert!(result.unwrap_err().contains("invalid public key"));
187 }
188
189 #[test]
190 fn revoke_recipient_by_pubkey() {
191 let (_, pk1) = generate_keypair();
192 let (_, pk2) = generate_keypair();
193 let mut vault = empty_vault();
194 vault.recipients = vec![pk1.clone(), pk2.clone()];
195 vault.schema.insert(
196 "KEY".into(),
197 types::SchemaEntry {
198 description: String::new(),
199 example: None,
200 tags: vec![],
201 },
202 );
203 let mut murk = empty_murk();
204 murk.recipients.insert(pk2.clone(), "bob".into());
205
206 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
207 assert_eq!(result.display_name.as_deref(), Some("bob"));
208 assert!(!vault.recipients.contains(&pk2));
209 assert!(vault.recipients.contains(&pk1));
210 assert_eq!(result.exposed_keys, vec!["KEY"]);
211 }
212
213 #[test]
214 fn revoke_recipient_by_name() {
215 let (_, pk1) = generate_keypair();
216 let (_, pk2) = generate_keypair();
217 let mut vault = empty_vault();
218 vault.recipients = vec![pk1.clone(), pk2.clone()];
219 let mut murk = empty_murk();
220 murk.recipients.insert(pk2.clone(), "bob".into());
221
222 let result = revoke_recipient(&mut vault, &mut murk, "bob").unwrap();
223 assert_eq!(result.display_name.as_deref(), Some("bob"));
224 assert!(!vault.recipients.contains(&pk2));
225 }
226
227 #[test]
228 fn revoke_recipient_last_fails() {
229 let (_, pk) = generate_keypair();
230 let mut vault = empty_vault();
231 vault.recipients = vec![pk.clone()];
232 let mut murk = empty_murk();
233
234 let result = revoke_recipient(&mut vault, &mut murk, &pk);
235 assert!(result.is_err());
236 assert!(result.unwrap_err().contains("cannot revoke last recipient"));
237 }
238
239 #[test]
240 fn revoke_recipient_unknown_fails() {
241 let (_, pk) = generate_keypair();
242 let mut vault = empty_vault();
243 vault.recipients = vec![pk.clone()];
244 let mut murk = empty_murk();
245
246 let result = revoke_recipient(&mut vault, &mut murk, "nobody");
247 assert!(result.is_err());
248 assert!(result.unwrap_err().contains("recipient not found"));
249 }
250
251 #[test]
252 fn revoke_recipient_removes_scoped() {
253 let (_, pk1) = generate_keypair();
254 let (_, pk2) = generate_keypair();
255 let mut vault = empty_vault();
256 vault.recipients = vec![pk1.clone(), pk2.clone()];
257 vault.secrets.insert(
258 "KEY".into(),
259 types::SecretEntry {
260 shared: "ct".into(),
261 scoped: BTreeMap::from([(pk2.clone(), "scoped_ct".into())]),
262 },
263 );
264 let mut murk = empty_murk();
265 let mut scoped = HashMap::new();
266 scoped.insert(pk2.clone(), "scoped_val".into());
267 murk.scoped.insert("KEY".into(), scoped);
268
269 revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
270
271 assert!(vault.secrets["KEY"].scoped.is_empty());
272 assert!(murk.scoped["KEY"].is_empty());
273 }
274
275 #[test]
276 fn revoke_recipient_reports_exposed_keys() {
277 let (_, pk1) = generate_keypair();
278 let (_, pk2) = generate_keypair();
279 let mut vault = empty_vault();
280 vault.recipients = vec![pk1.clone(), pk2.clone()];
281 vault.schema.insert(
283 "DB_URL".into(),
284 types::SchemaEntry {
285 description: "db".into(),
286 example: None,
287 tags: vec![],
288 },
289 );
290 vault.schema.insert(
291 "API_KEY".into(),
292 types::SchemaEntry {
293 description: "api".into(),
294 example: None,
295 tags: vec![],
296 },
297 );
298 vault.secrets.insert(
299 "DB_URL".into(),
300 types::SecretEntry {
301 shared: "ct".into(),
302 scoped: BTreeMap::from([(pk2.clone(), "scoped_db".into())]),
303 },
304 );
305 vault.secrets.insert(
306 "API_KEY".into(),
307 types::SecretEntry {
308 shared: "ct2".into(),
309 scoped: BTreeMap::from([(pk2.clone(), "scoped_api".into())]),
310 },
311 );
312 let mut murk = empty_murk();
313 murk.scoped
314 .insert("DB_URL".into(), HashMap::from([(pk2.clone(), "v".into())]));
315 murk.scoped.insert(
316 "API_KEY".into(),
317 HashMap::from([(pk2.clone(), "v2".into())]),
318 );
319
320 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
321 let mut keys = result.exposed_keys.clone();
322 keys.sort();
323 assert_eq!(keys, vec!["API_KEY", "DB_URL"]);
324 assert!(vault.secrets["DB_URL"].scoped.is_empty());
325 assert!(vault.secrets["API_KEY"].scoped.is_empty());
326 }
327
328 #[test]
331 fn list_recipients_with_meta() {
332 let (secret, pubkey) = generate_keypair();
333 let (_, pk2) = generate_keypair();
334 let recipient = make_recipient(&pubkey);
335
336 let mut names = std::collections::HashMap::new();
337 names.insert(pubkey.clone(), "Alice".to_string());
338 names.insert(pk2.clone(), "Bob".to_string());
339 let meta = types::Meta {
340 recipients: names,
341 mac: String::new(),
342 };
343 let meta_json = serde_json::to_vec(&meta).unwrap();
344 let r2 = make_recipient(&pk2);
345 let meta_enc = crate::encrypt_value(&meta_json, &[recipient, r2]).unwrap();
346
347 let mut vault = empty_vault();
348 vault.recipients = vec![pubkey.clone(), pk2.clone()];
349 vault.meta = meta_enc;
350
351 let entries = list_recipients(&vault, Some(&secret));
352 assert_eq!(entries.len(), 2);
353 let me = entries.iter().find(|e| e.pubkey == pubkey).unwrap();
354 assert!(me.is_self);
355 assert_eq!(me.display_name.as_deref(), Some("Alice"));
356 let other = entries.iter().find(|e| e.pubkey == pk2).unwrap();
357 assert!(!other.is_self);
358 assert_eq!(other.display_name.as_deref(), Some("Bob"));
359 }
360
361 #[test]
362 fn list_recipients_without_key() {
363 let (_, pubkey) = generate_keypair();
364 let mut vault = empty_vault();
365 vault.recipients = vec![pubkey.clone()];
366
367 let entries = list_recipients(&vault, None);
368 assert_eq!(entries.len(), 1);
369 assert_eq!(entries[0].pubkey, pubkey);
370 assert!(entries[0].display_name.is_none());
371 assert!(!entries[0].is_self);
372 }
373
374 #[test]
375 fn list_recipients_wrong_key() {
376 let (_, pubkey) = generate_keypair();
377 let recipient = make_recipient(&pubkey);
378 let (wrong_secret, _) = generate_keypair();
379
380 let meta = types::Meta {
381 recipients: std::collections::HashMap::from([(pubkey.clone(), "Alice".into())]),
382 mac: String::new(),
383 };
384 let meta_json = serde_json::to_vec(&meta).unwrap();
385 let meta_enc = crate::encrypt_value(&meta_json, &[recipient]).unwrap();
386
387 let mut vault = empty_vault();
388 vault.recipients = vec![pubkey.clone()];
389 vault.meta = meta_enc;
390
391 let entries = list_recipients(&vault, Some(&wrong_secret));
392 assert_eq!(entries.len(), 1);
393 assert!(entries[0].display_name.is_none());
394 assert!(!entries[0].is_self);
395 }
396
397 #[test]
398 fn list_recipients_empty_vault() {
399 let vault = empty_vault();
400 let entries = list_recipients(&vault, None);
401 assert!(entries.is_empty());
402 }
403
404 #[test]
405 fn revoke_recipient_no_scoped() {
406 let (_, pk1) = generate_keypair();
407 let (_, pk2) = generate_keypair();
408 let mut vault = empty_vault();
409 vault.recipients = vec![pk1.clone(), pk2.clone()];
410 let mut murk = empty_murk();
411 murk.recipients.insert(pk2.clone(), "bob".into());
412
413 let result = revoke_recipient(&mut vault, &mut murk, &pk2).unwrap();
414 assert_eq!(result.display_name.as_deref(), Some("bob"));
415 assert!(!vault.recipients.contains(&pk2));
416 }
417
418 #[test]
419 fn revoke_by_name_removes_all_matching_keys() {
420 let (_, pk_owner) = generate_keypair();
421 let (_, pk_ssh1) = generate_keypair();
422 let (_, pk_ssh2) = generate_keypair();
423 let mut vault = empty_vault();
424 vault.recipients = vec![pk_owner.clone(), pk_ssh1.clone(), pk_ssh2.clone()];
425 let mut murk = empty_murk();
426 murk.recipients
427 .insert(pk_ssh1.clone(), "alice@github".into());
428 murk.recipients
429 .insert(pk_ssh2.clone(), "alice@github".into());
430
431 let result = revoke_recipient(&mut vault, &mut murk, "alice@github").unwrap();
432 assert_eq!(result.display_name.as_deref(), Some("alice@github"));
433 assert!(!vault.recipients.contains(&pk_ssh1));
434 assert!(!vault.recipients.contains(&pk_ssh2));
435 assert!(vault.recipients.contains(&pk_owner));
436 }
437
438 #[test]
439 fn revoke_all_matching_blocked_if_last() {
440 let (_, pk_ssh1) = generate_keypair();
441 let (_, pk_ssh2) = generate_keypair();
442 let mut vault = empty_vault();
443 vault.recipients = vec![pk_ssh1.clone(), pk_ssh2.clone()];
444 let mut murk = empty_murk();
445 murk.recipients
446 .insert(pk_ssh1.clone(), "alice@github".into());
447 murk.recipients
448 .insert(pk_ssh2.clone(), "alice@github".into());
449
450 let result = revoke_recipient(&mut vault, &mut murk, "alice@github");
451 assert!(result.is_err());
452 assert!(result.unwrap_err().contains("cannot revoke last recipient"));
453 }
454}