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}