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/// Actions that can be performed on parts of an envelope to obscure them.
13///
14/// Gordian Envelope supports several ways to obscure parts of an envelope while
15/// maintaining its semantic integrity and digest tree. This enum defines the
16/// possible actions that can be taken when obscuring envelope elements.
17///
18/// Obscuring parts of an envelope is a key feature for privacy and selective
19/// disclosure, allowing the holder of an envelope to share only specific parts
20/// while hiding, encrypting, or compressing others.
21pub enum ObscureAction {
22    /// Elide the target, leaving only its digest.
23    ///
24    /// Elision replaces the targeted envelope element with just its digest,
25    /// hiding its actual content while maintaining the integrity of the
26    /// envelope's digest tree. This is the most basic form of selective
27    /// disclosure.
28    ///
29    /// Elided elements can be revealed later by providing the original unelided
30    /// envelope to the recipient, who can verify that the revealed content
31    /// matches the digest in the elided version.
32    Elide,
33
34    /// Encrypt the target using the specified symmetric key.
35    ///
36    /// This encrypts the targeted envelope element using authenticated
37    /// encryption with the provided key. The encrypted content can only be
38    /// accessed by those who possess the symmetric key.
39    ///
40    /// This action is only available when the `encrypt` feature is enabled.
41    #[cfg(feature = "encrypt")]
42    Encrypt(SymmetricKey),
43
44    /// Compress the target using a compression algorithm.
45    ///
46    /// This compresses the targeted envelope element to reduce its size while
47    /// still allowing it to be decompressed by any recipient. Unlike elision or
48    /// encryption, compression doesn't provide privacy but can reduce the size
49    /// of large envelope components.
50    ///
51    /// This action is only available when the `compress` feature is enabled.
52    #[cfg(feature = "compress")]
53    Compress,
54}
55
56/// Support for eliding elements from envelopes.
57///
58/// This includes eliding, encrypting and compressing (obscuring) elements.
59impl Envelope {
60    /// Returns the elided variant of this envelope.
61    ///
62    /// Elision replaces an envelope with just its digest, hiding its content
63    /// while maintaining the integrity of the envelope's digest tree. This
64    /// is a fundamental privacy feature of Gordian Envelope that enables
65    /// selective disclosure.
66    ///
67    /// Returns the same envelope if it is already elided.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// # use bc_envelope::prelude::*;
73    /// # use indoc::indoc;
74    /// let envelope = Envelope::new("Hello.");
75    /// let elided = envelope.elide();
76    ///
77    /// // The elided envelope shows only "ELIDED" in formatting
78    /// assert_eq!(elided.format_flat(), "ELIDED");
79    ///
80    /// // But it maintains the same digest as the original
81    /// assert!(envelope.is_equivalent_to(&elided));
82    /// ```
83    pub fn elide(&self) -> Self {
84        match self.case() {
85            EnvelopeCase::Elided(_) => self.clone(),
86            _ => Self::new_elided(self.digest().into_owned()),
87        }
88    }
89
90    /// Returns a version of this envelope with elements in the `target` set
91    /// elided.
92    ///
93    /// This function obscures elements in the envelope whose digests are in the
94    /// provided target set, applying the specified action (elision,
95    /// encryption, or compression) to those elements while leaving other
96    /// elements intact.
97    ///
98    /// # Parameters
99    ///
100    /// * `target` - The set of digests that identify elements to be obscured
101    /// * `action` - The action to perform on the targeted elements (elide,
102    ///   encrypt, or compress)
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// # use bc_envelope::prelude::*;
108    /// # use std::collections::HashSet;
109    /// let envelope = Envelope::new("Alice")
110    ///     .add_assertion("knows", "Bob")
111    ///     .add_assertion("livesAt", "123 Main St.");
112    ///
113    /// // Create a set of digests targeting the "livesAt" assertion
114    /// let mut target = HashSet::new();
115    /// let livesAt_assertion = envelope.assertion_with_predicate("livesAt").unwrap();
116    /// target.insert(livesAt_assertion.digest().into_owned());
117    ///
118    /// // Elide that specific assertion
119    /// let elided = envelope.elide_removing_set_with_action(&target, &ObscureAction::Elide);
120    ///
121    /// // The result will have the "livesAt" assertion elided but "knows" still visible
122    /// ```
123    pub fn elide_removing_set_with_action(
124        &self,
125        target: &HashSet<Digest>,
126        action: &ObscureAction,
127    ) -> Self {
128        self.elide_set_with_action(target, false, action)
129    }
130
131    /// Returns a version of this envelope with elements in the `target` set
132    /// elided.
133    ///
134    /// This is a convenience function that calls `elide_set` with
135    /// `is_revealing` set to `false`, using the standard elision action.
136    /// Use this when you want to simply elide elements rather than encrypt
137    /// or compress them.
138    ///
139    /// # Parameters
140    ///
141    /// * `target` - The set of digests that identify elements to be elided
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// # use bc_envelope::prelude::*;
147    /// # use std::collections::HashSet;
148    /// let envelope = Envelope::new("Alice")
149    ///     .add_assertion("knows", "Bob")
150    ///     .add_assertion("email", "alice@example.com");
151    ///
152    /// // Create a set of digests targeting the email assertion
153    /// let mut target = HashSet::new();
154    /// let email_assertion = envelope.assertion_with_predicate("email").unwrap();
155    /// target.insert(email_assertion.digest().into_owned());
156    ///
157    /// // Elide the email assertion for privacy
158    /// let redacted = envelope.elide_removing_set(&target);
159    /// ```
160    pub fn elide_removing_set(&self, target: &HashSet<Digest>) -> Self {
161        self.elide_set(target, false)
162    }
163
164    /// Returns a version of this envelope with elements in the `target` set
165    /// elided.
166    ///
167    /// - Parameters:
168    ///   - target: An array of `DigestProvider`s.
169    ///   - action: Perform the specified action (elision, encryption or
170    ///     compression).
171    ///
172    /// - Returns: The elided envelope.
173    pub fn elide_removing_array_with_action(
174        &self,
175        target: &[&dyn DigestProvider],
176        action: &ObscureAction,
177    ) -> Self {
178        self.elide_array_with_action(target, false, action)
179    }
180
181    /// Returns a version of this envelope with elements in the `target` set
182    /// elided.
183    ///
184    /// - Parameters:
185    ///   - target: An array of `DigestProvider`s.
186    ///   - action: Perform the specified action (elision, encryption or
187    ///     compression).
188    ///
189    /// - Returns: The elided envelope.
190    pub fn elide_removing_array(&self, target: &[&dyn DigestProvider]) -> Self {
191        self.elide_array(target, false)
192    }
193
194    /// Returns a version of this envelope with the target element elided.
195    ///
196    /// - Parameters:
197    ///   - target: A `DigestProvider`.
198    ///   - action: Perform the specified action (elision, encryption or
199    ///     compression).
200    ///
201    /// - Returns: The elided envelope.
202    pub fn elide_removing_target_with_action(
203        &self,
204        target: &dyn DigestProvider,
205        action: &ObscureAction,
206    ) -> Self {
207        self.elide_target_with_action(target, false, action)
208    }
209
210    /// Returns a version of this envelope with the target element elided.
211    ///
212    /// - Parameters:
213    ///   - target: A `DigestProvider`.
214    ///
215    /// - Returns: The elided envelope.
216    pub fn elide_removing_target(&self, target: &dyn DigestProvider) -> Self {
217        self.elide_target(target, false)
218    }
219
220    /// Returns a version of this envelope with only elements in the `target`
221    /// set revealed, and all other elements elided.
222    ///
223    /// This function performs the opposite operation of
224    /// `elide_removing_set_with_action`. Instead of specifying which
225    /// elements to obscure, you specify which elements to reveal,
226    /// and everything else will be obscured using the specified action.
227    ///
228    /// This is particularly useful for selective disclosure where you want to
229    /// reveal only specific portions of an envelope while keeping the rest
230    /// private.
231    ///
232    /// # Parameters
233    ///
234    /// * `target` - The set of digests that identify elements to be revealed
235    /// * `action` - The action to perform on all other elements (elide,
236    ///   encrypt, or compress)
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// # use bc_envelope::prelude::*;
242    /// # use std::collections::HashSet;
243    /// let envelope = Envelope::new("Alice")
244    ///     .add_assertion("name", "Alice Smith")
245    ///     .add_assertion("age", 30)
246    ///     .add_assertion("ssn", "123-45-6789");
247    ///
248    /// // Create a set of digests for elements we want to reveal
249    /// let mut reveal_set = HashSet::new();
250    ///
251    /// // Add the subject and the name assertion to the set to reveal
252    /// reveal_set.insert(envelope.subject().digest().into_owned());
253    /// reveal_set.insert(
254    ///     envelope
255    ///         .assertion_with_predicate("name")
256    ///         .unwrap()
257    ///         .digest()
258    ///         .into_owned(),
259    /// );
260    ///
261    /// // Create an envelope that only reveals name and hides age and SSN
262    /// let selective = envelope
263    ///     .elide_revealing_set_with_action(&reveal_set, &ObscureAction::Elide);
264    /// ```
265    pub fn elide_revealing_set_with_action(
266        &self,
267        target: &HashSet<Digest>,
268        action: &ObscureAction,
269    ) -> Self {
270        self.elide_set_with_action(target, true, action)
271    }
272
273    /// Returns a version of this envelope with elements *not* in the `target`
274    /// set elided.
275    ///
276    /// - Parameters:
277    ///   - target: The target set of digests.
278    ///
279    /// - Returns: The elided envelope.
280    pub fn elide_revealing_set(&self, target: &HashSet<Digest>) -> Self {
281        self.elide_set(target, true)
282    }
283
284    /// Returns a version of this envelope with elements *not* in the `target`
285    /// set elided.
286    ///
287    /// - Parameters:
288    ///   - target: An array of `DigestProvider`s.
289    ///   - action: Perform the specified action (elision, encryption or
290    ///     compression).
291    ///
292    /// - Returns: The elided envelope.
293    pub fn elide_revealing_array_with_action(
294        &self,
295        target: &[&dyn DigestProvider],
296        action: &ObscureAction,
297    ) -> Self {
298        self.elide_array_with_action(target, true, action)
299    }
300
301    /// Returns a version of this envelope with elements *not* in the `target`
302    /// set elided.
303    ///
304    /// - Parameters:
305    ///   - target: An array of `DigestProvider`s.
306    ///
307    /// - Returns: The elided envelope.
308    pub fn elide_revealing_array(
309        &self,
310        target: &[&dyn DigestProvider],
311    ) -> Self {
312        self.elide_array(target, true)
313    }
314
315    /// Returns a version of this envelope with all elements *except* the target
316    /// element elided.
317    ///
318    /// - Parameters:
319    ///   - target: A `DigestProvider`.
320    ///   - action: Perform the specified action (elision, encryption or
321    ///     compression).
322    ///
323    /// - Returns: The elided envelope.
324    pub fn elide_revealing_target_with_action(
325        &self,
326        target: &dyn DigestProvider,
327        action: &ObscureAction,
328    ) -> Self {
329        self.elide_target_with_action(target, true, action)
330    }
331
332    /// Returns a version of this envelope with all elements *except* the target
333    /// element elided.
334    ///
335    /// - Parameters:
336    ///   - target: A `DigestProvider`.
337    ///
338    /// - Returns: The elided envelope.
339    pub fn elide_revealing_target(&self, target: &dyn DigestProvider) -> Self {
340        self.elide_target(target, true)
341    }
342
343    // Target Matches   isRevealing     elide
344    // ----------------------------------------
345    //     false           false        false
346    //     false           true         true
347    //     true            false        true
348    //     true            true         false
349
350    /// Returns an elided version of this envelope.
351    ///
352    /// - Parameters:
353    ///   - target: The target set of digests.
354    ///   - isRevealing: If `true`, the target set contains the digests of the
355    ///     elements to leave revealed. If it is `false`, the target set
356    ///     contains the digests of the elements to elide.
357    ///   - action: Perform the specified action (elision, encryption or
358    ///     compression).
359    ///
360    /// - Returns: The elided envelope.
361    pub fn elide_set_with_action(
362        &self,
363        target: &HashSet<Digest>,
364        is_revealing: bool,
365        action: &ObscureAction,
366    ) -> Self {
367        let self_digest = self.digest().into_owned();
368        if target.contains(&self_digest) != is_revealing {
369            match action {
370                ObscureAction::Elide => self.elide(),
371                #[cfg(feature = "encrypt")]
372                ObscureAction::Encrypt(key) => {
373                    let message = key.encrypt_with_digest(
374                        self.tagged_cbor().to_cbor_data(),
375                        self_digest,
376                        None::<Nonce>,
377                    );
378                    Self::new_with_encrypted(message).unwrap()
379                }
380                #[cfg(feature = "compress")]
381                ObscureAction::Compress => self.compress().unwrap(),
382            }
383        } else if let EnvelopeCase::Assertion(assertion) = self.case() {
384            let predicate = assertion.predicate().elide_set_with_action(
385                target,
386                is_revealing,
387                action,
388            );
389            let object = assertion.object().elide_set_with_action(
390                target,
391                is_revealing,
392                action,
393            );
394            let elided_assertion = Assertion::new(predicate, object);
395            assert!(&elided_assertion == assertion);
396            Self::new_with_assertion(elided_assertion)
397        } else if let EnvelopeCase::Node { subject, assertions, .. } =
398            self.case()
399        {
400            let elided_subject =
401                subject.elide_set_with_action(target, is_revealing, action);
402            assert!(elided_subject.digest() == subject.digest());
403            let elided_assertions = assertions
404                .iter()
405                .map(|assertion| {
406                    let elided_assertion = assertion.elide_set_with_action(
407                        target,
408                        is_revealing,
409                        action,
410                    );
411                    assert!(elided_assertion.digest() == assertion.digest());
412                    elided_assertion
413                })
414                .collect();
415            Self::new_with_unchecked_assertions(
416                elided_subject,
417                elided_assertions,
418            )
419        } else if let EnvelopeCase::Wrapped { envelope, .. } = self.case() {
420            let elided_envelope =
421                envelope.elide_set_with_action(target, is_revealing, action);
422            assert!(elided_envelope.digest() == envelope.digest());
423            Self::new_wrapped(elided_envelope)
424        } else {
425            self.clone()
426        }
427    }
428
429    /// Returns an elided version of this envelope.
430    ///
431    /// - Parameters:
432    ///   - target: The target set of digests.
433    ///   - isRevealing: If `true`, the target set contains the digests of the
434    ///     elements to leave revealed. If it is `false`, the target set
435    ///     contains the digests of the elements to elide.
436    ///
437    /// - Returns: The elided envelope.
438    pub fn elide_set(
439        &self,
440        target: &HashSet<Digest>,
441        is_revealing: bool,
442    ) -> Self {
443        self.elide_set_with_action(target, is_revealing, &ObscureAction::Elide)
444    }
445
446    /// Returns an elided version of this envelope.
447    ///
448    /// - Parameters:
449    ///   - target: An array of `DigestProvider`s.
450    ///   - isRevealing: If `true`, the target set contains the digests of the
451    ///     elements to leave revealed. If it is `false`, the target set
452    ///     contains the digests of the elements to elide.
453    ///   - action: Perform the specified action (elision, encryption or
454    ///     compression).
455    ///
456    /// - Returns: The elided envelope.
457    pub fn elide_array_with_action(
458        &self,
459        target: &[&dyn DigestProvider],
460        is_revealing: bool,
461        action: &ObscureAction,
462    ) -> Self {
463        self.elide_set_with_action(
464            &target
465                .iter()
466                .map(|provider| provider.digest().into_owned())
467                .collect(),
468            is_revealing,
469            action,
470        )
471    }
472
473    /// Returns an elided version of this envelope.
474    ///
475    /// - Parameters:
476    ///   - target: An array of `DigestProvider`s.
477    ///   - isRevealing: If `true`, the target set contains the digests of the
478    ///     elements to leave revealed. If it is `false`, the target set
479    ///     contains the digests of the elements to elide.
480    ///
481    /// - Returns: The elided envelope.
482    pub fn elide_array(
483        &self,
484        target: &[&dyn DigestProvider],
485        is_revealing: bool,
486    ) -> Self {
487        self.elide_array_with_action(
488            target,
489            is_revealing,
490            &ObscureAction::Elide,
491        )
492    }
493
494    /// Returns an elided version of this envelope.
495    ///
496    /// - Parameters:
497    ///   - target: A `DigestProvider`.
498    ///   - isRevealing: If `true`, the target is the element to leave revealed,
499    ///     eliding all others. If it is `false`, the target is the element to
500    ///     elide, leaving all others revealed.
501    ///   - action: Perform the specified action (elision, encryption or
502    ///     compression).
503    ///
504    /// - Returns: The elided envelope.
505    pub fn elide_target_with_action(
506        &self,
507        target: &dyn DigestProvider,
508        is_revealing: bool,
509        action: &ObscureAction,
510    ) -> Self {
511        self.elide_array_with_action(&[target], is_revealing, action)
512    }
513
514    /// Returns an elided version of this envelope.
515    ///
516    /// - Parameters:
517    ///   - target: A `DigestProvider`.
518    ///   - isRevealing: If `true`, the target is the element to leave revealed,
519    ///     eliding all others. If it is `false`, the target is the element to
520    ///     elide, leaving all others revealed.
521    ///
522    /// - Returns: The elided envelope.
523    pub fn elide_target(
524        &self,
525        target: &dyn DigestProvider,
526        is_revealing: bool,
527    ) -> Self {
528        self.elide_target_with_action(
529            target,
530            is_revealing,
531            &ObscureAction::Elide,
532        )
533    }
534
535    /// Returns the unelided variant of this envelope by revealing the original
536    /// content.
537    ///
538    /// This function allows restoring an elided envelope to its original form,
539    /// but only if the provided envelope's digest matches the elided
540    /// envelope's digest. This ensures the integrity of the revealed
541    /// content.
542    ///
543    /// Returns the same envelope if it is already unelided.
544    ///
545    /// # Errors
546    ///
547    /// Returns `EnvelopeError::InvalidDigest` if the provided envelope's digest
548    /// doesn't match the current envelope's digest.
549    ///
550    /// # Examples
551    ///
552    /// ```
553    /// # use bc_envelope::prelude::*;
554    /// let original = Envelope::new("Hello.");
555    /// let elided = original.elide();
556    ///
557    /// // Later, we can unelide the envelope if we have the original
558    /// let revealed = elided.unelide(&original).unwrap();
559    /// assert_eq!(revealed.format(), "\"Hello.\"");
560    ///
561    /// // Attempting to unelide with a different envelope will fail
562    /// let different = Envelope::new("Different");
563    /// assert!(elided.unelide(&different).is_err());
564    /// ```
565    pub fn unelide(&self, envelope: impl Into<Envelope>) -> Result<Self> {
566        let envelope = envelope.into();
567        if self.digest() == envelope.digest() {
568            Ok(envelope)
569        } else {
570            Err(Error::InvalidDigest)
571        }
572    }
573}