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}