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}