1use zeroize::Zeroizing;
4
5use crate::{crypto, now_utc, types};
6
7pub fn add_secret(
11 vault: &mut types::Vault,
12 murk: &mut types::Murk,
13 key: &str,
14 value: &str,
15 desc: Option<&str>,
16 scoped: bool,
17 tags: &[String],
18 identity: &crypto::MurkIdentity,
19) -> bool {
20 if scoped {
21 let pubkey = identity.pubkey_string().expect("valid identity has pubkey");
22 murk.scoped
23 .entry(key.into())
24 .or_default()
25 .insert(pubkey, Zeroizing::new(value.to_owned()));
26 } else {
27 murk.values
28 .insert(key.into(), Zeroizing::new(value.to_owned()));
29 }
30
31 let is_new = !vault.schema.contains_key(key);
32
33 let now = now_utc();
34 if let Some(entry) = vault.schema.get_mut(key) {
35 if let Some(d) = desc {
36 entry.description = d.into();
37 }
38 if !tags.is_empty() {
39 for t in tags {
40 if !entry.tags.contains(t) {
41 entry.tags.push(t.clone());
42 }
43 }
44 }
45 entry.updated = Some(now);
46 } else {
47 vault.schema.insert(
48 key.into(),
49 types::SchemaEntry {
50 description: desc.unwrap_or("").into(),
51 example: None,
52 tags: tags.to_vec(),
53 created: Some(now.clone()),
54 updated: Some(now),
55 },
56 );
57 }
58
59 is_new && desc.is_none()
60}
61
62pub fn remove_secret(vault: &mut types::Vault, murk: &mut types::Murk, key: &str) {
64 murk.values.remove(key);
65 murk.scoped.remove(key);
66 vault.schema.remove(key);
67}
68
69pub fn get_secret<'a>(murk: &'a types::Murk, key: &str, pubkey: &str) -> Option<&'a str> {
71 if let Some(value) = murk.scoped.get(key).and_then(|m| m.get(pubkey)) {
72 return Some(value.as_str());
73 }
74 murk.values.get(key).map(|v| v.as_str())
75}
76
77pub fn list_keys<'a>(vault: &'a types::Vault, tags: &[String]) -> Vec<&'a str> {
79 vault
80 .schema
81 .iter()
82 .filter(|(_, entry)| tags.is_empty() || entry.tags.iter().any(|t| tags.contains(t)))
83 .map(|(key, _)| key.as_str())
84 .collect()
85}
86
87pub fn import_secrets(
95 vault: &mut types::Vault,
96 murk: &mut types::Murk,
97 pairs: &[(String, Zeroizing<String>)],
98) -> Vec<String> {
99 let now = now_utc();
100 let mut imported = Vec::new();
101 for (key, value) in pairs {
102 murk.values.insert(key.clone(), value.clone());
103
104 if let Some(entry) = vault.schema.get_mut(key.as_str()) {
105 entry.updated = Some(now.clone());
106 } else {
107 vault.schema.insert(
108 key.clone(),
109 types::SchemaEntry {
110 description: String::new(),
111 example: None,
112 tags: vec![],
113 created: Some(now.clone()),
114 updated: Some(now.clone()),
115 },
116 );
117 }
118
119 imported.push(key.clone());
120 }
121 imported
122}
123
124pub fn describe_key(
126 vault: &mut types::Vault,
127 key: &str,
128 description: &str,
129 example: Option<&str>,
130 tags: &[String],
131) {
132 if let Some(entry) = vault.schema.get_mut(key) {
133 entry.description = description.into();
134 entry.example = example.map(Into::into);
135 if !tags.is_empty() {
136 entry.tags = tags.to_vec();
137 }
138 } else {
139 let now = now_utc();
140 vault.schema.insert(
141 key.into(),
142 types::SchemaEntry {
143 description: description.into(),
144 example: example.map(Into::into),
145 tags: tags.to_vec(),
146 created: Some(now.clone()),
147 updated: Some(now),
148 },
149 );
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::testutil::*;
157 use std::collections::HashMap;
158
159 #[test]
160 fn add_secret_shared() {
161 let (secret, _) = generate_keypair();
162 let identity = make_identity(&secret);
163 let mut vault = empty_vault();
164 let mut murk = empty_murk();
165
166 let needs_hint = add_secret(
167 &mut vault,
168 &mut murk,
169 "KEY",
170 "value",
171 None,
172 false,
173 &[],
174 &identity,
175 );
176
177 assert!(needs_hint);
178 assert_eq!(murk.values["KEY"].as_str(), "value");
179 assert!(vault.schema.contains_key("KEY"));
180 assert!(vault.schema["KEY"].description.is_empty());
181 }
182
183 #[test]
184 fn add_secret_with_description() {
185 let (secret, _) = generate_keypair();
186 let identity = make_identity(&secret);
187 let mut vault = empty_vault();
188 let mut murk = empty_murk();
189
190 let needs_hint = add_secret(
191 &mut vault,
192 &mut murk,
193 "KEY",
194 "value",
195 Some("a desc"),
196 false,
197 &[],
198 &identity,
199 );
200
201 assert!(!needs_hint);
202 assert_eq!(vault.schema["KEY"].description, "a desc");
203 }
204
205 #[test]
206 fn add_secret_scoped() {
207 let (secret, pubkey) = generate_keypair();
208 let identity = make_identity(&secret);
209 let mut vault = empty_vault();
210 let mut murk = empty_murk();
211
212 add_secret(
213 &mut vault,
214 &mut murk,
215 "KEY",
216 "scoped_val",
217 None,
218 true,
219 &[],
220 &identity,
221 );
222
223 assert!(!murk.values.contains_key("KEY"));
224 assert_eq!(murk.scoped["KEY"][&pubkey].as_str(), "scoped_val");
225 }
226
227 #[test]
228 fn add_secret_merges_tags() {
229 let (secret, _) = generate_keypair();
230 let identity = make_identity(&secret);
231 let mut vault = empty_vault();
232 let mut murk = empty_murk();
233
234 let tags1 = vec!["db".into()];
235 add_secret(
236 &mut vault, &mut murk, "KEY", "v1", None, false, &tags1, &identity,
237 );
238 assert_eq!(vault.schema["KEY"].tags, vec!["db"]);
239
240 let tags2 = vec!["backend".into()];
241 add_secret(
242 &mut vault, &mut murk, "KEY", "v2", None, false, &tags2, &identity,
243 );
244 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
245
246 let tags3 = vec!["db".into()];
248 add_secret(
249 &mut vault, &mut murk, "KEY", "v3", None, false, &tags3, &identity,
250 );
251 assert_eq!(vault.schema["KEY"].tags, vec!["db", "backend"]);
252 }
253
254 #[test]
255 fn add_secret_updates_existing_desc() {
256 let (secret, _) = generate_keypair();
257 let identity = make_identity(&secret);
258 let mut vault = empty_vault();
259 let mut murk = empty_murk();
260
261 add_secret(
262 &mut vault,
263 &mut murk,
264 "KEY",
265 "v1",
266 Some("old"),
267 false,
268 &[],
269 &identity,
270 );
271 add_secret(
272 &mut vault,
273 &mut murk,
274 "KEY",
275 "v2",
276 Some("new"),
277 false,
278 &[],
279 &identity,
280 );
281 assert_eq!(vault.schema["KEY"].description, "new");
282 }
283
284 #[test]
285 fn remove_secret_clears_all() {
286 let mut vault = empty_vault();
287 vault.schema.insert(
288 "KEY".into(),
289 types::SchemaEntry {
290 description: "desc".into(),
291 example: None,
292 tags: vec![],
293 ..Default::default()
294 },
295 );
296 let mut murk = empty_murk();
297 murk.values.insert("KEY".into(), secret("val"));
298 let mut scoped = HashMap::new();
299 scoped.insert("age1pk".into(), secret("scoped_val"));
300 murk.scoped.insert("KEY".into(), scoped);
301
302 remove_secret(&mut vault, &mut murk, "KEY");
303
304 assert!(!murk.values.contains_key("KEY"));
305 assert!(!murk.scoped.contains_key("KEY"));
306 assert!(!vault.schema.contains_key("KEY"));
307 }
308
309 #[test]
310 fn get_secret_shared_value() {
311 let mut murk = empty_murk();
312 murk.values.insert("KEY".into(), secret("shared_val"));
313
314 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("shared_val"));
315 }
316
317 #[test]
318 fn get_secret_scoped_overrides_shared() {
319 let mut murk = empty_murk();
320 murk.values.insert("KEY".into(), secret("shared_val"));
321 let mut scoped = HashMap::new();
322 scoped.insert("age1pk".into(), secret("scoped_val"));
323 murk.scoped.insert("KEY".into(), scoped);
324
325 assert_eq!(get_secret(&murk, "KEY", "age1pk"), Some("scoped_val"));
326 }
327
328 #[test]
329 fn get_secret_missing_returns_none() {
330 let murk = empty_murk();
331 assert_eq!(get_secret(&murk, "NONEXISTENT", "age1pk"), None);
332 }
333
334 #[test]
335 fn list_keys_no_filter() {
336 let mut vault = empty_vault();
337 vault.schema.insert(
338 "A".into(),
339 types::SchemaEntry {
340 description: String::new(),
341 example: None,
342 tags: vec![],
343 ..Default::default()
344 },
345 );
346 vault.schema.insert(
347 "B".into(),
348 types::SchemaEntry {
349 description: String::new(),
350 example: None,
351 tags: vec![],
352 ..Default::default()
353 },
354 );
355
356 let keys = list_keys(&vault, &[]);
357 assert_eq!(keys, vec!["A", "B"]);
358 }
359
360 #[test]
361 fn list_keys_with_tag_filter() {
362 let mut vault = empty_vault();
363 vault.schema.insert(
364 "A".into(),
365 types::SchemaEntry {
366 description: String::new(),
367 example: None,
368 tags: vec!["db".into()],
369 ..Default::default()
370 },
371 );
372 vault.schema.insert(
373 "B".into(),
374 types::SchemaEntry {
375 description: String::new(),
376 example: None,
377 tags: vec!["api".into()],
378 ..Default::default()
379 },
380 );
381 vault.schema.insert(
382 "C".into(),
383 types::SchemaEntry {
384 description: String::new(),
385 example: None,
386 tags: vec![],
387 ..Default::default()
388 },
389 );
390
391 let keys = list_keys(&vault, &["db".into()]);
392 assert_eq!(keys, vec!["A"]);
393 }
394
395 #[test]
396 fn list_keys_no_matches() {
397 let mut vault = empty_vault();
398 vault.schema.insert(
399 "A".into(),
400 types::SchemaEntry {
401 description: String::new(),
402 example: None,
403 tags: vec!["db".into()],
404 ..Default::default()
405 },
406 );
407
408 let keys = list_keys(&vault, &["nonexistent".into()]);
409 assert!(keys.is_empty());
410 }
411
412 #[test]
413 fn describe_key_creates_new() {
414 let mut vault = empty_vault();
415 describe_key(
416 &mut vault,
417 "KEY",
418 "a description",
419 Some("example"),
420 &["tag".into()],
421 );
422
423 assert_eq!(vault.schema["KEY"].description, "a description");
424 assert_eq!(vault.schema["KEY"].example.as_deref(), Some("example"));
425 assert_eq!(vault.schema["KEY"].tags, vec!["tag"]);
426 }
427
428 #[test]
429 fn describe_key_updates_existing() {
430 let mut vault = empty_vault();
431 vault.schema.insert(
432 "KEY".into(),
433 types::SchemaEntry {
434 description: "old".into(),
435 example: Some("old_ex".into()),
436 tags: vec!["old_tag".into()],
437 ..Default::default()
438 },
439 );
440
441 describe_key(&mut vault, "KEY", "new", None, &["new_tag".into()]);
442
443 assert_eq!(vault.schema["KEY"].description, "new");
444 assert_eq!(vault.schema["KEY"].example, None);
445 assert_eq!(vault.schema["KEY"].tags, vec!["new_tag"]);
446 }
447
448 #[test]
449 fn describe_key_preserves_tags_if_empty() {
450 let mut vault = empty_vault();
451 vault.schema.insert(
452 "KEY".into(),
453 types::SchemaEntry {
454 description: "old".into(),
455 example: None,
456 tags: vec!["keep".into()],
457 ..Default::default()
458 },
459 );
460
461 describe_key(&mut vault, "KEY", "new desc", None, &[]);
462
463 assert_eq!(vault.schema["KEY"].tags, vec!["keep"]);
464 }
465
466 #[test]
469 fn add_secret_overwrite_shared_with_scoped() {
470 let (secret, pubkey) = generate_keypair();
471 let identity = make_identity(&secret);
472 let mut vault = empty_vault();
473 let mut murk = empty_murk();
474
475 add_secret(
476 &mut vault,
477 &mut murk,
478 "KEY",
479 "shared_val",
480 None,
481 false,
482 &[],
483 &identity,
484 );
485 assert_eq!(murk.values["KEY"].as_str(), "shared_val");
486
487 add_secret(
488 &mut vault,
489 &mut murk,
490 "KEY",
491 "scoped_val",
492 None,
493 true,
494 &[],
495 &identity,
496 );
497 assert_eq!(murk.values["KEY"].as_str(), "shared_val");
499 assert_eq!(murk.scoped["KEY"][&pubkey].as_str(), "scoped_val");
500 }
501
502 #[test]
503 fn add_secret_empty_value() {
504 let (secret, _) = generate_keypair();
505 let identity = make_identity(&secret);
506 let mut vault = empty_vault();
507 let mut murk = empty_murk();
508
509 add_secret(
510 &mut vault,
511 &mut murk,
512 "KEY",
513 "",
514 None,
515 false,
516 &[],
517 &identity,
518 );
519 assert_eq!(murk.values["KEY"].as_str(), "");
520 }
521
522 #[test]
523 fn import_secrets_basic() {
524 let mut vault = empty_vault();
525 let mut murk = empty_murk();
526
527 let pairs = vec![
528 ("KEY1".into(), Zeroizing::new("val1".into())),
529 ("KEY2".into(), Zeroizing::new("val2".into())),
530 ];
531 let imported = import_secrets(&mut vault, &mut murk, &pairs);
532
533 assert_eq!(imported, vec!["KEY1", "KEY2"]);
534 assert_eq!(murk.values["KEY1"].as_str(), "val1");
535 assert_eq!(murk.values["KEY2"].as_str(), "val2");
536 assert!(vault.schema.contains_key("KEY1"));
537 assert!(vault.schema.contains_key("KEY2"));
538 }
539
540 #[test]
541 fn import_secrets_existing_schema_preserved() {
542 let mut vault = empty_vault();
543 vault.schema.insert(
544 "KEY1".into(),
545 types::SchemaEntry {
546 description: "existing desc".into(),
547 example: Some("ex".into()),
548 tags: vec!["tag".into()],
549 ..Default::default()
550 },
551 );
552 let mut murk = empty_murk();
553
554 let pairs = vec![("KEY1".into(), Zeroizing::new("new_val".into()))];
555 import_secrets(&mut vault, &mut murk, &pairs);
556
557 assert_eq!(murk.values["KEY1"].as_str(), "new_val");
558 assert_eq!(vault.schema["KEY1"].description, "existing desc");
559 }
560
561 #[test]
562 fn import_secrets_empty() {
563 let mut vault = empty_vault();
564 let mut murk = empty_murk();
565 let imported = import_secrets(&mut vault, &mut murk, &[]);
566 assert!(imported.is_empty());
567 }
568
569 #[test]
570 fn remove_secret_nonexistent() {
571 let mut vault = empty_vault();
572 let mut murk = empty_murk();
573
574 remove_secret(&mut vault, &mut murk, "NONEXISTENT");
576 }
577}