1use std::collections::{BTreeMap, HashMap};
4
5use crate::types;
6
7pub fn resolve_secrets(
10 vault: &types::Vault,
11 murk: &types::Murk,
12 pubkey: &str,
13 tags: &[String],
14) -> BTreeMap<String, String> {
15 let mut values = murk.values.clone();
16
17 for (key, scoped_map) in &murk.scoped {
19 if let Some(value) = scoped_map.get(pubkey) {
20 values.insert(key.clone(), value.clone());
21 }
22 }
23
24 let allowed_keys: Option<std::collections::HashSet<&str>> = if tags.is_empty() {
26 None
27 } else {
28 Some(
29 vault
30 .schema
31 .iter()
32 .filter(|(_, e)| e.tags.iter().any(|t| tags.contains(t)))
33 .map(|(k, _)| k.as_str())
34 .collect(),
35 )
36 };
37
38 let mut result = BTreeMap::new();
39 for (k, v) in &values {
40 if allowed_keys
41 .as_ref()
42 .is_some_and(|a| !a.contains(k.as_str()))
43 {
44 continue;
45 }
46 result.insert(k.clone(), v.clone());
47 }
48 result
49}
50
51pub fn export_secrets(
54 vault: &types::Vault,
55 murk: &types::Murk,
56 pubkey: &str,
57 tags: &[String],
58) -> BTreeMap<String, String> {
59 resolve_secrets(vault, murk, pubkey, tags)
60 .into_iter()
61 .map(|(k, v)| (k, v.replace('\'', "'\\''")))
62 .collect()
63}
64
65pub fn decrypt_vault_values(
70 vault: &types::Vault,
71 identity: &crate::crypto::MurkIdentity,
72) -> HashMap<String, String> {
73 let mut values = HashMap::new();
74 for (key, entry) in &vault.secrets {
75 if let Ok(value) = crate::decrypt_value(&entry.shared, identity)
76 .and_then(|pt| String::from_utf8(pt).map_err(|e| e.to_string()))
77 {
78 values.insert(key.clone(), value);
79 }
80 }
81 values
82}
83
84pub fn parse_and_decrypt_values(
89 vault_contents: &str,
90 identity: &crate::crypto::MurkIdentity,
91) -> Result<HashMap<String, String>, String> {
92 let vault = crate::vault::parse(vault_contents).map_err(|e| e.to_string())?;
93 Ok(decrypt_vault_values(&vault, identity))
94}
95
96#[derive(Debug, PartialEq, Eq)]
98pub enum DiffKind {
99 Added,
100 Removed,
101 Changed,
102}
103
104#[derive(Debug)]
106pub struct DiffEntry {
107 pub key: String,
108 pub kind: DiffKind,
109 pub old_value: Option<String>,
110 pub new_value: Option<String>,
111}
112
113pub fn diff_secrets(
115 old: &HashMap<String, String>,
116 new: &HashMap<String, String>,
117) -> Vec<DiffEntry> {
118 let mut all_keys: Vec<&str> = old
119 .keys()
120 .chain(new.keys())
121 .map(String::as_str)
122 .collect::<std::collections::HashSet<_>>()
123 .into_iter()
124 .collect();
125 all_keys.sort_unstable();
126
127 let mut entries = Vec::new();
128 for key in all_keys {
129 match (old.get(key), new.get(key)) {
130 (None, Some(v)) => entries.push(DiffEntry {
131 key: key.into(),
132 kind: DiffKind::Added,
133 old_value: None,
134 new_value: Some(v.clone()),
135 }),
136 (Some(v), None) => entries.push(DiffEntry {
137 key: key.into(),
138 kind: DiffKind::Removed,
139 old_value: Some(v.clone()),
140 new_value: None,
141 }),
142 (Some(old_v), Some(new_v)) if old_v != new_v => entries.push(DiffEntry {
143 key: key.into(),
144 kind: DiffKind::Changed,
145 old_value: Some(old_v.clone()),
146 new_value: Some(new_v.clone()),
147 }),
148 _ => {}
149 }
150 }
151 entries
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::testutil::*;
158 use crate::types;
159
160 #[test]
161 fn export_secrets_basic() {
162 let mut vault = empty_vault();
163 vault.schema.insert(
164 "FOO".into(),
165 types::SchemaEntry {
166 description: String::new(),
167 example: None,
168 tags: vec![],
169 },
170 );
171
172 let mut murk = empty_murk();
173 murk.values.insert("FOO".into(), "bar".into());
174
175 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
176 assert_eq!(exports.len(), 1);
177 assert_eq!(exports["FOO"], "bar");
178 }
179
180 #[test]
181 fn export_secrets_scoped_override() {
182 let mut vault = empty_vault();
183 vault.schema.insert(
184 "KEY".into(),
185 types::SchemaEntry {
186 description: String::new(),
187 example: None,
188 tags: vec![],
189 },
190 );
191
192 let mut murk = empty_murk();
193 murk.values.insert("KEY".into(), "shared".into());
194 let mut scoped = HashMap::new();
195 scoped.insert("age1pk".into(), "override".into());
196 murk.scoped.insert("KEY".into(), scoped);
197
198 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
199 assert_eq!(exports["KEY"], "override");
200 }
201
202 #[test]
203 fn export_secrets_tag_filter() {
204 let mut vault = empty_vault();
205 vault.schema.insert(
206 "A".into(),
207 types::SchemaEntry {
208 description: String::new(),
209 example: None,
210 tags: vec!["db".into()],
211 },
212 );
213 vault.schema.insert(
214 "B".into(),
215 types::SchemaEntry {
216 description: String::new(),
217 example: None,
218 tags: vec!["api".into()],
219 },
220 );
221
222 let mut murk = empty_murk();
223 murk.values.insert("A".into(), "val_a".into());
224 murk.values.insert("B".into(), "val_b".into());
225
226 let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
227 assert_eq!(exports.len(), 1);
228 assert_eq!(exports["A"], "val_a");
229 }
230
231 #[test]
232 fn export_secrets_shell_escaping() {
233 let mut vault = empty_vault();
234 vault.schema.insert(
235 "KEY".into(),
236 types::SchemaEntry {
237 description: String::new(),
238 example: None,
239 tags: vec![],
240 },
241 );
242
243 let mut murk = empty_murk();
244 murk.values.insert("KEY".into(), "it's a test".into());
245
246 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
247 assert_eq!(exports["KEY"], "it'\\''s a test");
248 }
249
250 #[test]
251 fn diff_secrets_no_changes() {
252 let old = HashMap::from([("K".into(), "V".into())]);
253 let new = old.clone();
254 assert!(diff_secrets(&old, &new).is_empty());
255 }
256
257 #[test]
258 fn diff_secrets_added() {
259 let old = HashMap::new();
260 let new = HashMap::from([("KEY".into(), "val".into())]);
261 let entries = diff_secrets(&old, &new);
262 assert_eq!(entries.len(), 1);
263 assert_eq!(entries[0].kind, DiffKind::Added);
264 assert_eq!(entries[0].key, "KEY");
265 assert_eq!(entries[0].new_value.as_deref(), Some("val"));
266 }
267
268 #[test]
269 fn diff_secrets_removed() {
270 let old = HashMap::from([("KEY".into(), "val".into())]);
271 let new = HashMap::new();
272 let entries = diff_secrets(&old, &new);
273 assert_eq!(entries.len(), 1);
274 assert_eq!(entries[0].kind, DiffKind::Removed);
275 assert_eq!(entries[0].old_value.as_deref(), Some("val"));
276 }
277
278 #[test]
279 fn diff_secrets_changed() {
280 let old = HashMap::from([("KEY".into(), "old_val".into())]);
281 let new = HashMap::from([("KEY".into(), "new_val".into())]);
282 let entries = diff_secrets(&old, &new);
283 assert_eq!(entries.len(), 1);
284 assert_eq!(entries[0].kind, DiffKind::Changed);
285 assert_eq!(entries[0].old_value.as_deref(), Some("old_val"));
286 assert_eq!(entries[0].new_value.as_deref(), Some("new_val"));
287 }
288
289 #[test]
290 fn diff_secrets_mixed() {
291 let old = HashMap::from([
292 ("KEEP".into(), "same".into()),
293 ("REMOVE".into(), "gone".into()),
294 ("CHANGE".into(), "old".into()),
295 ]);
296 let new = HashMap::from([
297 ("KEEP".into(), "same".into()),
298 ("ADD".into(), "new".into()),
299 ("CHANGE".into(), "new".into()),
300 ]);
301 let entries = diff_secrets(&old, &new);
302 assert_eq!(entries.len(), 3);
303
304 let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
305 assert!(kinds.contains(&&DiffKind::Added));
306 assert!(kinds.contains(&&DiffKind::Removed));
307 assert!(kinds.contains(&&DiffKind::Changed));
308 }
309
310 #[test]
311 fn diff_secrets_sorted_by_key() {
312 let old = HashMap::new();
313 let new = HashMap::from([
314 ("Z".into(), "z".into()),
315 ("A".into(), "a".into()),
316 ("M".into(), "m".into()),
317 ]);
318 let entries = diff_secrets(&old, &new);
319 let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
320 assert_eq!(keys, vec!["A", "M", "Z"]);
321 }
322
323 #[test]
326 fn resolve_secrets_basic() {
327 let mut vault = empty_vault();
328 vault.schema.insert(
329 "FOO".into(),
330 types::SchemaEntry {
331 description: String::new(),
332 example: None,
333 tags: vec![],
334 },
335 );
336
337 let mut murk = empty_murk();
338 murk.values.insert("FOO".into(), "bar".into());
339
340 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
341 assert_eq!(resolved.len(), 1);
342 assert_eq!(resolved["FOO"], "bar");
343 }
344
345 #[test]
346 fn resolve_secrets_no_escaping() {
347 let mut vault = empty_vault();
348 vault.schema.insert(
349 "KEY".into(),
350 types::SchemaEntry {
351 description: String::new(),
352 example: None,
353 tags: vec![],
354 },
355 );
356
357 let mut murk = empty_murk();
358 murk.values.insert("KEY".into(), "it's a test".into());
359
360 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
361 assert_eq!(resolved["KEY"], "it's a test");
362 }
363
364 #[test]
365 fn resolve_secrets_scoped_override() {
366 let mut vault = empty_vault();
367 vault.schema.insert(
368 "KEY".into(),
369 types::SchemaEntry {
370 description: String::new(),
371 example: None,
372 tags: vec![],
373 },
374 );
375
376 let mut murk = empty_murk();
377 murk.values.insert("KEY".into(), "shared".into());
378 let mut scoped = HashMap::new();
379 scoped.insert("age1pk".into(), "override".into());
380 murk.scoped.insert("KEY".into(), scoped);
381
382 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
383 assert_eq!(resolved["KEY"], "override");
384 }
385
386 #[test]
387 fn resolve_secrets_tag_filter() {
388 let mut vault = empty_vault();
389 vault.schema.insert(
390 "A".into(),
391 types::SchemaEntry {
392 description: String::new(),
393 example: None,
394 tags: vec!["db".into()],
395 },
396 );
397 vault.schema.insert(
398 "B".into(),
399 types::SchemaEntry {
400 description: String::new(),
401 example: None,
402 tags: vec!["api".into()],
403 },
404 );
405
406 let mut murk = empty_murk();
407 murk.values.insert("A".into(), "val_a".into());
408 murk.values.insert("B".into(), "val_b".into());
409
410 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
411 assert_eq!(resolved.len(), 1);
412 assert_eq!(resolved["A"], "val_a");
413 }
414
415 #[test]
416 fn resolve_secrets_tag_in_schema_but_no_secret() {
417 let mut vault = empty_vault();
418 vault.schema.insert(
420 "ORPHAN".into(),
421 types::SchemaEntry {
422 description: "orphan key".into(),
423 example: None,
424 tags: vec!["db".into()],
425 },
426 );
427 vault.schema.insert(
428 "REAL".into(),
429 types::SchemaEntry {
430 description: "has a value".into(),
431 example: None,
432 tags: vec!["db".into()],
433 },
434 );
435
436 let mut murk = empty_murk();
437 murk.values.insert("REAL".into(), "real_val".into());
439
440 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
441 assert_eq!(resolved.len(), 1);
443 assert_eq!(resolved["REAL"], "real_val");
444 assert!(!resolved.contains_key("ORPHAN"));
445 }
446
447 #[test]
448 fn resolve_secrets_scoped_pubkey_not_in_recipients() {
449 let mut vault = empty_vault();
450 vault.recipients = vec!["age1alice".into()];
451 vault.schema.insert(
452 "KEY".into(),
453 types::SchemaEntry {
454 description: String::new(),
455 example: None,
456 tags: vec![],
457 },
458 );
459
460 let mut murk = empty_murk();
461 murk.values.insert("KEY".into(), "shared".into());
462 let mut scoped = HashMap::new();
464 scoped.insert("age1outsider".into(), "outsider_val".into());
465 murk.scoped.insert("KEY".into(), scoped);
466
467 let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
469 assert_eq!(resolved["KEY"], "outsider_val");
470
471 let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
473 assert_eq!(resolved_alice["KEY"], "shared");
474 }
475
476 #[test]
479 fn export_secrets_empty_vault() {
480 let vault = empty_vault();
481 let murk = empty_murk();
482 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
483 assert!(exports.is_empty());
484 }
485
486 #[test]
487 fn decrypt_vault_values_basic() {
488 let (secret, pubkey) = generate_keypair();
489 let recipient = make_recipient(&pubkey);
490 let identity = make_identity(&secret);
491
492 let mut vault = empty_vault();
493 vault.recipients = vec![pubkey];
494 vault.secrets.insert(
495 "KEY1".into(),
496 types::SecretEntry {
497 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
498 scoped: std::collections::BTreeMap::new(),
499 },
500 );
501 vault.secrets.insert(
502 "KEY2".into(),
503 types::SecretEntry {
504 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
505 scoped: std::collections::BTreeMap::new(),
506 },
507 );
508
509 let values = crate::export::decrypt_vault_values(&vault, &identity);
510 assert_eq!(values.len(), 2);
511 assert_eq!(values["KEY1"], "val1");
512 assert_eq!(values["KEY2"], "val2");
513 }
514
515 #[test]
516 fn decrypt_vault_values_wrong_key_skips() {
517 let (_, pubkey) = generate_keypair();
518 let recipient = make_recipient(&pubkey);
519 let (wrong_secret, _) = generate_keypair();
520 let wrong_identity = make_identity(&wrong_secret);
521
522 let mut vault = empty_vault();
523 vault.recipients = vec![pubkey];
524 vault.secrets.insert(
525 "KEY1".into(),
526 types::SecretEntry {
527 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
528 scoped: std::collections::BTreeMap::new(),
529 },
530 );
531
532 let values = crate::export::decrypt_vault_values(&vault, &wrong_identity);
533 assert!(values.is_empty());
534 }
535
536 #[test]
537 fn decrypt_vault_values_empty_vault() {
538 let (secret, _) = generate_keypair();
539 let identity = make_identity(&secret);
540 let vault = empty_vault();
541
542 let values = crate::export::decrypt_vault_values(&vault, &identity);
543 assert!(values.is_empty());
544 }
545
546 #[test]
547 fn diff_secrets_both_empty() {
548 let old = HashMap::new();
549 let new = HashMap::new();
550 assert!(diff_secrets(&old, &new).is_empty());
551 }
552
553 #[test]
556 fn parse_and_decrypt_values_roundtrip() {
557 let (secret, pubkey) = generate_keypair();
558 let recipient = make_recipient(&pubkey);
559 let identity = make_identity(&secret);
560
561 let mut vault = empty_vault();
562 vault.recipients = vec![pubkey];
563 vault.secrets.insert(
564 "KEY1".into(),
565 types::SecretEntry {
566 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
567 scoped: std::collections::BTreeMap::new(),
568 },
569 );
570 vault.secrets.insert(
571 "KEY2".into(),
572 types::SecretEntry {
573 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
574 scoped: std::collections::BTreeMap::new(),
575 },
576 );
577
578 let json = serde_json::to_string(&vault).unwrap();
579 let values = parse_and_decrypt_values(&json, &identity).unwrap();
580 assert_eq!(values.len(), 2);
581 assert_eq!(values["KEY1"], "val1");
582 assert_eq!(values["KEY2"], "val2");
583 }
584
585 #[test]
586 fn parse_and_decrypt_values_invalid_json() {
587 let (secret, _) = generate_keypair();
588 let identity = make_identity(&secret);
589
590 let result = parse_and_decrypt_values("not valid json", &identity);
591 assert!(result.is_err());
592 }
593
594 #[test]
595 fn parse_and_decrypt_values_wrong_key() {
596 let (_, pubkey) = generate_keypair();
597 let recipient = make_recipient(&pubkey);
598 let (wrong_secret, _) = generate_keypair();
599 let wrong_identity = make_identity(&wrong_secret);
600
601 let mut vault = empty_vault();
602 vault.recipients = vec![pubkey];
603 vault.secrets.insert(
604 "KEY1".into(),
605 types::SecretEntry {
606 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
607 scoped: std::collections::BTreeMap::new(),
608 },
609 );
610
611 let json = serde_json::to_string(&vault).unwrap();
612 let values = parse_and_decrypt_values(&json, &wrong_identity).unwrap();
613 assert!(values.is_empty());
614 }
615}