Skip to main content

murk_cli/
export.rs

1//! Export and diff logic for vault secrets.
2
3use std::collections::{BTreeMap, HashMap};
4
5use zeroize::Zeroizing;
6
7use crate::types;
8
9/// Merge private overrides over shared values and filter by tag.
10/// Returns raw (unescaped) values suitable for env var injection.
11///
12/// Values are wrapped in `Zeroizing` so plaintext is cleared from memory
13/// when the returned map is dropped.
14pub fn resolve_secrets(
15    vault: &types::Vault,
16    murk: &types::Murk,
17    pubkey: &str,
18    tags: &[String],
19) -> BTreeMap<String, Zeroizing<String>> {
20    // Collect straight into the result map — no intermediate map holding
21    // extra copies of plaintext.
22    let mut values = murk
23        .values
24        .iter()
25        .map(|(k, v)| (k.clone(), v.clone()))
26        .collect::<BTreeMap<String, Zeroizing<String>>>();
27
28    // Apply named-group values we can read. A group secret has no shared value,
29    // so this is usually an addition; the private override (below) still wins over it.
30    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    // Apply private overrides.
37    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    // Filter by tag.
44    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
56/// Build shell-escaped export key-value pairs for `eval $(murk export)`.
57/// Wraps values in single quotes with embedded quote escaping.
58pub 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
70/// Decrypt all shared secret values from a vault.
71///
72/// Silently skips entries that fail to decrypt (the caller may not have been
73/// a recipient at the time the vault was written). Returns a map of key → plaintext value.
74pub 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        // Decrypt shared value.
82        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        // Private override takes priority.
91        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
103/// Parse a vault from its JSON string and decrypt all shared values.
104///
105/// Combines `vault::parse` with `decrypt_vault_values` for use cases
106/// where the vault contents come from a string (e.g., `git show`).
107pub 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/// The kind of change in a diff entry.
116#[derive(Debug, PartialEq, Eq)]
117pub enum DiffKind {
118    Added,
119    Removed,
120    Changed,
121}
122
123/// A single entry in a secret diff.
124///
125/// `old_value` and `new_value` are held in `Zeroizing` so plaintext is cleared
126/// when the entry is dropped. Formatting/printing callers should take care not
127/// to retain their own unzeroed copies.
128#[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
136/// Compare two sets of secret values and return the differences.
137pub 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
177/// Format diff entries as display lines.
178/// Returns plain-text lines (no ANSI colors) suitable for testing.
179pub 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    // ── format_diff_lines tests ──
386
387    #[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    // ── resolve_secrets tests ──
441
442    #[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        // Schema says key "ORPHAN" exists with tag "db", but no secret value.
541        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        // Only REAL has a value, ORPHAN does not.
562        murk.values.insert("REAL".into(), secret("real_val"));
563
564        let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
565        // ORPHAN should not appear since it has no value.
566        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        // Private override for a pubkey NOT in vault.recipients.
588        let mut scoped = HashMap::new();
589        scoped.insert("age1outsider".into(), secret("outsider_val"));
590        murk.private.insert("KEY".into(), scoped);
591
592        // The outsider's override should still be applied (resolve doesn't gate on recipient list).
593        let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
594        assert_eq!(resolved["KEY"].as_str(), "outsider_val");
595
596        // Alice gets the shared value since she has no private override.
597        let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
598        assert_eq!(resolved_alice["KEY"].as_str(), "shared");
599    }
600
601    // ── New edge-case tests ──
602
603    #[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    // ── parse_and_decrypt_values tests ──
682
683    #[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}