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}