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 pubkey = identity.pubkey_string().unwrap_or_default();
74 let mut values = HashMap::new();
75 for (key, entry) in &vault.secrets {
76 if !entry.shared.is_empty()
78 && let Ok(value) = crate::decrypt_value(&entry.shared, identity).and_then(|pt| {
79 String::from_utf8(pt).map_err(|e| crate::error::MurkError::Secret(e.to_string()))
80 })
81 {
82 values.insert(key.clone(), value);
83 }
84 if let Some(encoded) = entry.scoped.get(&pubkey)
86 && let Ok(value) = crate::decrypt_value(encoded, identity).and_then(|pt| {
87 String::from_utf8(pt).map_err(|e| crate::error::MurkError::Secret(e.to_string()))
88 })
89 {
90 values.insert(key.clone(), value);
91 }
92 }
93 values
94}
95
96pub fn parse_and_decrypt_values(
101 vault_contents: &str,
102 identity: &crate::crypto::MurkIdentity,
103) -> Result<HashMap<String, String>, String> {
104 let vault = crate::vault::parse(vault_contents).map_err(|e| e.to_string())?;
105 Ok(decrypt_vault_values(&vault, identity))
106}
107
108#[derive(Debug, PartialEq, Eq)]
110pub enum DiffKind {
111 Added,
112 Removed,
113 Changed,
114}
115
116#[derive(Debug)]
118pub struct DiffEntry {
119 pub key: String,
120 pub kind: DiffKind,
121 pub old_value: Option<String>,
122 pub new_value: Option<String>,
123}
124
125pub fn diff_secrets(
127 old: &HashMap<String, String>,
128 new: &HashMap<String, String>,
129) -> Vec<DiffEntry> {
130 let mut all_keys: Vec<&str> = old
131 .keys()
132 .chain(new.keys())
133 .map(String::as_str)
134 .collect::<std::collections::HashSet<_>>()
135 .into_iter()
136 .collect();
137 all_keys.sort_unstable();
138
139 let mut entries = Vec::new();
140 for key in all_keys {
141 match (old.get(key), new.get(key)) {
142 (None, Some(v)) => entries.push(DiffEntry {
143 key: key.into(),
144 kind: DiffKind::Added,
145 old_value: None,
146 new_value: Some(v.clone()),
147 }),
148 (Some(v), None) => entries.push(DiffEntry {
149 key: key.into(),
150 kind: DiffKind::Removed,
151 old_value: Some(v.clone()),
152 new_value: None,
153 }),
154 (Some(old_v), Some(new_v)) if old_v != new_v => entries.push(DiffEntry {
155 key: key.into(),
156 kind: DiffKind::Changed,
157 old_value: Some(old_v.clone()),
158 new_value: Some(new_v.clone()),
159 }),
160 _ => {}
161 }
162 }
163 entries
164}
165
166pub fn format_diff_lines(entries: &[DiffEntry], show_values: bool) -> Vec<String> {
169 entries
170 .iter()
171 .map(|entry| {
172 let symbol = match entry.kind {
173 DiffKind::Added => "+",
174 DiffKind::Removed => "-",
175 DiffKind::Changed => "~",
176 };
177 if show_values {
178 match entry.kind {
179 DiffKind::Added => format!(
180 "{symbol} {} = {}",
181 entry.key,
182 entry.new_value.as_deref().unwrap_or("")
183 ),
184 DiffKind::Removed => format!(
185 "{symbol} {} = {}",
186 entry.key,
187 entry.old_value.as_deref().unwrap_or("")
188 ),
189 DiffKind::Changed => format!(
190 "{symbol} {} {} → {}",
191 entry.key,
192 entry.old_value.as_deref().unwrap_or(""),
193 entry.new_value.as_deref().unwrap_or("")
194 ),
195 }
196 } else {
197 format!("{symbol} {}", entry.key)
198 }
199 })
200 .collect()
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::testutil::*;
207 use crate::types;
208
209 #[test]
210 fn export_secrets_basic() {
211 let mut vault = empty_vault();
212 vault.schema.insert(
213 "FOO".into(),
214 types::SchemaEntry {
215 description: String::new(),
216 example: None,
217 tags: vec![],
218 ..Default::default()
219 },
220 );
221
222 let mut murk = empty_murk();
223 murk.values.insert("FOO".into(), "bar".into());
224
225 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
226 assert_eq!(exports.len(), 1);
227 assert_eq!(exports["FOO"], "bar");
228 }
229
230 #[test]
231 fn export_secrets_scoped_override() {
232 let mut vault = empty_vault();
233 vault.schema.insert(
234 "KEY".into(),
235 types::SchemaEntry {
236 description: String::new(),
237 example: None,
238 tags: vec![],
239 ..Default::default()
240 },
241 );
242
243 let mut murk = empty_murk();
244 murk.values.insert("KEY".into(), "shared".into());
245 let mut scoped = HashMap::new();
246 scoped.insert("age1pk".into(), "override".into());
247 murk.scoped.insert("KEY".into(), scoped);
248
249 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
250 assert_eq!(exports["KEY"], "override");
251 }
252
253 #[test]
254 fn export_secrets_tag_filter() {
255 let mut vault = empty_vault();
256 vault.schema.insert(
257 "A".into(),
258 types::SchemaEntry {
259 description: String::new(),
260 example: None,
261 tags: vec!["db".into()],
262 ..Default::default()
263 },
264 );
265 vault.schema.insert(
266 "B".into(),
267 types::SchemaEntry {
268 description: String::new(),
269 example: None,
270 tags: vec!["api".into()],
271 ..Default::default()
272 },
273 );
274
275 let mut murk = empty_murk();
276 murk.values.insert("A".into(), "val_a".into());
277 murk.values.insert("B".into(), "val_b".into());
278
279 let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
280 assert_eq!(exports.len(), 1);
281 assert_eq!(exports["A"], "val_a");
282 }
283
284 #[test]
285 fn export_secrets_shell_escaping() {
286 let mut vault = empty_vault();
287 vault.schema.insert(
288 "KEY".into(),
289 types::SchemaEntry {
290 description: String::new(),
291 example: None,
292 tags: vec![],
293 ..Default::default()
294 },
295 );
296
297 let mut murk = empty_murk();
298 murk.values.insert("KEY".into(), "it's a test".into());
299
300 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
301 assert_eq!(exports["KEY"], "it'\\''s a test");
302 }
303
304 #[test]
305 fn diff_secrets_no_changes() {
306 let old = HashMap::from([("K".into(), "V".into())]);
307 let new = old.clone();
308 assert!(diff_secrets(&old, &new).is_empty());
309 }
310
311 #[test]
312 fn diff_secrets_added() {
313 let old = HashMap::new();
314 let new = HashMap::from([("KEY".into(), "val".into())]);
315 let entries = diff_secrets(&old, &new);
316 assert_eq!(entries.len(), 1);
317 assert_eq!(entries[0].kind, DiffKind::Added);
318 assert_eq!(entries[0].key, "KEY");
319 assert_eq!(entries[0].new_value.as_deref(), Some("val"));
320 }
321
322 #[test]
323 fn diff_secrets_removed() {
324 let old = HashMap::from([("KEY".into(), "val".into())]);
325 let new = HashMap::new();
326 let entries = diff_secrets(&old, &new);
327 assert_eq!(entries.len(), 1);
328 assert_eq!(entries[0].kind, DiffKind::Removed);
329 assert_eq!(entries[0].old_value.as_deref(), Some("val"));
330 }
331
332 #[test]
333 fn diff_secrets_changed() {
334 let old = HashMap::from([("KEY".into(), "old_val".into())]);
335 let new = HashMap::from([("KEY".into(), "new_val".into())]);
336 let entries = diff_secrets(&old, &new);
337 assert_eq!(entries.len(), 1);
338 assert_eq!(entries[0].kind, DiffKind::Changed);
339 assert_eq!(entries[0].old_value.as_deref(), Some("old_val"));
340 assert_eq!(entries[0].new_value.as_deref(), Some("new_val"));
341 }
342
343 #[test]
344 fn diff_secrets_mixed() {
345 let old = HashMap::from([
346 ("KEEP".into(), "same".into()),
347 ("REMOVE".into(), "gone".into()),
348 ("CHANGE".into(), "old".into()),
349 ]);
350 let new = HashMap::from([
351 ("KEEP".into(), "same".into()),
352 ("ADD".into(), "new".into()),
353 ("CHANGE".into(), "new".into()),
354 ]);
355 let entries = diff_secrets(&old, &new);
356 assert_eq!(entries.len(), 3);
357
358 let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
359 assert!(kinds.contains(&&DiffKind::Added));
360 assert!(kinds.contains(&&DiffKind::Removed));
361 assert!(kinds.contains(&&DiffKind::Changed));
362 }
363
364 #[test]
365 fn diff_secrets_sorted_by_key() {
366 let old = HashMap::new();
367 let new = HashMap::from([
368 ("Z".into(), "z".into()),
369 ("A".into(), "a".into()),
370 ("M".into(), "m".into()),
371 ]);
372 let entries = diff_secrets(&old, &new);
373 let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
374 assert_eq!(keys, vec!["A", "M", "Z"]);
375 }
376
377 #[test]
380 fn format_diff_lines_without_values() {
381 let entries = vec![
382 DiffEntry {
383 key: "NEW_KEY".into(),
384 kind: DiffKind::Added,
385 old_value: None,
386 new_value: Some("secret".into()),
387 },
388 DiffEntry {
389 key: "OLD_KEY".into(),
390 kind: DiffKind::Removed,
391 old_value: Some("old".into()),
392 new_value: None,
393 },
394 DiffEntry {
395 key: "MOD_KEY".into(),
396 kind: DiffKind::Changed,
397 old_value: Some("v1".into()),
398 new_value: Some("v2".into()),
399 },
400 ];
401 let lines = format_diff_lines(&entries, false);
402 assert_eq!(lines, vec!["+ NEW_KEY", "- OLD_KEY", "~ MOD_KEY"]);
403 }
404
405 #[test]
406 fn format_diff_lines_with_values() {
407 let entries = vec![
408 DiffEntry {
409 key: "KEY".into(),
410 kind: DiffKind::Added,
411 old_value: None,
412 new_value: Some("new_val".into()),
413 },
414 DiffEntry {
415 key: "KEY2".into(),
416 kind: DiffKind::Changed,
417 old_value: Some("old".into()),
418 new_value: Some("new".into()),
419 },
420 ];
421 let lines = format_diff_lines(&entries, true);
422 assert_eq!(lines[0], "+ KEY = new_val");
423 assert_eq!(lines[1], "~ KEY2 old → new");
424 }
425
426 #[test]
427 fn format_diff_lines_empty() {
428 let lines = format_diff_lines(&[], false);
429 assert!(lines.is_empty());
430 }
431
432 #[test]
435 fn resolve_secrets_basic() {
436 let mut vault = empty_vault();
437 vault.schema.insert(
438 "FOO".into(),
439 types::SchemaEntry {
440 description: String::new(),
441 example: None,
442 tags: vec![],
443 ..Default::default()
444 },
445 );
446
447 let mut murk = empty_murk();
448 murk.values.insert("FOO".into(), "bar".into());
449
450 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
451 assert_eq!(resolved.len(), 1);
452 assert_eq!(resolved["FOO"], "bar");
453 }
454
455 #[test]
456 fn resolve_secrets_no_escaping() {
457 let mut vault = empty_vault();
458 vault.schema.insert(
459 "KEY".into(),
460 types::SchemaEntry {
461 description: String::new(),
462 example: None,
463 tags: vec![],
464 ..Default::default()
465 },
466 );
467
468 let mut murk = empty_murk();
469 murk.values.insert("KEY".into(), "it's a test".into());
470
471 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
472 assert_eq!(resolved["KEY"], "it's a test");
473 }
474
475 #[test]
476 fn resolve_secrets_scoped_override() {
477 let mut vault = empty_vault();
478 vault.schema.insert(
479 "KEY".into(),
480 types::SchemaEntry {
481 description: String::new(),
482 example: None,
483 tags: vec![],
484 ..Default::default()
485 },
486 );
487
488 let mut murk = empty_murk();
489 murk.values.insert("KEY".into(), "shared".into());
490 let mut scoped = HashMap::new();
491 scoped.insert("age1pk".into(), "override".into());
492 murk.scoped.insert("KEY".into(), scoped);
493
494 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
495 assert_eq!(resolved["KEY"], "override");
496 }
497
498 #[test]
499 fn resolve_secrets_tag_filter() {
500 let mut vault = empty_vault();
501 vault.schema.insert(
502 "A".into(),
503 types::SchemaEntry {
504 description: String::new(),
505 example: None,
506 tags: vec!["db".into()],
507 ..Default::default()
508 },
509 );
510 vault.schema.insert(
511 "B".into(),
512 types::SchemaEntry {
513 description: String::new(),
514 example: None,
515 tags: vec!["api".into()],
516 ..Default::default()
517 },
518 );
519
520 let mut murk = empty_murk();
521 murk.values.insert("A".into(), "val_a".into());
522 murk.values.insert("B".into(), "val_b".into());
523
524 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
525 assert_eq!(resolved.len(), 1);
526 assert_eq!(resolved["A"], "val_a");
527 }
528
529 #[test]
530 fn resolve_secrets_tag_in_schema_but_no_secret() {
531 let mut vault = empty_vault();
532 vault.schema.insert(
534 "ORPHAN".into(),
535 types::SchemaEntry {
536 description: "orphan key".into(),
537 example: None,
538 tags: vec!["db".into()],
539 ..Default::default()
540 },
541 );
542 vault.schema.insert(
543 "REAL".into(),
544 types::SchemaEntry {
545 description: "has a value".into(),
546 example: None,
547 tags: vec!["db".into()],
548 ..Default::default()
549 },
550 );
551
552 let mut murk = empty_murk();
553 murk.values.insert("REAL".into(), "real_val".into());
555
556 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
557 assert_eq!(resolved.len(), 1);
559 assert_eq!(resolved["REAL"], "real_val");
560 assert!(!resolved.contains_key("ORPHAN"));
561 }
562
563 #[test]
564 fn resolve_secrets_scoped_pubkey_not_in_recipients() {
565 let mut vault = empty_vault();
566 vault.recipients = vec!["age1alice".into()];
567 vault.schema.insert(
568 "KEY".into(),
569 types::SchemaEntry {
570 description: String::new(),
571 example: None,
572 tags: vec![],
573 ..Default::default()
574 },
575 );
576
577 let mut murk = empty_murk();
578 murk.values.insert("KEY".into(), "shared".into());
579 let mut scoped = HashMap::new();
581 scoped.insert("age1outsider".into(), "outsider_val".into());
582 murk.scoped.insert("KEY".into(), scoped);
583
584 let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
586 assert_eq!(resolved["KEY"], "outsider_val");
587
588 let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
590 assert_eq!(resolved_alice["KEY"], "shared");
591 }
592
593 #[test]
596 fn export_secrets_empty_vault() {
597 let vault = empty_vault();
598 let murk = empty_murk();
599 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
600 assert!(exports.is_empty());
601 }
602
603 #[test]
604 fn decrypt_vault_values_basic() {
605 let (secret, pubkey) = generate_keypair();
606 let recipient = make_recipient(&pubkey);
607 let identity = make_identity(&secret);
608
609 let mut vault = empty_vault();
610 vault.recipients = vec![pubkey];
611 vault.secrets.insert(
612 "KEY1".into(),
613 types::SecretEntry {
614 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
615 scoped: std::collections::BTreeMap::new(),
616 },
617 );
618 vault.secrets.insert(
619 "KEY2".into(),
620 types::SecretEntry {
621 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
622 scoped: std::collections::BTreeMap::new(),
623 },
624 );
625
626 let values = crate::export::decrypt_vault_values(&vault, &identity);
627 assert_eq!(values.len(), 2);
628 assert_eq!(values["KEY1"], "val1");
629 assert_eq!(values["KEY2"], "val2");
630 }
631
632 #[test]
633 fn decrypt_vault_values_wrong_key_skips() {
634 let (_, pubkey) = generate_keypair();
635 let recipient = make_recipient(&pubkey);
636 let (wrong_secret, _) = generate_keypair();
637 let wrong_identity = make_identity(&wrong_secret);
638
639 let mut vault = empty_vault();
640 vault.recipients = vec![pubkey];
641 vault.secrets.insert(
642 "KEY1".into(),
643 types::SecretEntry {
644 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
645 scoped: std::collections::BTreeMap::new(),
646 },
647 );
648
649 let values = crate::export::decrypt_vault_values(&vault, &wrong_identity);
650 assert!(values.is_empty());
651 }
652
653 #[test]
654 fn decrypt_vault_values_empty_vault() {
655 let (secret, _) = generate_keypair();
656 let identity = make_identity(&secret);
657 let vault = empty_vault();
658
659 let values = crate::export::decrypt_vault_values(&vault, &identity);
660 assert!(values.is_empty());
661 }
662
663 #[test]
664 fn diff_secrets_both_empty() {
665 let old = HashMap::new();
666 let new = HashMap::new();
667 assert!(diff_secrets(&old, &new).is_empty());
668 }
669
670 #[test]
673 fn parse_and_decrypt_values_roundtrip() {
674 let (secret, pubkey) = generate_keypair();
675 let recipient = make_recipient(&pubkey);
676 let identity = make_identity(&secret);
677
678 let mut vault = empty_vault();
679 vault.recipients = vec![pubkey];
680 vault.secrets.insert(
681 "KEY1".into(),
682 types::SecretEntry {
683 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
684 scoped: std::collections::BTreeMap::new(),
685 },
686 );
687 vault.secrets.insert(
688 "KEY2".into(),
689 types::SecretEntry {
690 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
691 scoped: std::collections::BTreeMap::new(),
692 },
693 );
694
695 let json = serde_json::to_string(&vault).unwrap();
696 let values = parse_and_decrypt_values(&json, &identity).unwrap();
697 assert_eq!(values.len(), 2);
698 assert_eq!(values["KEY1"], "val1");
699 assert_eq!(values["KEY2"], "val2");
700 }
701
702 #[test]
703 fn parse_and_decrypt_values_invalid_json() {
704 let (secret, _) = generate_keypair();
705 let identity = make_identity(&secret);
706
707 let result = parse_and_decrypt_values("not valid json", &identity);
708 assert!(result.is_err());
709 }
710
711 #[test]
712 fn parse_and_decrypt_values_wrong_key() {
713 let (_, pubkey) = generate_keypair();
714 let recipient = make_recipient(&pubkey);
715 let (wrong_secret, _) = generate_keypair();
716 let wrong_identity = make_identity(&wrong_secret);
717
718 let mut vault = empty_vault();
719 vault.recipients = vec![pubkey];
720 vault.secrets.insert(
721 "KEY1".into(),
722 types::SecretEntry {
723 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
724 scoped: std::collections::BTreeMap::new(),
725 },
726 );
727
728 let json = serde_json::to_string(&vault).unwrap();
729 let values = parse_and_decrypt_values(&json, &wrong_identity).unwrap();
730 assert!(values.is_empty());
731 }
732}