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