bc_envelope/base/
elide.rs

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