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 let Some(ref allowed) = allowed_keys {
41 if !allowed.contains(k.as_str()) {
42 continue;
43 }
44 }
45 result.insert(k.clone(), v.clone());
46 }
47 result
48}
49
50pub fn export_secrets(
53 vault: &types::Vault,
54 murk: &types::Murk,
55 pubkey: &str,
56 tags: &[String],
57) -> BTreeMap<String, String> {
58 resolve_secrets(vault, murk, pubkey, tags)
59 .into_iter()
60 .map(|(k, v)| (k, v.replace('\'', "'\\''")))
61 .collect()
62}
63
64pub fn decrypt_vault_values(
69 vault: &types::Vault,
70 identity: &age::x25519::Identity,
71) -> HashMap<String, String> {
72 let mut values = HashMap::new();
73 for (key, entry) in &vault.secrets {
74 if let Ok(plaintext) = crate::decrypt_value(&entry.shared, identity) {
75 if let Ok(value) = String::from_utf8(plaintext) {
76 values.insert(key.clone(), value);
77 }
78 }
79 }
80 values
81}
82
83pub fn parse_and_decrypt_values(
88 vault_contents: &str,
89 identity: &age::x25519::Identity,
90) -> Result<HashMap<String, String>, String> {
91 let vault = crate::vault::parse(vault_contents).map_err(|e| e.to_string())?;
92 Ok(decrypt_vault_values(&vault, identity))
93}
94
95#[derive(Debug, PartialEq, Eq)]
97pub enum DiffKind {
98 Added,
99 Removed,
100 Changed,
101}
102
103#[derive(Debug)]
105pub struct DiffEntry {
106 pub key: String,
107 pub kind: DiffKind,
108 pub old_value: Option<String>,
109 pub new_value: Option<String>,
110}
111
112pub fn diff_secrets(
114 old: &HashMap<String, String>,
115 new: &HashMap<String, String>,
116) -> Vec<DiffEntry> {
117 let mut all_keys: Vec<&str> = old
118 .keys()
119 .chain(new.keys())
120 .map(String::as_str)
121 .collect::<std::collections::HashSet<_>>()
122 .into_iter()
123 .collect();
124 all_keys.sort_unstable();
125
126 let mut entries = Vec::new();
127 for key in all_keys {
128 match (old.get(key), new.get(key)) {
129 (None, Some(v)) => entries.push(DiffEntry {
130 key: key.into(),
131 kind: DiffKind::Added,
132 old_value: None,
133 new_value: Some(v.clone()),
134 }),
135 (Some(v), None) => entries.push(DiffEntry {
136 key: key.into(),
137 kind: DiffKind::Removed,
138 old_value: Some(v.clone()),
139 new_value: None,
140 }),
141 (Some(old_v), Some(new_v)) if old_v != new_v => entries.push(DiffEntry {
142 key: key.into(),
143 kind: DiffKind::Changed,
144 old_value: Some(old_v.clone()),
145 new_value: Some(new_v.clone()),
146 }),
147 _ => {}
148 }
149 }
150 entries
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::testutil::*;
157 use crate::types;
158
159 #[test]
160 fn export_secrets_basic() {
161 let mut vault = empty_vault();
162 vault.schema.insert(
163 "FOO".into(),
164 types::SchemaEntry {
165 description: String::new(),
166 example: None,
167 tags: vec![],
168 },
169 );
170
171 let mut murk = empty_murk();
172 murk.values.insert("FOO".into(), "bar".into());
173
174 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
175 assert_eq!(exports.len(), 1);
176 assert_eq!(exports["FOO"], "bar");
177 }
178
179 #[test]
180 fn export_secrets_scoped_override() {
181 let mut vault = empty_vault();
182 vault.schema.insert(
183 "KEY".into(),
184 types::SchemaEntry {
185 description: String::new(),
186 example: None,
187 tags: vec![],
188 },
189 );
190
191 let mut murk = empty_murk();
192 murk.values.insert("KEY".into(), "shared".into());
193 let mut scoped = HashMap::new();
194 scoped.insert("age1pk".into(), "override".into());
195 murk.scoped.insert("KEY".into(), scoped);
196
197 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
198 assert_eq!(exports["KEY"], "override");
199 }
200
201 #[test]
202 fn export_secrets_tag_filter() {
203 let mut vault = empty_vault();
204 vault.schema.insert(
205 "A".into(),
206 types::SchemaEntry {
207 description: String::new(),
208 example: None,
209 tags: vec!["db".into()],
210 },
211 );
212 vault.schema.insert(
213 "B".into(),
214 types::SchemaEntry {
215 description: String::new(),
216 example: None,
217 tags: vec!["api".into()],
218 },
219 );
220
221 let mut murk = empty_murk();
222 murk.values.insert("A".into(), "val_a".into());
223 murk.values.insert("B".into(), "val_b".into());
224
225 let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
226 assert_eq!(exports.len(), 1);
227 assert_eq!(exports["A"], "val_a");
228 }
229
230 #[test]
231 fn export_secrets_shell_escaping() {
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 },
240 );
241
242 let mut murk = empty_murk();
243 murk.values.insert("KEY".into(), "it's a test".into());
244
245 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
246 assert_eq!(exports["KEY"], "it'\\''s a test");
247 }
248
249 #[test]
250 fn diff_secrets_no_changes() {
251 let old = HashMap::from([("K".into(), "V".into())]);
252 let new = old.clone();
253 assert!(diff_secrets(&old, &new).is_empty());
254 }
255
256 #[test]
257 fn diff_secrets_added() {
258 let old = HashMap::new();
259 let new = HashMap::from([("KEY".into(), "val".into())]);
260 let entries = diff_secrets(&old, &new);
261 assert_eq!(entries.len(), 1);
262 assert_eq!(entries[0].kind, DiffKind::Added);
263 assert_eq!(entries[0].key, "KEY");
264 assert_eq!(entries[0].new_value.as_deref(), Some("val"));
265 }
266
267 #[test]
268 fn diff_secrets_removed() {
269 let old = HashMap::from([("KEY".into(), "val".into())]);
270 let new = HashMap::new();
271 let entries = diff_secrets(&old, &new);
272 assert_eq!(entries.len(), 1);
273 assert_eq!(entries[0].kind, DiffKind::Removed);
274 assert_eq!(entries[0].old_value.as_deref(), Some("val"));
275 }
276
277 #[test]
278 fn diff_secrets_changed() {
279 let old = HashMap::from([("KEY".into(), "old_val".into())]);
280 let new = HashMap::from([("KEY".into(), "new_val".into())]);
281 let entries = diff_secrets(&old, &new);
282 assert_eq!(entries.len(), 1);
283 assert_eq!(entries[0].kind, DiffKind::Changed);
284 assert_eq!(entries[0].old_value.as_deref(), Some("old_val"));
285 assert_eq!(entries[0].new_value.as_deref(), Some("new_val"));
286 }
287
288 #[test]
289 fn diff_secrets_mixed() {
290 let old = HashMap::from([
291 ("KEEP".into(), "same".into()),
292 ("REMOVE".into(), "gone".into()),
293 ("CHANGE".into(), "old".into()),
294 ]);
295 let new = HashMap::from([
296 ("KEEP".into(), "same".into()),
297 ("ADD".into(), "new".into()),
298 ("CHANGE".into(), "new".into()),
299 ]);
300 let entries = diff_secrets(&old, &new);
301 assert_eq!(entries.len(), 3);
302
303 let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
304 assert!(kinds.contains(&&DiffKind::Added));
305 assert!(kinds.contains(&&DiffKind::Removed));
306 assert!(kinds.contains(&&DiffKind::Changed));
307 }
308
309 #[test]
310 fn diff_secrets_sorted_by_key() {
311 let old = HashMap::new();
312 let new = HashMap::from([
313 ("Z".into(), "z".into()),
314 ("A".into(), "a".into()),
315 ("M".into(), "m".into()),
316 ]);
317 let entries = diff_secrets(&old, &new);
318 let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
319 assert_eq!(keys, vec!["A", "M", "Z"]);
320 }
321
322 #[test]
325 fn resolve_secrets_basic() {
326 let mut vault = empty_vault();
327 vault.schema.insert(
328 "FOO".into(),
329 types::SchemaEntry {
330 description: String::new(),
331 example: None,
332 tags: vec![],
333 },
334 );
335
336 let mut murk = empty_murk();
337 murk.values.insert("FOO".into(), "bar".into());
338
339 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
340 assert_eq!(resolved.len(), 1);
341 assert_eq!(resolved["FOO"], "bar");
342 }
343
344 #[test]
345 fn resolve_secrets_no_escaping() {
346 let mut vault = empty_vault();
347 vault.schema.insert(
348 "KEY".into(),
349 types::SchemaEntry {
350 description: String::new(),
351 example: None,
352 tags: vec![],
353 },
354 );
355
356 let mut murk = empty_murk();
357 murk.values.insert("KEY".into(), "it's a test".into());
358
359 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
360 assert_eq!(resolved["KEY"], "it's a test");
361 }
362
363 #[test]
364 fn resolve_secrets_scoped_override() {
365 let mut vault = empty_vault();
366 vault.schema.insert(
367 "KEY".into(),
368 types::SchemaEntry {
369 description: String::new(),
370 example: None,
371 tags: vec![],
372 },
373 );
374
375 let mut murk = empty_murk();
376 murk.values.insert("KEY".into(), "shared".into());
377 let mut scoped = HashMap::new();
378 scoped.insert("age1pk".into(), "override".into());
379 murk.scoped.insert("KEY".into(), scoped);
380
381 let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
382 assert_eq!(resolved["KEY"], "override");
383 }
384
385 #[test]
386 fn resolve_secrets_tag_filter() {
387 let mut vault = empty_vault();
388 vault.schema.insert(
389 "A".into(),
390 types::SchemaEntry {
391 description: String::new(),
392 example: None,
393 tags: vec!["db".into()],
394 },
395 );
396 vault.schema.insert(
397 "B".into(),
398 types::SchemaEntry {
399 description: String::new(),
400 example: None,
401 tags: vec!["api".into()],
402 },
403 );
404
405 let mut murk = empty_murk();
406 murk.values.insert("A".into(), "val_a".into());
407 murk.values.insert("B".into(), "val_b".into());
408
409 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
410 assert_eq!(resolved.len(), 1);
411 assert_eq!(resolved["A"], "val_a");
412 }
413
414 #[test]
415 fn resolve_secrets_tag_in_schema_but_no_secret() {
416 let mut vault = empty_vault();
417 vault.schema.insert(
419 "ORPHAN".into(),
420 types::SchemaEntry {
421 description: "orphan key".into(),
422 example: None,
423 tags: vec!["db".into()],
424 },
425 );
426 vault.schema.insert(
427 "REAL".into(),
428 types::SchemaEntry {
429 description: "has a value".into(),
430 example: None,
431 tags: vec!["db".into()],
432 },
433 );
434
435 let mut murk = empty_murk();
436 murk.values.insert("REAL".into(), "real_val".into());
438
439 let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
440 assert_eq!(resolved.len(), 1);
442 assert_eq!(resolved["REAL"], "real_val");
443 assert!(!resolved.contains_key("ORPHAN"));
444 }
445
446 #[test]
447 fn resolve_secrets_scoped_pubkey_not_in_recipients() {
448 let mut vault = empty_vault();
449 vault.recipients = vec!["age1alice".into()];
450 vault.schema.insert(
451 "KEY".into(),
452 types::SchemaEntry {
453 description: String::new(),
454 example: None,
455 tags: vec![],
456 },
457 );
458
459 let mut murk = empty_murk();
460 murk.values.insert("KEY".into(), "shared".into());
461 let mut scoped = HashMap::new();
463 scoped.insert("age1outsider".into(), "outsider_val".into());
464 murk.scoped.insert("KEY".into(), scoped);
465
466 let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
468 assert_eq!(resolved["KEY"], "outsider_val");
469
470 let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
472 assert_eq!(resolved_alice["KEY"], "shared");
473 }
474
475 #[test]
478 fn export_secrets_empty_vault() {
479 let vault = empty_vault();
480 let murk = empty_murk();
481 let exports = export_secrets(&vault, &murk, "age1pk", &[]);
482 assert!(exports.is_empty());
483 }
484
485 #[test]
486 fn decrypt_vault_values_basic() {
487 let (secret, pubkey) = generate_keypair();
488 let recipient = make_recipient(&pubkey);
489 let identity = make_identity(&secret);
490
491 let mut vault = empty_vault();
492 vault.recipients = vec![pubkey];
493 vault.secrets.insert(
494 "KEY1".into(),
495 types::SecretEntry {
496 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
497 scoped: std::collections::BTreeMap::new(),
498 },
499 );
500 vault.secrets.insert(
501 "KEY2".into(),
502 types::SecretEntry {
503 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
504 scoped: std::collections::BTreeMap::new(),
505 },
506 );
507
508 let values = crate::export::decrypt_vault_values(&vault, &identity);
509 assert_eq!(values.len(), 2);
510 assert_eq!(values["KEY1"], "val1");
511 assert_eq!(values["KEY2"], "val2");
512 }
513
514 #[test]
515 fn decrypt_vault_values_wrong_key_skips() {
516 let (_, pubkey) = generate_keypair();
517 let recipient = make_recipient(&pubkey);
518 let (wrong_secret, _) = generate_keypair();
519 let wrong_identity = make_identity(&wrong_secret);
520
521 let mut vault = empty_vault();
522 vault.recipients = vec![pubkey];
523 vault.secrets.insert(
524 "KEY1".into(),
525 types::SecretEntry {
526 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
527 scoped: std::collections::BTreeMap::new(),
528 },
529 );
530
531 let values = crate::export::decrypt_vault_values(&vault, &wrong_identity);
532 assert!(values.is_empty());
533 }
534
535 #[test]
536 fn decrypt_vault_values_empty_vault() {
537 let (secret, _) = generate_keypair();
538 let identity = make_identity(&secret);
539 let vault = empty_vault();
540
541 let values = crate::export::decrypt_vault_values(&vault, &identity);
542 assert!(values.is_empty());
543 }
544
545 #[test]
546 fn diff_secrets_both_empty() {
547 let old = HashMap::new();
548 let new = HashMap::new();
549 assert!(diff_secrets(&old, &new).is_empty());
550 }
551
552 #[test]
555 fn parse_and_decrypt_values_roundtrip() {
556 let (secret, pubkey) = generate_keypair();
557 let recipient = make_recipient(&pubkey);
558 let identity = make_identity(&secret);
559
560 let mut vault = empty_vault();
561 vault.recipients = vec![pubkey];
562 vault.secrets.insert(
563 "KEY1".into(),
564 types::SecretEntry {
565 shared: crate::encrypt_value(b"val1", &[recipient.clone()]).unwrap(),
566 scoped: std::collections::BTreeMap::new(),
567 },
568 );
569 vault.secrets.insert(
570 "KEY2".into(),
571 types::SecretEntry {
572 shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
573 scoped: std::collections::BTreeMap::new(),
574 },
575 );
576
577 let json = serde_json::to_string(&vault).unwrap();
578 let values = parse_and_decrypt_values(&json, &identity).unwrap();
579 assert_eq!(values.len(), 2);
580 assert_eq!(values["KEY1"], "val1");
581 assert_eq!(values["KEY2"], "val2");
582 }
583
584 #[test]
585 fn parse_and_decrypt_values_invalid_json() {
586 let (secret, _) = generate_keypair();
587 let identity = make_identity(&secret);
588
589 let result = parse_and_decrypt_values("not valid json", &identity);
590 assert!(result.is_err());
591 }
592
593 #[test]
594 fn parse_and_decrypt_values_wrong_key() {
595 let (_, pubkey) = generate_keypair();
596 let recipient = make_recipient(&pubkey);
597 let (wrong_secret, _) = generate_keypair();
598 let wrong_identity = make_identity(&wrong_secret);
599
600 let mut vault = empty_vault();
601 vault.recipients = vec![pubkey];
602 vault.secrets.insert(
603 "KEY1".into(),
604 types::SecretEntry {
605 shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
606 scoped: std::collections::BTreeMap::new(),
607 },
608 );
609
610 let json = serde_json::to_string(&vault).unwrap();
611 let values = parse_and_decrypt_values(&json, &wrong_identity).unwrap();
612 assert!(values.is_empty());
613 }
614}