bc_envelope/base/
elide.rs

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