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 },
219 );
220
221 let mut murk = empty_murk();
222 murk.values.insert("FOO".into(), "bar".into());
223
224 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
225 assert_eq!(exports.len(), 1);
226 assert_eq!(exports["FOO"], "bar");
227 }
228
229 #[test]
230 fn export_secrets_scoped_override() {
231 let mut vault = empty_vault();
232 vault.schema.insert(
233 "KEY".into(),
234 types::SchemaEntry {
235 description: String::new(),
236 example: None,
237 tags: vec![],
238 },
239 );
240
241 let mut murk = empty_murk();
242 murk.values.insert("KEY".into(), "shared".into());
243 let mut scoped = HashMap::new();
244 scoped.insert("age1pk".into(), "override".into());
245 murk.scoped.insert("KEY".into(), scoped);
246
247 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
248 assert_eq!(exports["KEY"], "override");
249 }
250
251 #[test]
252 fn export_secrets_tag_filter() {
253 let mut vault = empty_vault();
254 vault.schema.insert(
255 "A".into(),
256 types::SchemaEntry {
257 description: String::new(),
258 example: None,
259 tags: vec!["db".into()],
260 },
261 );
262 vault.schema.insert(
263 "B".into(),
264 types::SchemaEntry {
265 description: String::new(),
266 example: None,
267 tags: vec!["api".into()],
268 },
269 );
270
271 let mut murk = empty_murk();
272 murk.values.insert("A".into(), "val_a".into());
273 murk.values.insert("B".into(), "val_b".into());
274
275 let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
276 assert_eq!(exports.len(), 1);
277 assert_eq!(exports["A"], "val_a");
278 }
279
280 #[test]
281 fn export_secrets_shell_escaping() {
282 let mut vault = empty_vault();
283 vault.schema.insert(
284 "KEY".into(),
285 types::SchemaEntry {
286 description: String::new(),
287 example: None,
288 tags: vec![],
289 },
290 );
291
292 let mut murk = empty_murk();
293 murk.values.insert("KEY".into(), "it's a test".into());
294
295 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
296 assert_eq!(exports["KEY"], "it'\\''s a test");
297 }
298
299 #[test]
300 fn diff_secrets_no_changes() {
301 let old = HashMap::from([("K".into(), "V".into())]);
302 let new = old.clone();
303 assert!(diff_secrets(&old, &new).is_empty());
304 }
305
306 #[test]
307 fn diff_secrets_added() {
308 let old = HashMap::new();
309 let new = HashMap::from([("KEY".into(), "val".into())]);
310 let entries = diff_secrets(&old, &new);
311 assert_eq!(entries.len(), 1);
312 assert_eq!(entries[0].kind, DiffKind::Added);
313 assert_eq!(entries[0].key, "KEY");
314 assert_eq!(entries[0].new_value.as_deref(), Some("val"));
315 }
316
317 #[test]
318 fn diff_secrets_removed() {
319 let old = HashMap::from([("KEY".into(), "val".into())]);
320 let new = HashMap::new();
321 let entries = diff_secrets(&old, &new);
322 assert_eq!(entries.len(), 1);
323 assert_eq!(entries[0].kind, DiffKind::Removed);
324 assert_eq!(entries[0].old_value.as_deref(), Some("val"));
325 }
326
327 #[test]
328 fn diff_secrets_changed() {
329 let old = HashMap::from([("KEY".into(), "old_val".into())]);
330 let new = HashMap::from([("KEY".into(), "new_val".into())]);
331 let entries = diff_secrets(&old, &new);
332 assert_eq!(entries.len(), 1);
333 assert_eq!(entries[0].kind, DiffKind::Changed);
334 assert_eq!(entries[0].old_value.as_deref(), Some("old_val"));
335 assert_eq!(entries[0].new_value.as_deref(), Some("new_val"));
336 }
337
338 #[test]
339 fn diff_secrets_mixed() {
340 let old = HashMap::from([
341 ("KEEP".into(), "same".into()),
342 ("REMOVE".into(), "gone".into()),
343 ("CHANGE".into(), "old".into()),
344 ]);
345 let new = HashMap::from([
346 ("KEEP".into(), "same".into()),
347 ("ADD".into(), "new".into()),
348 ("CHANGE".into(), "new".into()),
349 ]);
350 let entries = diff_secrets(&old, &new);
351 assert_eq!(entries.len(), 3);
352
353 let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
354 assert!(kinds.contains(&&DiffKind::Added));
355 assert!(kinds.contains(&&DiffKind::Removed));
356 assert!(kinds.contains(&&DiffKind::Changed));
357 }
358
359 #[test]
360 fn diff_secrets_sorted_by_key() {
361 let old = HashMap::new();
362 let new = HashMap::from([
363 ("Z".into(), "z".into()),
364 ("A".into(), "a".into()),
365 ("M".into(), "m".into()),
366 ]);
367 let entries = diff_secrets(&old, &new);
368 let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
369 assert_eq!(keys, vec!["A", "M", "Z"]);
370 }
371
372 #[test]
375 fn format_diff_lines_without_values() {
376 let entries = vec![
377 DiffEntry {
378 key: "NEW_KEY".into(),
379 kind: DiffKind::Added,
380 old_value: None,
381 new_value: Some("secret".into()),
382 },
383 DiffEntry {
384 key: "OLD_KEY".into(),
385 kind: DiffKind::Removed,
386 old_value: Some("old".into()),
387 new_value: None,
388 },
389 DiffEntry {
390 key: "MOD_KEY".into(),
391 kind: DiffKind::Changed,
392 old_value: Some("v1".into()),
393 new_value: Some("v2".into()),
394 },
395 ];
396 let lines = format_diff_lines(&entries, false);
397 assert_eq!(lines, vec!["+ NEW_KEY", "- OLD_KEY", "~ MOD_KEY"]);
398 }
399
400 #[test]
401 fn format_diff_lines_with_values() {
402 let entries = vec![
403 DiffEntry {
404 key: "KEY".into(),
405 kind: DiffKind::Added,
406 old_value: None,
407 new_value: Some("new_val".into()),
408 },
409 DiffEntry {
410 key: "KEY2".into(),
411 kind: DiffKind::Changed,
412 old_value: Some("old".into()),
413 new_value: Some("new".into()),
414 },
415 ];
416 let lines = format_diff_lines(&entries, true);
417 assert_eq!(lines[0], "+ KEY = new_val");
418 assert_eq!(lines[1], "~ KEY2 old → new");
419 }
420
421 #[test]
422 fn format_diff_lines_empty() {
423 let lines = format_diff_lines(&[], false);
424 assert!(lines.is_empty());
425 }
426
427 #[test]
430 fn resolve_secrets_basic() {
431 let mut vault = empty_vault();
432 vault.schema.insert(
433 "FOO".into(),
434 types::SchemaEntry {
435 description: String::new(),
436 example: None,
437 tags: vec![],
438 },
439 );
440
441 let mut murk = empty_murk();
442 murk.values.insert("FOO".into(), "bar".into());
443
444 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
445 assert_eq!(resolved.len(), 1);
446 assert_eq!(resolved["FOO"], "bar");
447 }
448
449 #[test]
450 fn resolve_secrets_no_escaping() {
451 let mut vault = empty_vault();
452 vault.schema.insert(
453 "KEY".into(),
454 types::SchemaEntry {
455 description: String::new(),
456 example: None,
457 tags: vec![],
458 },
459 );
460
461 let mut murk = empty_murk();
462 murk.values.insert("KEY".into(), "it's a test".into());
463
464 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
465 assert_eq!(resolved["KEY"], "it's a test");
466 }
467
468 #[test]
469 fn resolve_secrets_scoped_override() {
470 let mut vault = empty_vault();
471 vault.schema.insert(
472 "KEY".into(),
473 types::SchemaEntry {
474 description: String::new(),
475 example: None,
476 tags: vec![],
477 },
478 );
479
480 let mut murk = empty_murk();
481 murk.values.insert("KEY".into(), "shared".into());
482 let mut scoped = HashMap::new();
483 scoped.insert("age1pk".into(), "override".into());
484 murk.scoped.insert("KEY".into(), scoped);
485
486 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
487 assert_eq!(resolved["KEY"], "override");
488 }
489
490 #[test]
491 fn resolve_secrets_tag_filter() {
492 let mut vault = empty_vault();
493 vault.schema.insert(
494 "A".into(),
495 types::SchemaEntry {
496 description: String::new(),
497 example: None,
498 tags: vec!["db".into()],
499 },
500 );
501 vault.schema.insert(
502 "B".into(),
503 types::SchemaEntry {
504 description: String::new(),
505 example: None,
506 tags: vec!["api".into()],
507 },
508 );
509
510 let mut murk = empty_murk();
511 murk.values.insert("A".into(), "val_a".into());
512 murk.values.insert("B".into(), "val_b".into());
513
514 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
515 assert_eq!(resolved.len(), 1);
516 assert_eq!(resolved["A"], "val_a");
517 }
518
519 #[test]
520 fn resolve_secrets_tag_in_schema_but_no_secret() {
521 let mut vault = empty_vault();
522 vault.schema.insert(
524 "ORPHAN".into(),
525 types::SchemaEntry {
526 description: "orphan key".into(),
527 example: None,
528 tags: vec!["db".into()],
529 },
530 );
531 vault.schema.insert(
532 "REAL".into(),
533 types::SchemaEntry {
534 description: "has a value".into(),
535 example: None,
536 tags: vec!["db".into()],
537 },
538 );
539
540 let mut murk = empty_murk();
541 murk.values.insert("REAL".into(), "real_val".into());
543
544 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
545 assert_eq!(resolved.len(), 1);
547 assert_eq!(resolved["REAL"], "real_val");
548 assert!(!resolved.contains_key("ORPHAN"));
549 }
550
551 #[test]
552 fn resolve_secrets_scoped_pubkey_not_in_recipients() {
553 let mut vault = empty_vault();
554 vault.recipients = vec!["age1alice".into()];
555 vault.schema.insert(
556 "KEY".into(),
557 types::SchemaEntry {
558 description: String::new(),
559 example: None,
560 tags: vec![],
561 },
562 );
563
564 let mut murk = empty_murk();
565 murk.values.insert("KEY".into(), "shared".into());
566 let mut scoped = HashMap::new();
568 scoped.insert("age1outsider".into(), "outsider_val".into());
569 murk.scoped.insert("KEY".into(), scoped);
570
571 let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
573 assert_eq!(resolved["KEY"], "outsider_val");
574
575 let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
577 assert_eq!(resolved_alice["KEY"], "shared");
578 }
579
580 #[test]
583 fn export_secrets_empty_vault() {
584 let vault = empty_vault();
585 let murk = empty_murk();
586 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
587 assert!(exports.is_empty());
588 }
589
590 #[test]
591 fn decrypt_vault_values_basic() {
592 let (secret, pubkey) = generate_keypair();
593 let recipient = make_recipient(&pubkey);
594 let identity = make_identity(&secret);
595
596 let mut vault = empty_vault();
597 vault.recipients = vec![pubkey];
598 vault.secrets.insert(
599 "KEY1".into(),
600 types::SecretEntry {
601 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
602 scoped: std::collections::BTreeMap::new(),
603 },
604 );
605 vault.secrets.insert(
606 "KEY2".into(),
607 types::SecretEntry {
608 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
609 scoped: std::collections::BTreeMap::new(),
610 },
611 );
612
613 let values = crate::export::decrypt_vault_values(&vault, &identity);
614 assert_eq!(values.len(), 2);
615 assert_eq!(values["KEY1"], "val1");
616 assert_eq!(values["KEY2"], "val2");
617 }
618
619 #[test]
620 fn decrypt_vault_values_wrong_key_skips() {
621 let (_, pubkey) = generate_keypair();
622 let recipient = make_recipient(&pubkey);
623 let (wrong_secret, _) = generate_keypair();
624 let wrong_identity = make_identity(&wrong_secret);
625
626 let mut vault = empty_vault();
627 vault.recipients = vec![pubkey];
628 vault.secrets.insert(
629 "KEY1".into(),
630 types::SecretEntry {
631 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
632 scoped: std::collections::BTreeMap::new(),
633 },
634 );
635
636 let values = crate::export::decrypt_vault_values(&vault, &wrong_identity);
637 assert!(values.is_empty());
638 }
639
640 #[test]
641 fn decrypt_vault_values_empty_vault() {
642 let (secret, _) = generate_keypair();
643 let identity = make_identity(&secret);
644 let vault = empty_vault();
645
646 let values = crate::export::decrypt_vault_values(&vault, &identity);
647 assert!(values.is_empty());
648 }
649
650 #[test]
651 fn diff_secrets_both_empty() {
652 let old = HashMap::new();
653 let new = HashMap::new();
654 assert!(diff_secrets(&old, &new).is_empty());
655 }
656
657 #[test]
660 fn parse_and_decrypt_values_roundtrip() {
661 let (secret, pubkey) = generate_keypair();
662 let recipient = make_recipient(&pubkey);
663 let identity = make_identity(&secret);
664
665 let mut vault = empty_vault();
666 vault.recipients = vec![pubkey];
667 vault.secrets.insert(
668 "KEY1".into(),
669 types::SecretEntry {
670 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
671 scoped: std::collections::BTreeMap::new(),
672 },
673 );
674 vault.secrets.insert(
675 "KEY2".into(),
676 types::SecretEntry {
677 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
678 scoped: std::collections::BTreeMap::new(),
679 },
680 );
681
682 let json = serde_json::to_string(&vault).unwrap();
683 let values = parse_and_decrypt_values(&json, &identity).unwrap();
684 assert_eq!(values.len(), 2);
685 assert_eq!(values["KEY1"], "val1");
686 assert_eq!(values["KEY2"], "val2");
687 }
688
689 #[test]
690 fn parse_and_decrypt_values_invalid_json() {
691 let (secret, _) = generate_keypair();
692 let identity = make_identity(&secret);
693
694 let result = parse_and_decrypt_values("not valid json", &identity);
695 assert!(result.is_err());
696 }
697
698 #[test]
699 fn parse_and_decrypt_values_wrong_key() {
700 let (_, pubkey) = generate_keypair();
701 let recipient = make_recipient(&pubkey);
702 let (wrong_secret, _) = generate_keypair();
703 let wrong_identity = make_identity(&wrong_secret);
704
705 let mut vault = empty_vault();
706 vault.recipients = vec![pubkey];
707 vault.secrets.insert(
708 "KEY1".into(),
709 types::SecretEntry {
710 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
711 scoped: std::collections::BTreeMap::new(),
712 },
713 );
714
715 let json = serde_json::to_string(&vault).unwrap();
716 let values = parse_and_decrypt_values(&json, &wrong_identity).unwrap();
717 assert!(values.is_empty());
718 }
719}