bc_envelope/base/elide.rs
1use std::collections::HashSet;
2
3use bc_components::{Digest, DigestProvider};
4#[cfg(feature = "encrypt")]
5use bc_components::{Nonce, SymmetricKey};
6#[cfg(feature = "encrypt")]
7use dcbor::prelude::*;
8
9use super::envelope::EnvelopeCase;
10use crate::{Assertion, Envelope, Error, Result};
11
12/// Types of obscuration that can be applied to envelope elements.
13///
14/// This enum identifies the different ways an envelope element can be obscured.
15/// Unlike `ObscureAction` which is used to perform obscuration operations,
16/// `ObscureType` is used to identify and filter elements based on their
17/// obscuration state.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ObscureType {
20 /// The element has been elided, showing only its digest.
21 Elided,
22
23 /// The element has been encrypted using symmetric encryption.
24 ///
25 /// This variant is only available when the `encrypt` feature is enabled.
26 #[cfg(feature = "encrypt")]
27 Encrypted,
28
29 /// The element has been compressed to reduce its size.
30 ///
31 /// This variant is only available when the `compress` feature is enabled.
32 #[cfg(feature = "compress")]
33 Compressed,
34}
35
36/// Actions that can be performed on parts of an envelope to obscure them.
37///
38/// Gordian Envelope supports several ways to obscure parts of an envelope while
39/// maintaining its semantic integrity and digest tree. This enum defines the
40/// possible actions that can be taken when obscuring envelope elements.
41///
42/// Obscuring parts of an envelope is a key feature for privacy and selective
43/// disclosure, allowing the holder of an envelope to share only specific parts
44/// while hiding, encrypting, or compressing others.
45pub enum ObscureAction {
46 /// Elide the target, leaving only its digest.
47 ///
48 /// Elision replaces the targeted envelope element with just its digest,
49 /// hiding its actual content while maintaining the integrity of the
50 /// envelope's digest tree. This is the most basic form of selective
51 /// disclosure.
52 ///
53 /// Elided elements can be revealed later by providing the original unelided
54 /// envelope to the recipient, who can verify that the revealed content
55 /// matches the digest in the elided version.
56 Elide,
57
58 /// Encrypt the target using the specified symmetric key.
59 ///
60 /// This encrypts the targeted envelope element using authenticated
61 /// encryption with the provided key. The encrypted content can only be
62 /// accessed by those who possess the symmetric key.
63 ///
64 /// This action is only available when the `encrypt` feature is enabled.
65 #[cfg(feature = "encrypt")]
66 Encrypt(SymmetricKey),
67
68 /// Compress the target using a compression algorithm.
69 ///
70 /// This compresses the targeted envelope element to reduce its size while
71 /// still allowing it to be decompressed by any recipient. Unlike elision or
72 /// encryption, compression doesn't provide privacy but can reduce the size
73 /// of large envelope components.
74 ///
75 /// This action is only available when the `compress` feature is enabled.
76 #[cfg(feature = "compress")]
77 Compress,
78}
79
80/// Support for eliding elements from envelopes.
81///
82/// This includes eliding, encrypting and compressing (obscuring) elements.
83impl Envelope {
84 /// Returns the elided variant of this envelope.
85 ///
86 /// Elision replaces an envelope with just its digest, hiding its content
87 /// while maintaining the integrity of the envelope's digest tree. This
88 /// is a fundamental privacy feature of Gordian Envelope that enables
89 /// selective disclosure.
90 ///
91 /// Returns the same envelope if it is already elided.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// # use bc_envelope::prelude::*;
97 /// # use indoc::indoc;
98 /// let envelope = Envelope::new("Hello.");
99 /// let elided = envelope.elide();
100 ///
101 /// // The elided envelope shows only "ELIDED" in formatting
102 /// assert_eq!(elided.format_flat(), "ELIDED");
103 ///
104 /// // But it maintains the same digest as the original
105 /// assert!(envelope.is_equivalent_to(&elided));
106 /// ```
107 pub fn elide(&self) -> Self {
108 match self.case() {
109 EnvelopeCase::Elided(_) => self.clone(),
110 _ => Self::new_elided(self.digest()),
111 }
112 }
113
114 /// Returns a version of this envelope with elements in the `target` set
115 /// elided.
116 ///
117 /// This function obscures elements in the envelope whose digests are in the
118 /// provided target set, applying the specified action (elision,
119 /// encryption, or compression) to those elements while leaving other
120 /// elements intact.
121 ///
122 /// # Parameters
123 ///
124 /// * `target` - The set of digests that identify elements to be obscured
125 /// * `action` - The action to perform on the targeted elements (elide,
126 /// encrypt, or compress)
127 ///
128 /// # Examples
129 ///
130 /// ```
131 /// # use bc_envelope::prelude::*;
132 /// # use std::collections::HashSet;
133 /// let envelope = Envelope::new("Alice")
134 /// .add_assertion("knows", "Bob")
135 /// .add_assertion("livesAt", "123 Main St.");
136 ///
137 /// // Create a set of digests targeting the "livesAt" assertion
138 /// let mut target = HashSet::new();
139 /// let livesAt_assertion = envelope.assertion_with_predicate("livesAt").unwrap();
140 /// target.insert(livesAt_assertion.digest());
141 ///
142 /// // Elide that specific assertion
143 /// let elided = envelope.elide_removing_set_with_action(&target, &ObscureAction::Elide);
144 ///
145 /// // The result will have the "livesAt" assertion elided but "knows" still visible
146 /// ```
147 pub fn elide_removing_set_with_action(
148 &self,
149 target: &HashSet<Digest>,
150 action: &ObscureAction,
151 ) -> Self {
152 self.elide_set_with_action(target, false, action)
153 }
154
155 /// Returns a version of this envelope with elements in the `target` set
156 /// elided.
157 ///
158 /// This is a convenience function that calls `elide_set` with
159 /// `is_revealing` set to `false`, using the standard elision action.
160 /// Use this when you want to simply elide elements rather than encrypt
161 /// or compress them.
162 ///
163 /// # Parameters
164 ///
165 /// * `target` - The set of digests that identify elements to be elided
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// # use bc_envelope::prelude::*;
171 /// # use std::collections::HashSet;
172 /// let envelope = Envelope::new("Alice")
173 /// .add_assertion("knows", "Bob")
174 /// .add_assertion("email", "alice@example.com");
175 ///
176 /// // Create a set of digests targeting the email assertion
177 /// let mut target = HashSet::new();
178 /// let email_assertion = envelope.assertion_with_predicate("email").unwrap();
179 /// target.insert(email_assertion.digest());
180 ///
181 /// // Elide the email assertion for privacy
182 /// let redacted = envelope.elide_removing_set(&target);
183 /// ```
184 pub fn elide_removing_set(&self, target: &HashSet<Digest>) -> Self {
185 self.elide_set(target, false)
186 }
187
188 /// Returns a version of this envelope with elements in the `target` set
189 /// elided.
190 ///
191 /// - Parameters:
192 /// - target: An array of `DigestProvider`s.
193 /// - action: Perform the specified action (elision, encryption or
194 /// compression).
195 ///
196 /// - Returns: The elided envelope.
197 pub fn elide_removing_array_with_action(
198 &self,
199 target: &[&dyn DigestProvider],
200 action: &ObscureAction,
201 ) -> Self {
202 self.elide_array_with_action(target, false, action)
203 }
204
205 /// Returns a version of this envelope with elements in the `target` set
206 /// elided.
207 ///
208 /// - Parameters:
209 /// - target: An array of `DigestProvider`s.
210 /// - action: Perform the specified action (elision, encryption or
211 /// compression).
212 ///
213 /// - Returns: The elided envelope.
214 pub fn elide_removing_array(&self, target: &[&dyn DigestProvider]) -> Self {
215 self.elide_array(target, false)
216 }
217
218 /// Returns a version of this envelope with the target element elided.
219 ///
220 /// - Parameters:
221 /// - target: A `DigestProvider`.
222 /// - action: Perform the specified action (elision, encryption or
223 /// compression).
224 ///
225 /// - Returns: The elided envelope.
226 pub fn elide_removing_target_with_action(
227 &self,
228 target: &dyn DigestProvider,
229 action: &ObscureAction,
230 ) -> Self {
231 self.elide_target_with_action(target, false, action)
232 }
233
234 /// Returns a version of this envelope with the target element elided.
235 ///
236 /// - Parameters:
237 /// - target: A `DigestProvider`.
238 ///
239 /// - Returns: The elided envelope.
240 pub fn elide_removing_target(&self, target: &dyn DigestProvider) -> Self {
241 self.elide_target(target, false)
242 }
243
244 /// Returns a version of this envelope with only elements in the `target`
245 /// set revealed, and all other elements elided.
246 ///
247 /// This function performs the opposite operation of
248 /// `elide_removing_set_with_action`. Instead of specifying which
249 /// elements to obscure, you specify which elements to reveal,
250 /// and everything else will be obscured using the specified action.
251 ///
252 /// This is particularly useful for selective disclosure where you want to
253 /// reveal only specific portions of an envelope while keeping the rest
254 /// private.
255 ///
256 /// # Parameters
257 ///
258 /// * `target` - The set of digests that identify elements to be revealed
259 /// * `action` - The action to perform on all other elements (elide,
260 /// encrypt, or compress)
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// # use bc_envelope::prelude::*;
266 /// # use std::collections::HashSet;
267 /// let envelope = Envelope::new("Alice")
268 /// .add_assertion("name", "Alice Smith")
269 /// .add_assertion("age", 30)
270 /// .add_assertion("ssn", "123-45-6789");
271 ///
272 /// // Create a set of digests for elements we want to reveal
273 /// let mut reveal_set = HashSet::new();
274 ///
275 /// // Add the subject and the name assertion to the set to reveal
276 /// reveal_set.insert(envelope.subject().digest());
277 /// reveal_set
278 /// .insert(envelope.assertion_with_predicate("name").unwrap().digest());
279 ///
280 /// // Create an envelope that only reveals name and hides age and SSN
281 /// let selective = envelope
282 /// .elide_revealing_set_with_action(&reveal_set, &ObscureAction::Elide);
283 /// ```
284 pub fn elide_revealing_set_with_action(
285 &self,
286 target: &HashSet<Digest>,
287 action: &ObscureAction,
288 ) -> Self {
289 self.elide_set_with_action(target, true, action)
290 }
291
292 /// Returns a version of this envelope with elements *not* in the `target`
293 /// set elided.
294 ///
295 /// - Parameters:
296 /// - target: The target set of digests.
297 ///
298 /// - Returns: The elided envelope.
299 pub fn elide_revealing_set(&self, target: &HashSet<Digest>) -> Self {
300 self.elide_set(target, true)
301 }
302
303 /// Returns a version of this envelope with elements *not* in the `target`
304 /// set elided.
305 ///
306 /// - Parameters:
307 /// - target: An array of `DigestProvider`s.
308 /// - action: Perform the specified action (elision, encryption or
309 /// compression).
310 ///
311 /// - Returns: The elided envelope.
312 pub fn elide_revealing_array_with_action(
313 &self,
314 target: &[&dyn DigestProvider],
315 action: &ObscureAction,
316 ) -> Self {
317 self.elide_array_with_action(target, true, action)
318 }
319
320 /// Returns a version of this envelope with elements *not* in the `target`
321 /// set elided.
322 ///
323 /// - Parameters:
324 /// - target: An array of `DigestProvider`s.
325 ///
326 /// - Returns: The elided envelope.
327 pub fn elide_revealing_array(
328 &self,
329 target: &[&dyn DigestProvider],
330 ) -> Self {
331 self.elide_array(target, true)
332 }
333
334 /// Returns a version of this envelope with all elements *except* the target
335 /// element elided.
336 ///
337 /// - Parameters:
338 /// - target: A `DigestProvider`.
339 /// - action: Perform the specified action (elision, encryption or
340 /// compression).
341 ///
342 /// - Returns: The elided envelope.
343 pub fn elide_revealing_target_with_action(
344 &self,
345 target: &dyn DigestProvider,
346 action: &ObscureAction,
347 ) -> Self {
348 self.elide_target_with_action(target, true, action)
349 }
350
351 /// Returns a version of this envelope with all elements *except* the target
352 /// element elided.
353 ///
354 /// - Parameters:
355 /// - target: A `DigestProvider`.
356 ///
357 /// - Returns: The elided envelope.
358 pub fn elide_revealing_target(&self, target: &dyn DigestProvider) -> Self {
359 self.elide_target(target, true)
360 }
361
362 // Target Matches isRevealing elide
363 // ----------------------------------------
364 // false false false
365 // false true true
366 // true false true
367 // true true false
368
369 /// Returns an elided version of this envelope.
370 ///
371 /// - Parameters:
372 /// - target: The target set of digests.
373 /// - isRevealing: If `true`, the target set contains the digests of the
374 /// elements to leave revealed. If it is `false`, the target set
375 /// contains the digests of the elements to elide.
376 /// - action: Perform the specified action (elision, encryption or
377 /// compression).
378 ///
379 /// - Returns: The elided envelope.
380 pub fn elide_set_with_action(
381 &self,
382 target: &HashSet<Digest>,
383 is_revealing: bool,
384 action: &ObscureAction,
385 ) -> Self {
386 let self_digest = self.digest();
387 if target.contains(&self_digest) != is_revealing {
388 match action {
389 ObscureAction::Elide => self.elide(),
390 #[cfg(feature = "encrypt")]
391 ObscureAction::Encrypt(key) => {
392 let message = key.encrypt_with_digest(
393 self.tagged_cbor().to_cbor_data(),
394 self_digest,
395 None::<Nonce>,
396 );
397 Self::new_with_encrypted(message).unwrap()
398 }
399 #[cfg(feature = "compress")]
400 ObscureAction::Compress => self.compress().unwrap(),
401 }
402 } else if let EnvelopeCase::Assertion(assertion) = self.case() {
403 let predicate = assertion.predicate().elide_set_with_action(
404 target,
405 is_revealing,
406 action,
407 );
408 let object = assertion.object().elide_set_with_action(
409 target,
410 is_revealing,
411 action,
412 );
413 let elided_assertion = Assertion::new(predicate, object);
414 assert!(&elided_assertion == assertion);
415 Self::new_with_assertion(elided_assertion)
416 } else if let EnvelopeCase::Node { subject, assertions, .. } =
417 self.case()
418 {
419 let elided_subject =
420 subject.elide_set_with_action(target, is_revealing, action);
421 assert!(elided_subject.digest() == subject.digest());
422 let elided_assertions = assertions
423 .iter()
424 .map(|assertion| {
425 let elided_assertion = assertion.elide_set_with_action(
426 target,
427 is_revealing,
428 action,
429 );
430 assert!(elided_assertion.digest() == assertion.digest());
431 elided_assertion
432 })
433 .collect();
434 Self::new_with_unchecked_assertions(
435 elided_subject,
436 elided_assertions,
437 )
438 } else if let EnvelopeCase::Wrapped { envelope, .. } = self.case() {
439 let elided_envelope =
440 envelope.elide_set_with_action(target, is_revealing, action);
441 assert!(elided_envelope.digest() == envelope.digest());
442 Self::new_wrapped(elided_envelope)
443 } else {
444 self.clone()
445 }
446 }
447
448 /// Returns an elided version of this envelope.
449 ///
450 /// - Parameters:
451 /// - target: The target set of digests.
452 /// - isRevealing: If `true`, the target set contains the digests of the
453 /// elements to leave revealed. If it is `false`, the target set
454 /// contains the digests of the elements to elide.
455 ///
456 /// - Returns: The elided envelope.
457 pub fn elide_set(
458 &self,
459 target: &HashSet<Digest>,
460 is_revealing: bool,
461 ) -> Self {
462 self.elide_set_with_action(target, is_revealing, &ObscureAction::Elide)
463 }
464
465 /// Returns an elided version of this envelope.
466 ///
467 /// - Parameters:
468 /// - target: An array of `DigestProvider`s.
469 /// - isRevealing: If `true`, the target set contains the digests of the
470 /// elements to leave revealed. If it is `false`, the target set
471 /// contains the digests of the elements to elide.
472 /// - action: Perform the specified action (elision, encryption or
473 /// compression).
474 ///
475 /// - Returns: The elided envelope.
476 pub fn elide_array_with_action(
477 &self,
478 target: &[&dyn DigestProvider],
479 is_revealing: bool,
480 action: &ObscureAction,
481 ) -> Self {
482 self.elide_set_with_action(
483 &target.iter().map(|provider| provider.digest()).collect(),
484 is_revealing,
485 action,
486 )
487 }
488
489 /// Returns an elided version of this envelope.
490 ///
491 /// - Parameters:
492 /// - target: An array of `DigestProvider`s.
493 /// - isRevealing: If `true`, the target set contains the digests of the
494 /// elements to leave revealed. If it is `false`, the target set
495 /// contains the digests of the elements to elide.
496 ///
497 /// - Returns: The elided envelope.
498 pub fn elide_array(
499 &self,
500 target: &[&dyn DigestProvider],
501 is_revealing: bool,
502 ) -> Self {
503 self.elide_array_with_action(
504 target,
505 is_revealing,
506 &ObscureAction::Elide,
507 )
508 }
509
510 /// Returns an elided version of this envelope.
511 ///
512 /// - Parameters:
513 /// - target: A `DigestProvider`.
514 /// - isRevealing: If `true`, the target is the element to leave revealed,
515 /// eliding all others. If it is `false`, the target is the element to
516 /// elide, leaving all others revealed.
517 /// - action: Perform the specified action (elision, encryption or
518 /// compression).
519 ///
520 /// - Returns: The elided envelope.
521 pub fn elide_target_with_action(
522 &self,
523 target: &dyn DigestProvider,
524 is_revealing: bool,
525 action: &ObscureAction,
526 ) -> Self {
527 self.elide_array_with_action(&[target], is_revealing, action)
528 }
529
530 /// Returns an elided version of this envelope.
531 ///
532 /// - Parameters:
533 /// - target: A `DigestProvider`.
534 /// - isRevealing: If `true`, the target is the element to leave revealed,
535 /// eliding all others. If it is `false`, the target is the element to
536 /// elide, leaving all others revealed.
537 ///
538 /// - Returns: The elided envelope.
539 pub fn elide_target(
540 &self,
541 target: &dyn DigestProvider,
542 is_revealing: bool,
543 ) -> Self {
544 self.elide_target_with_action(
545 target,
546 is_revealing,
547 &ObscureAction::Elide,
548 )
549 }
550
551 /// Returns the unelided variant of this envelope by revealing the original
552 /// content.
553 ///
554 /// This function allows restoring an elided envelope to its original form,
555 /// but only if the provided envelope's digest matches the elided
556 /// envelope's digest. This ensures the integrity of the revealed
557 /// content.
558 ///
559 /// Returns the same envelope if it is already unelided.
560 ///
561 /// # Errors
562 ///
563 /// Returns `EnvelopeError::InvalidDigest` if the provided envelope's digest
564 /// doesn't match the current envelope's digest.
565 ///
566 /// # Examples
567 ///
568 /// ```
569 /// # use bc_envelope::prelude::*;
570 /// let original = Envelope::new("Hello.");
571 /// let elided = original.elide();
572 ///
573 /// // Later, we can unelide the envelope if we have the original
574 /// let revealed = elided.unelide(&original).unwrap();
575 /// assert_eq!(revealed.format(), "\"Hello.\"");
576 ///
577 /// // Attempting to unelide with a different envelope will fail
578 /// let different = Envelope::new("Different");
579 /// assert!(elided.unelide(&different).is_err());
580 /// ```
581 pub fn unelide(&self, envelope: impl Into<Envelope>) -> Result<Self> {
582 let envelope = envelope.into();
583 if self.digest() == envelope.digest() {
584 Ok(envelope)
585 } else {
586 Err(Error::InvalidDigest)
587 }
588 }
589
590 /// Returns the set of digests of nodes matching the specified criteria.
591 ///
592 /// This function walks the envelope hierarchy and returns digests of nodes
593 /// that match both:
594 /// - The optional target digest set (if provided; otherwise all nodes
595 /// match)
596 /// - Any of the specified obscuration types
597 ///
598 /// If no obscuration types are provided, all nodes in the target set (or
599 /// all nodes if no target set) are returned.
600 ///
601 /// # Parameters
602 ///
603 /// * `target_digests` - Optional set of digests to filter by. If `None`,
604 /// all nodes are considered for matching.
605 /// * `obscure_types` - Slice of `ObscureType` values to match against. Only
606 /// nodes obscured in one of these ways will be included.
607 ///
608 /// # Returns
609 ///
610 /// A `HashSet` of digests for nodes matching the specified criteria.
611 ///
612 /// # Examples
613 ///
614 /// ```
615 /// # use bc_envelope::prelude::*;
616 /// # use std::collections::HashSet;
617 /// let envelope = Envelope::new("Alice")
618 /// .add_assertion("knows", "Bob")
619 /// .add_assertion("age", 30);
620 ///
621 /// // Elide one assertion
622 /// let knows_digest =
623 /// envelope.assertion_with_predicate("knows").unwrap().digest();
624 /// let mut target = HashSet::new();
625 /// target.insert(knows_digest.clone());
626 ///
627 /// let elided = envelope.elide_removing_set(&target);
628 ///
629 /// // Find all elided nodes
630 /// let elided_digests = elided.nodes_matching(None, &[ObscureType::Elided]);
631 /// assert!(elided_digests.contains(&knows_digest));
632 /// ```
633 pub fn nodes_matching(
634 &self,
635 target_digests: Option<&HashSet<Digest>>,
636 obscure_types: &[ObscureType],
637 ) -> HashSet<Digest> {
638 use std::cell::RefCell;
639
640 use super::walk::EdgeType;
641
642 let result = RefCell::new(HashSet::new());
643
644 let visitor = |envelope: &Envelope,
645 _level: usize,
646 _edge: EdgeType,
647 _state: ()|
648 -> ((), bool) {
649 // Check if this node matches the target digests (or if no target
650 // specified)
651 let digest_matches = target_digests
652 .map(|targets| targets.contains(&envelope.digest()))
653 .unwrap_or(true);
654
655 if !digest_matches {
656 return ((), false);
657 }
658
659 // If no obscure types specified, include all nodes
660 if obscure_types.is_empty() {
661 result.borrow_mut().insert(envelope.digest());
662 return ((), false);
663 }
664
665 // Check if this node matches any of the specified obscure types
666 let type_matches =
667 obscure_types.iter().any(|obscure_type| {
668 match (obscure_type, envelope.case()) {
669 (ObscureType::Elided, EnvelopeCase::Elided(_)) => true,
670 #[cfg(feature = "encrypt")]
671 (
672 ObscureType::Encrypted,
673 EnvelopeCase::Encrypted(_),
674 ) => true,
675 #[cfg(feature = "compress")]
676 (
677 ObscureType::Compressed,
678 EnvelopeCase::Compressed(_),
679 ) => true,
680 _ => false,
681 }
682 });
683
684 if type_matches {
685 result.borrow_mut().insert(envelope.digest());
686 }
687
688 ((), false)
689 };
690
691 self.walk(false, (), &visitor);
692 result.into_inner()
693 }
694
695 /// Returns a new envelope with elided nodes restored from the provided set.
696 ///
697 /// This function walks the envelope hierarchy and attempts to restore any
698 /// elided nodes by matching their digests against the provided set of
699 /// envelopes. If a match is found, the elided node is replaced with the
700 /// matching envelope.
701 ///
702 /// If no matches are found, the original envelope is returned unchanged.
703 ///
704 /// # Parameters
705 ///
706 /// * `envelopes` - A slice of envelopes that may match elided nodes in
707 /// `self`
708 ///
709 /// # Returns
710 ///
711 /// A new envelope with elided nodes restored where possible.
712 ///
713 /// # Examples
714 ///
715 /// ```
716 /// # use bc_envelope::prelude::*;
717 /// let alice = Envelope::new("Alice");
718 /// let bob = Envelope::new("Bob");
719 /// let envelope = Envelope::new("Alice").add_assertion("knows", "Bob");
720 ///
721 /// // Elide both the subject and an assertion
722 /// let elided = envelope
723 /// .elide_removing_target(&alice)
724 /// .elide_removing_target(&bob);
725 ///
726 /// // Restore the elided nodes
727 /// let restored = elided.walk_unelide(&[alice, bob]);
728 ///
729 /// // The restored envelope should match the original
730 /// assert_eq!(restored.format(), envelope.format());
731 /// ```
732 pub fn walk_unelide(&self, envelopes: &[Envelope]) -> Self {
733 use std::collections::HashMap;
734
735 // Build a lookup map of digest -> envelope
736 let mut envelope_map = HashMap::new();
737 for envelope in envelopes {
738 envelope_map.insert(envelope.digest(), envelope.clone());
739 }
740
741 self.walk_unelide_with_map(&envelope_map)
742 }
743
744 fn walk_unelide_with_map(
745 &self,
746 envelope_map: &std::collections::HashMap<Digest, Envelope>,
747 ) -> Self {
748 match self.case() {
749 EnvelopeCase::Elided(_) => {
750 // Try to find a matching envelope to restore
751 if let Some(replacement) = envelope_map.get(&self.digest()) {
752 replacement.clone()
753 } else {
754 self.clone()
755 }
756 }
757 EnvelopeCase::Node { subject, assertions, .. } => {
758 // Recursively unelide subject and assertions
759 let new_subject = subject.walk_unelide_with_map(envelope_map);
760 let new_assertions: Vec<_> = assertions
761 .iter()
762 .map(|a| a.walk_unelide_with_map(envelope_map))
763 .collect();
764
765 if new_subject.is_identical_to(subject)
766 && new_assertions
767 .iter()
768 .zip(assertions.iter())
769 .all(|(a, b)| a.is_identical_to(b))
770 {
771 self.clone()
772 } else {
773 Self::new_with_unchecked_assertions(
774 new_subject,
775 new_assertions,
776 )
777 }
778 }
779 EnvelopeCase::Wrapped { envelope, .. } => {
780 let new_envelope = envelope.walk_unelide_with_map(envelope_map);
781 if new_envelope.is_identical_to(envelope) {
782 self.clone()
783 } else {
784 new_envelope.wrap()
785 }
786 }
787 EnvelopeCase::Assertion(assertion) => {
788 // Recursively unelide predicate and object
789 let new_predicate =
790 assertion.predicate().walk_unelide_with_map(envelope_map);
791 let new_object =
792 assertion.object().walk_unelide_with_map(envelope_map);
793
794 if new_predicate.is_identical_to(&assertion.predicate())
795 && new_object.is_identical_to(&assertion.object())
796 {
797 self.clone()
798 } else {
799 Envelope::new_assertion(new_predicate, new_object)
800 }
801 }
802 _ => self.clone(),
803 }
804 }
805
806 /// Returns a new envelope with nodes matching target digests replaced.
807 ///
808 /// This function walks the envelope hierarchy and replaces any nodes whose
809 /// digests match those in the provided target set with clones of the
810 /// replacement envelope. Unlike `walk_unelide`, the replacement envelope
811 /// does not need to have the same digest as the node being replaced.
812 ///
813 /// This enables transforming specific elements in an envelope structure
814 /// while preserving the overall hierarchy. The replacement is applied
815 /// recursively throughout the tree.
816 ///
817 /// # Parameters
818 ///
819 /// * `target` - Set of digests identifying nodes to replace
820 /// * `replacement` - The envelope to clone for each matching node
821 ///
822 /// # Returns
823 ///
824 /// A new envelope with matching nodes replaced.
825 ///
826 /// # Errors
827 ///
828 /// Returns `Error::InvalidFormat` if attempting to replace an assertion
829 /// with a non-assertion that is also not obscured (elided, encrypted,
830 /// or compressed). Assertions in a node's assertions array must be
831 /// either assertions or obscured elements (which are presumed to be
832 /// obscured assertions).
833 ///
834 /// # Examples
835 ///
836 /// ```
837 /// # use bc_envelope::prelude::*;
838 /// # use std::collections::HashSet;
839 /// let alice = Envelope::new("Alice");
840 /// let bob = Envelope::new("Bob");
841 /// let charlie = Envelope::new("Charlie");
842 ///
843 /// let envelope = Envelope::new("Alice")
844 /// .add_assertion("knows", "Bob")
845 /// .add_assertion("likes", "Bob");
846 ///
847 /// // Replace all instances of "Bob" with "Charlie"
848 /// let mut target = HashSet::new();
849 /// target.insert(bob.digest());
850 ///
851 /// let modified = envelope.walk_replace(&target, &charlie).unwrap();
852 ///
853 /// // Both assertions now reference Charlie instead of Bob
854 /// assert!(modified.format().contains("Charlie"));
855 /// assert!(!modified.format().contains("Bob"));
856 /// ```
857 pub fn walk_replace(
858 &self,
859 target: &HashSet<Digest>,
860 replacement: &Envelope,
861 ) -> Result<Self> {
862 // Check if this node matches the target
863 if target.contains(&self.digest()) {
864 return Ok(replacement.clone());
865 }
866
867 // Recursively process children
868 match self.case() {
869 EnvelopeCase::Node { subject, assertions, .. } => {
870 let new_subject = subject.walk_replace(target, replacement)?;
871 let new_assertions: Vec<_> = assertions
872 .iter()
873 .map(|a| a.walk_replace(target, replacement))
874 .collect::<Result<Vec<_>>>()?;
875
876 if new_subject.is_identical_to(subject)
877 && new_assertions
878 .iter()
879 .zip(assertions.iter())
880 .all(|(a, b)| a.is_identical_to(b))
881 {
882 Ok(self.clone())
883 } else {
884 // Use new_with_assertions to validate that all assertions
885 // are either assertions or obscured
886 Self::new_with_assertions(new_subject, new_assertions)
887 }
888 }
889 EnvelopeCase::Wrapped { envelope, .. } => {
890 let new_envelope =
891 envelope.walk_replace(target, replacement)?;
892 if new_envelope.is_identical_to(envelope) {
893 Ok(self.clone())
894 } else {
895 Ok(new_envelope.wrap())
896 }
897 }
898 EnvelopeCase::Assertion(assertion) => {
899 let new_predicate =
900 assertion.predicate().walk_replace(target, replacement)?;
901 let new_object =
902 assertion.object().walk_replace(target, replacement)?;
903
904 if new_predicate.is_identical_to(&assertion.predicate())
905 && new_object.is_identical_to(&assertion.object())
906 {
907 Ok(self.clone())
908 } else {
909 Ok(Envelope::new_assertion(new_predicate, new_object))
910 }
911 }
912 _ => Ok(self.clone()),
913 }
914 }
915
916 /// Returns a new envelope with encrypted nodes decrypted using the
917 /// provided keys.
918 ///
919 /// This function walks the envelope hierarchy and attempts to decrypt any
920 /// encrypted nodes using the provided set of symmetric keys. Each key is
921 /// tried in sequence until one succeeds or all fail.
922 ///
923 /// If no nodes can be decrypted, the original envelope is returned
924 /// unchanged.
925 ///
926 /// This function is only available when the `encrypt` feature is enabled.
927 ///
928 /// # Parameters
929 ///
930 /// * `keys` - A slice of `SymmetricKey` values to use for decryption
931 ///
932 /// # Returns
933 ///
934 /// A new envelope with encrypted nodes decrypted where possible.
935 ///
936 /// # Examples
937 ///
938 /// ```
939 /// # use bc_envelope::prelude::*;
940 /// # use bc_components::SymmetricKey;
941 /// let key1 = SymmetricKey::new();
942 /// let key2 = SymmetricKey::new();
943 ///
944 /// let envelope = Envelope::new("Alice")
945 /// .add_assertion("knows", "Bob")
946 /// .add_assertion("age", 30);
947 ///
948 /// // Encrypt different parts with different keys
949 /// let encrypted = envelope.elide_removing_set_with_action(
950 /// &std::collections::HashSet::from([envelope
951 /// .assertion_with_predicate("knows")
952 /// .unwrap()
953 /// .digest()]),
954 /// &ObscureAction::Encrypt(key1.clone()),
955 /// );
956 ///
957 /// // Decrypt with a set of keys
958 /// let decrypted = encrypted.walk_decrypt(&[key1, key2]);
959 ///
960 /// // The decrypted envelope should match the original
961 /// assert!(decrypted.is_equivalent_to(&envelope));
962 /// ```
963 #[cfg(feature = "encrypt")]
964 pub fn walk_decrypt(&self, keys: &[SymmetricKey]) -> Self {
965 match self.case() {
966 EnvelopeCase::Encrypted(_) => {
967 // Try each key until one works
968 for key in keys {
969 if let Ok(decrypted) = self.decrypt_subject(key) {
970 return decrypted.walk_decrypt(keys);
971 }
972 }
973 // No key worked, return unchanged
974 self.clone()
975 }
976 EnvelopeCase::Node { subject, assertions, .. } => {
977 // Recursively decrypt subject and assertions
978 let new_subject = subject.walk_decrypt(keys);
979 let new_assertions: Vec<_> =
980 assertions.iter().map(|a| a.walk_decrypt(keys)).collect();
981
982 if new_subject.is_identical_to(subject)
983 && new_assertions
984 .iter()
985 .zip(assertions.iter())
986 .all(|(a, b)| a.is_identical_to(b))
987 {
988 self.clone()
989 } else {
990 Self::new_with_unchecked_assertions(
991 new_subject,
992 new_assertions,
993 )
994 }
995 }
996 EnvelopeCase::Wrapped { envelope, .. } => {
997 let new_envelope = envelope.walk_decrypt(keys);
998 if new_envelope.is_identical_to(envelope) {
999 self.clone()
1000 } else {
1001 new_envelope.wrap()
1002 }
1003 }
1004 EnvelopeCase::Assertion(assertion) => {
1005 // Recursively decrypt predicate and object
1006 let new_predicate = assertion.predicate().walk_decrypt(keys);
1007 let new_object = assertion.object().walk_decrypt(keys);
1008
1009 if new_predicate.is_identical_to(&assertion.predicate())
1010 && new_object.is_identical_to(&assertion.object())
1011 {
1012 self.clone()
1013 } else {
1014 Envelope::new_assertion(new_predicate, new_object)
1015 }
1016 }
1017 _ => self.clone(),
1018 }
1019 }
1020
1021 /// Returns a new envelope with compressed nodes decompressed.
1022 ///
1023 /// This function walks the envelope hierarchy and decompresses nodes that:
1024 /// - Are compressed, AND
1025 /// - Match the target digest set (if provided), OR all compressed nodes if
1026 /// no target set is provided
1027 ///
1028 /// If no nodes can be decompressed, the original envelope is returned
1029 /// unchanged.
1030 ///
1031 /// This function is only available when the `compress` feature is enabled.
1032 ///
1033 /// # Parameters
1034 ///
1035 /// * `target_digests` - Optional set of digests to filter by. If `None`,
1036 /// all compressed nodes will be decompressed.
1037 ///
1038 /// # Returns
1039 ///
1040 /// A new envelope with compressed nodes decompressed where they match the
1041 /// criteria.
1042 ///
1043 /// # Examples
1044 ///
1045 /// ```
1046 /// # use bc_envelope::prelude::*;
1047 /// # use std::collections::HashSet;
1048 /// let envelope = Envelope::new("Alice")
1049 /// .add_assertion("knows", "Bob")
1050 /// .add_assertion("bio", "A".repeat(1000));
1051 ///
1052 /// // Compress one assertion
1053 /// let bio_assertion = envelope.assertion_with_predicate("bio").unwrap();
1054 /// let bio_digest = bio_assertion.digest();
1055 /// let mut target = HashSet::new();
1056 /// target.insert(bio_digest);
1057 ///
1058 /// let compressed = envelope
1059 /// .elide_removing_set_with_action(&target, &ObscureAction::Compress);
1060 ///
1061 /// // Decompress just the targeted node
1062 /// let decompressed = compressed.walk_decompress(Some(&target));
1063 ///
1064 /// // The decompressed envelope should match the original
1065 /// assert!(decompressed.is_equivalent_to(&envelope));
1066 /// ```
1067 #[cfg(feature = "compress")]
1068 pub fn walk_decompress(
1069 &self,
1070 target_digests: Option<&HashSet<Digest>>,
1071 ) -> Self {
1072 match self.case() {
1073 EnvelopeCase::Compressed(_) => {
1074 // Check if this node matches the target (if target specified)
1075 let matches_target = target_digests
1076 .map(|targets| targets.contains(&self.digest()))
1077 .unwrap_or(true);
1078
1079 if matches_target {
1080 // Try to decompress
1081 if let Ok(decompressed) = self.decompress() {
1082 return decompressed.walk_decompress(target_digests);
1083 }
1084 }
1085 // Either doesn't match target or decompress failed
1086 self.clone()
1087 }
1088 EnvelopeCase::Node { subject, assertions, .. } => {
1089 // Recursively decompress subject and assertions
1090 let new_subject = subject.walk_decompress(target_digests);
1091 let new_assertions: Vec<_> = assertions
1092 .iter()
1093 .map(|a| a.walk_decompress(target_digests))
1094 .collect();
1095
1096 if new_subject.is_identical_to(subject)
1097 && new_assertions
1098 .iter()
1099 .zip(assertions.iter())
1100 .all(|(a, b)| a.is_identical_to(b))
1101 {
1102 self.clone()
1103 } else {
1104 Self::new_with_unchecked_assertions(
1105 new_subject,
1106 new_assertions,
1107 )
1108 }
1109 }
1110 EnvelopeCase::Wrapped { envelope, .. } => {
1111 let new_envelope = envelope.walk_decompress(target_digests);
1112 if new_envelope.is_identical_to(envelope) {
1113 self.clone()
1114 } else {
1115 new_envelope.wrap()
1116 }
1117 }
1118 EnvelopeCase::Assertion(assertion) => {
1119 // Recursively decompress predicate and object
1120 let new_predicate =
1121 assertion.predicate().walk_decompress(target_digests);
1122 let new_object =
1123 assertion.object().walk_decompress(target_digests);
1124
1125 if new_predicate.is_identical_to(&assertion.predicate())
1126 && new_object.is_identical_to(&assertion.object())
1127 {
1128 self.clone()
1129 } else {
1130 Envelope::new_assertion(new_predicate, new_object)
1131 }
1132 }
1133 _ => self.clone(),
1134 }
1135 }
1136}