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