bc_envelope/extension/
sskr.rs

1use std::collections::HashMap;
2
3use anyhow::{Result, bail};
4pub use bc_components::{
5    SSKRError, SSKRGroupSpec, SSKRSecret, SSKRShare, SSKRSpec,
6};
7use bc_components::{SymmetricKey, sskr_combine, sskr_generate_using};
8use bc_rand::RandomNumberGenerator;
9#[cfg(feature = "known_value")]
10use known_values;
11
12use crate::{Envelope, Error};
13
14/// Support for splitting and combining envelopes using SSKR (Shamir's Secret
15/// Sharing).
16///
17/// This module extends Gordian Envelope with functions to support Sharded
18/// Secret Key Reconstruction (SSKR), which is an implementation of Shamir's
19/// Secret Sharing. SSKR allows splitting a secret (in this case, a symmetric
20/// encryption key) into multiple shares, with a threshold required for
21/// reconstruction.
22///
23/// SSKR provides social recovery for encrypted envelopes by allowing the owner
24/// to distribute shares to trusted individuals or storage locations, with a
25/// specified threshold required to reconstruct the original envelope.
26///
27/// # How SSKR Works with Envelopes
28///
29/// The overall process is as follows:
30///
31/// 1. A Gordian Envelope is encrypted with a symmetric key
32/// 2. The symmetric key is split into multiple SSKR shares using a group
33///    threshold and member thresholds
34/// 3. Each share is added as an assertion to a copy of the encrypted envelope
35/// 4. These envelope copies are distributed to trusted individuals or storage
36///    locations
37/// 5. Later, when enough shares are brought together, the original envelope can
38///    be reconstructed
39///
40/// Sharded Secret Key Reconstruction (SSKR) for Envelopes.
41///
42/// For a complete working example, see the `sskr_split()` and `sskr_join()`
43/// method documentation.
44impl Envelope {
45    /// Returns a new `Envelope` with a `sskrShare: SSKRShare` assertion added.
46    ///
47    /// This is an internal helper function used by the SSKR split methods.
48    fn add_sskr_share(&self, share: &SSKRShare) -> Self {
49        self.add_assertion(known_values::SSKR_SHARE, share.clone())
50    }
51
52    /// Splits the envelope into a set of SSKR shares.
53    ///
54    /// This method splits the symmetric key used to encrypt the envelope into
55    /// SSKR shares, and returns multiple copies of the original envelope,
56    /// each with a different SSKR share added as an assertion. The envelope
57    /// subject should already be encrypted with the provided `content_key`.
58    ///
59    /// The returned structure is a nested array that preserves the group
60    /// structure of the SSKR shares. Each outer array represents a group,
61    /// and each inner array contains the shares for that group.
62    ///
63    /// # Parameters
64    ///
65    /// * `spec` - The SSKR specification that defines the group structure and
66    ///   thresholds
67    /// * `content_key` - The symmetric key that was used to encrypt the
68    ///   envelope
69    ///
70    /// # Returns
71    ///
72    /// A nested array of envelopes organized by groups, each envelope
73    /// containing the original encrypted envelope with a unique SSKR share
74    /// added as an assertion
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the SSKR shares cannot be generated
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use bc_envelope::prelude::*;
84    /// use bc_components::{SymmetricKey, SSKRGroupSpec, SSKRSpec};
85    ///
86    /// // Create and encrypt an envelope
87    /// let envelope = Envelope::new("Secret data").encrypt(&SymmetricKey::new());
88    ///
89    /// // Define a 2-of-3 SSKR split
90    /// let group_spec = SSKRGroupSpec::new(2, 3).unwrap();
91    /// let spec = SSKRSpec::new(1, vec![group_spec]).unwrap();
92    ///
93    /// // Create a symmetric key for the encrypted content
94    /// let content_key = SymmetricKey::new();
95    ///
96    /// // Split the envelope into shares
97    /// let shares = envelope.sskr_split(&spec, &content_key).unwrap();
98    ///
99    /// // The outer array represents groups (in this case, a single group)
100    /// assert_eq!(shares.len(), 1);
101    ///
102    /// // The inner array contains the shares for the group (3 shares in this example)
103    /// assert_eq!(shares[0].len(), 3);
104    ///
105    /// // Each share is an envelope with an 'sskrShare' assertion
106    /// for share in &shares[0] {
107    ///     assert!(share.assertions_with_predicate(known_values::SSKR_SHARE).len() > 0);
108    /// }
109    /// ```
110    pub fn sskr_split(
111        &self,
112        spec: &SSKRSpec,
113        content_key: &SymmetricKey,
114    ) -> Result<Vec<Vec<Envelope>>> {
115        let mut rng = bc_rand::SecureRandomNumberGenerator;
116        self.sskr_split_using(spec, content_key, &mut rng)
117    }
118
119    /// Splits the envelope into a flattened set of SSKR shares.
120    ///
121    /// This method works like `sskr_split()` but returns a flat array of all
122    /// shares rather than preserving the group structure. This is
123    /// convenient when the group structure is not needed for distribution.
124    ///
125    /// # Parameters
126    ///
127    /// * `spec` - The SSKR specification that defines the group structure and
128    ///   thresholds
129    /// * `content_key` - The symmetric key that was used to encrypt the
130    ///   envelope
131    ///
132    /// # Returns
133    ///
134    /// A flat array of all envelopes containing SSKR shares
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the SSKR shares cannot be generated
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use bc_components::{SSKRGroupSpec, SSKRSpec, SymmetricKey};
144    /// use bc_envelope::prelude::*;
145    ///
146    /// // Create and encrypt an envelope
147    /// let envelope = Envelope::new("Secret data").encrypt(&SymmetricKey::new());
148    ///
149    /// // Define a 2-of-3 SSKR split in a single group
150    /// let group_spec = SSKRGroupSpec::new(2, 3).unwrap();
151    /// let spec = SSKRSpec::new(1, vec![group_spec]).unwrap();
152    ///
153    /// // Create a symmetric key for the encrypted content
154    /// let content_key = SymmetricKey::new();
155    ///
156    /// // Split the envelope into a flat array of shares
157    /// let shares = envelope.sskr_split_flattened(&spec, &content_key).unwrap();
158    ///
159    /// // We get all 3 shares in a flat array
160    /// assert_eq!(shares.len(), 3);
161    ///
162    /// // Each share is an envelope with an 'sskrShare' assertion
163    /// for share in &shares {
164    ///     assert!(
165    ///         share
166    ///             .assertions_with_predicate(known_values::SSKR_SHARE)
167    ///             .len()
168    ///             > 0
169    ///     );
170    /// }
171    /// ```
172    pub fn sskr_split_flattened(
173        &self,
174        spec: &SSKRSpec,
175        content_key: &SymmetricKey,
176    ) -> Result<Vec<Envelope>> {
177        Ok(self
178            .sskr_split(spec, content_key)?
179            .into_iter()
180            .flatten()
181            .collect())
182    }
183
184    #[doc(hidden)]
185    /// Internal function that splits the envelope using a provided random
186    /// number generator.
187    ///
188    /// This method is primarily used for testing to ensure deterministic SSKR
189    /// shares.
190    pub fn sskr_split_using(
191        &self,
192        spec: &SSKRSpec,
193        content_key: &SymmetricKey,
194        test_rng: &mut impl RandomNumberGenerator,
195    ) -> Result<Vec<Vec<Envelope>>> {
196        let master_secret = SSKRSecret::new(content_key.data())?;
197        let shares = sskr_generate_using(spec, &master_secret, test_rng)?;
198        let mut result: Vec<Vec<Envelope>> = Vec::new();
199        for group in shares {
200            let mut group_result: Vec<Envelope> = Vec::new();
201            for share in group {
202                let share_result = self.add_sskr_share(&share);
203                group_result.push(share_result);
204            }
205            result.push(group_result);
206        }
207        Ok(result)
208    }
209
210    /// Internal helper function that extracts SSKR shares from a set of
211    /// envelopes.
212    ///
213    /// This function groups the shares by their identifier (which must match
214    /// for shares to be combined).
215    fn sskr_shares_in(
216        envelopes: &[&Envelope],
217    ) -> Result<HashMap<u16, Vec<SSKRShare>>> {
218        let mut result: HashMap<u16, Vec<SSKRShare>> = HashMap::new();
219        for envelope in envelopes {
220            for assertion in
221                envelope.assertions_with_predicate(known_values::SSKR_SHARE)
222            {
223                let share = assertion
224                    .as_object()
225                    .unwrap()
226                    .extract_subject::<SSKRShare>()?;
227                let identifier = share.identifier();
228                result
229                    .entry(identifier)
230                    .and_modify(|shares| shares.push(share.clone()))
231                    .or_insert(vec![share]);
232            }
233        }
234        Ok(result)
235    }
236
237    /// Reconstructs the original envelope from a set of SSKR shares.
238    ///
239    /// Given a set of envelopes with SSKR share assertions, this method
240    /// attempts to combine the shares to reconstruct the original symmetric
241    /// key. If successful, it uses the key to decrypt the envelope and
242    /// return the original envelope subject.
243    ///
244    /// The method will try all combinations of shares with matching identifiers
245    /// to find a valid reconstruction.
246    ///
247    /// # Parameters
248    ///
249    /// * `envelopes` - An array of envelope references containing SSKR shares
250    ///
251    /// # Returns
252    ///
253    /// The original envelope if reconstruction is successful
254    ///
255    /// # Errors
256    ///
257    /// * Returns `EnvelopeError::InvalidShares` if not enough valid shares are
258    ///   provided
259    /// * Returns various errors if decryption fails
260    ///
261    /// # Examples
262    ///
263    /// ```
264    /// use bc_envelope::prelude::*;
265    /// use bc_components::{SymmetricKey, SSKRGroupSpec, SSKRSpec};
266    ///
267    /// // Create the original envelope with an assertion
268    /// let original = Envelope::new("Secret message")
269    ///     .add_assertion("metadata", "This is a test");
270    ///
271    /// // Create a content key
272    /// let content_key = SymmetricKey::new();
273    ///
274    /// // Wrap the envelope (so the whole envelope including its assertions
275    /// // become the subject)
276    /// let wrapped_original = original
277    ///     .wrap();
278    ///
279    /// // Encrypt the wrapped envelope
280    /// let encrypted = wrapped_original
281    ///     .encrypt_subject(&content_key).unwrap();
282    ///
283    /// // Create a 2-of-3 SSKR split specification
284    /// let group = SSKRGroupSpec::new(2, 3).unwrap();
285    /// let spec = SSKRSpec::new(1, vec![group]).unwrap();
286    ///
287    /// // Split the encrypted envelope into shares
288    /// let shares = encrypted.sskr_split(&spec, &content_key).unwrap();
289    /// assert_eq!(shares[0].len(), 3);
290    ///
291    /// // The shares would normally be distributed to different people/places
292    /// // For recovery, we need at least the threshold number of shares (2 in this case)
293    /// let share1 = &shares[0][0];
294    /// let share2 = &shares[0][1];
295    ///
296    /// // Combine the shares to recover the original decrypted envelope
297    /// let recovered_wrapped = Envelope::sskr_join(&[share1, share2]).unwrap();
298    ///
299    /// // Unwrap the envelope to get the original envelope
300    /// let recovered = recovered_wrapped.try_unwrap().unwrap();
301    ///
302    /// // Check that the recovered envelope matches the original
303    /// assert!(recovered.is_identical_to(&original));
304    /// ```
305    pub fn sskr_join(envelopes: &[&Envelope]) -> Result<Envelope> {
306        if envelopes.is_empty() {
307            bail!(Error::InvalidShares);
308        }
309
310        let grouped_shares: Vec<_> =
311            Self::sskr_shares_in(envelopes)?.values().cloned().collect();
312        for shares in grouped_shares {
313            if let Ok(secret) = sskr_combine(&shares) {
314                if let Ok(content_key) = SymmetricKey::from_data_ref(&secret) {
315                    if let Ok(envelope) =
316                        envelopes.first().unwrap().decrypt_subject(&content_key)
317                    {
318                        return Ok(envelope.subject());
319                    }
320                }
321            }
322        }
323        bail!(Error::InvalidShares)
324    }
325}
326
327#[cfg(all(test, feature = "sskr", feature = "types", feature = "known_value"))]
328mod tests {
329    use bc_components::{SSKRGroupSpec, SSKRSpec, SymmetricKey};
330
331    use crate::prelude::*;
332
333    #[test]
334    fn test_sskr_split_and_join() {
335        // Create the original envelope with an assertion
336        let original = Envelope::new("Secret message")
337            .add_assertion("metadata", "This is a test");
338
339        // Create a content key
340        let content_key = SymmetricKey::new();
341
342        // Wrap the envelope (so the whole envelope including its assertions
343        // become the subject)
344        let wrapped_original = original.wrap();
345
346        // Encrypt the wrapped envelope
347        let encrypted = wrapped_original.encrypt_subject(&content_key).unwrap();
348
349        // Create a 2-of-3 SSKR split specification
350        let group = SSKRGroupSpec::new(2, 3).unwrap();
351        let spec = SSKRSpec::new(1, vec![group]).unwrap();
352
353        // Split the encrypted envelope into shares
354        let shares = encrypted.sskr_split(&spec, &content_key).unwrap();
355        assert_eq!(shares[0].len(), 3);
356
357        // The shares would normally be distributed to different people/places
358        // For recovery, we need at least the threshold number of shares (2 in
359        // this case)
360        let share1 = &shares[0][0];
361        let share2 = &shares[0][1];
362
363        // Combine the shares to recover the original decrypted envelope
364        let recovered_wrapped = Envelope::sskr_join(&[share1, share2]).unwrap();
365
366        // Unwrap the envelope to get the original envelope
367        let recovered = recovered_wrapped.try_unwrap().unwrap();
368
369        // Check that the recovered envelope matches the original
370        assert!(recovered.is_identical_to(&original));
371    }
372}