Skip to main content

bc_xid/
provenance.rs

1use bc_components::{KeyDerivationMethod, Salt};
2use bc_envelope::prelude::*;
3use known_values::PROVENANCE_GENERATOR;
4use provenance_mark::{ProvenanceMark, ProvenanceMarkGenerator};
5
6use crate::{Error, Result};
7
8/// Provenance mark generator data that can be either decrypted or encrypted.
9#[derive(Debug, Clone)]
10pub enum GeneratorData {
11    /// Decrypted generator that can be used for mark generation.
12    Decrypted(ProvenanceMarkGenerator),
13
14    /// Encrypted generator envelope that cannot be used without decryption.
15    /// This preserves the encrypted assertion when a document is loaded
16    /// without the decryption password.
17    ///
18    /// Note: Envelope uses internal reference counting (Rc/Arc) so cloning
19    /// is cheap - no need for additional wrapper.
20    Encrypted(Envelope),
21}
22
23impl PartialEq for GeneratorData {
24    fn eq(&self, other: &Self) -> bool {
25        match (self, other) {
26            (Self::Decrypted(a), Self::Decrypted(b)) => a == b,
27            (Self::Encrypted(a), Self::Encrypted(b)) => {
28                // Compare envelopes by their UR string representation
29                a.ur_string() == b.ur_string()
30            }
31            _ => false,
32        }
33    }
34}
35
36impl Eq for GeneratorData {}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Provenance {
40    mark: ProvenanceMark,
41    generator: Option<(GeneratorData, Salt)>,
42}
43
44impl Provenance {
45    pub fn new(mark: ProvenanceMark) -> Self { Self { mark, generator: None } }
46
47    pub fn new_with_generator(
48        generator: ProvenanceMarkGenerator,
49        mark: ProvenanceMark,
50    ) -> Self {
51        let salt = Salt::new_with_len(32).unwrap();
52        Self {
53            mark,
54            generator: Some((GeneratorData::Decrypted(generator), salt)),
55        }
56    }
57
58    pub fn mark(&self) -> &ProvenanceMark { &self.mark }
59
60    pub fn generator(&self) -> Option<&ProvenanceMarkGenerator> {
61        self.generator.as_ref().and_then(|(data, _)| match data {
62            GeneratorData::Decrypted(generator) => Some(generator),
63            GeneratorData::Encrypted(_) => None,
64        })
65    }
66
67    pub fn has_generator(&self) -> bool {
68        matches!(
69            self.generator.as_ref(),
70            Some((GeneratorData::Decrypted(_), _))
71        )
72    }
73
74    pub fn has_encrypted_generator(&self) -> bool {
75        matches!(
76            self.generator.as_ref(),
77            Some((GeneratorData::Encrypted(_), _))
78        )
79    }
80
81    pub fn generator_salt(&self) -> Option<&Salt> {
82        self.generator.as_ref().map(|(_, salt)| salt)
83    }
84
85    /// Get a mutable reference to the generator, decrypting if necessary.
86    ///
87    /// # Returns
88    ///
89    /// - `Ok(Some(&mut ProvenanceMarkGenerator))` if generator is available
90    /// - `Ok(None)` if no generator is present
91    /// - `Err(Error::InvalidPassword)` if encrypted and wrong password provided
92    pub fn generator_mut(
93        &mut self,
94        password: Option<&[u8]>,
95    ) -> Result<Option<&mut ProvenanceMarkGenerator>> {
96        match &mut self.generator {
97            None => Ok(None),
98            Some((GeneratorData::Decrypted(generator), _)) => {
99                Ok(Some(generator))
100            }
101            Some((encrypted @ GeneratorData::Encrypted(_), _salt)) => {
102                if let Some(pwd) = password {
103                    // Extract the encrypted envelope
104                    let encrypted_envelope = match encrypted {
105                        GeneratorData::Encrypted(env) => env.clone(),
106                        _ => unreachable!(),
107                    };
108
109                    // Try to decrypt
110                    match encrypted_envelope.unlock_subject(pwd) {
111                        Ok(decrypted) => {
112                            let unwrapped = decrypted.try_unwrap()?;
113                            let generator =
114                                ProvenanceMarkGenerator::try_from(unwrapped)?;
115
116                            // Replace encrypted with decrypted
117                            *encrypted = GeneratorData::Decrypted(generator);
118
119                            // Now get the mutable reference
120                            match encrypted {
121                                GeneratorData::Decrypted(g) => Ok(Some(g)),
122                                _ => unreachable!(),
123                            }
124                        }
125                        Err(_) => Err(Error::InvalidPassword),
126                    }
127                } else {
128                    // Cannot decrypt without password
129                    Err(Error::InvalidPassword)
130                }
131            }
132        }
133    }
134
135    /// Update the provenance mark.
136    pub fn set_mark(&mut self, mark: ProvenanceMark) { self.mark = mark; }
137
138    /// Extract the optional generator and salt for mutation or storage.
139    pub fn take_generator(&mut self) -> Option<(GeneratorData, Salt)> {
140        self.generator.take()
141    }
142
143    /// Set or replace the generator.
144    pub fn set_generator(&mut self, generator: ProvenanceMarkGenerator) {
145        let salt = Salt::new_with_len(32).unwrap();
146        self.generator = Some((GeneratorData::Decrypted(generator), salt));
147    }
148
149    /// Extract the generator envelope, optionally decrypting it.
150    ///
151    /// # Returns
152    ///
153    /// - `Ok(None)` if no generator is present
154    /// - `Ok(Some(Envelope))` containing:
155    ///   - Decrypted `ProvenanceMarkGenerator` if unencrypted
156    ///   - Decrypted `ProvenanceMarkGenerator` if encrypted and correct
157    ///     password provided
158    ///   - Encrypted envelope if encrypted and no password provided
159    /// - `Err(...)` if encrypted and wrong password provided
160    pub fn generator_envelope(
161        &self,
162        password: Option<&str>,
163    ) -> Result<Option<Envelope>> {
164        match &self.generator {
165            None => Ok(None),
166            Some((GeneratorData::Decrypted(generator), _)) => {
167                // Unencrypted generator - return as envelope
168                Ok(Some(Envelope::new(generator.clone())))
169            }
170            Some((GeneratorData::Encrypted(encrypted_envelope), _)) => {
171                if let Some(pwd) = password {
172                    // Try to decrypt with provided password
173                    match encrypted_envelope.clone().unlock_subject(pwd) {
174                        Ok(decrypted) => {
175                            // Successfully decrypted and unwrapped
176                            let unwrapped = decrypted.try_unwrap()?;
177                            Ok(Some(unwrapped))
178                        }
179                        Err(_) => {
180                            // Wrong password
181                            Err(Error::InvalidPassword)
182                        }
183                    }
184                } else {
185                    // No password provided, return encrypted envelope as-is
186                    Ok(Some(encrypted_envelope.clone()))
187                }
188            }
189        }
190    }
191}
192
193/// Options for handling generators in envelopes.
194#[derive(Clone, Debug, PartialEq, Eq, Default)]
195pub enum XIDGeneratorOptions {
196    /// Omit the generator from the envelope (default).
197    #[default]
198    Omit,
199
200    /// Include the generator in plaintext (with salt for decorrelation).
201    Include,
202
203    /// Include the generator assertion but elide it (maintains digest tree).
204    Elide,
205
206    /// Include the generator encrypted with a password using the specified
207    /// key derivation method.
208    Encrypt {
209        method: KeyDerivationMethod,
210        password: Vec<u8>,
211    },
212}
213
214impl Provenance {
215    fn generator_assertion_envelope(&self) -> Envelope {
216        let (generator_data, salt) = self.generator.clone().unwrap();
217        match generator_data {
218            GeneratorData::Decrypted(generator) => {
219                Envelope::new_assertion(PROVENANCE_GENERATOR, generator)
220                    .add_salt_instance(salt)
221            }
222            GeneratorData::Encrypted(encrypted_envelope) => {
223                // Already encrypted, just wrap with provenanceGenerator
224                // predicate and salt
225                Envelope::new_assertion(
226                    PROVENANCE_GENERATOR,
227                    encrypted_envelope,
228                )
229                .add_salt_instance(salt)
230            }
231        }
232    }
233
234    fn extract_optional_generator_with_password(
235        envelope: &Envelope,
236        password: Option<&[u8]>,
237    ) -> Result<Option<(GeneratorData, Salt)>> {
238        if let Some(generator_assertion) =
239            envelope.optional_assertion_with_predicate(PROVENANCE_GENERATOR)?
240        {
241            let generator_object =
242                generator_assertion.subject().try_object()?;
243
244            // Extract the salt (always present)
245            let salt = generator_assertion
246                .extract_object_for_predicate::<Salt>(known_values::SALT)?;
247
248            // Check if the generator object is locked with a password
249            if generator_object.is_locked_with_password() {
250                // Need a password to decrypt
251                if let Some(pwd) = password {
252                    // Try to unlock with the password
253                    match generator_object.unlock_subject(pwd) {
254                        Ok(decrypted) => {
255                            // Successfully decrypted, unwrap and extract the
256                            // generator
257                            let unwrapped = decrypted.try_unwrap()?;
258                            let generator =
259                                ProvenanceMarkGenerator::try_from(unwrapped)?;
260                            return Ok(Some((
261                                GeneratorData::Decrypted(generator),
262                                salt,
263                            )));
264                        }
265                        Err(_) => {
266                            // Wrong password or decryption failed
267                            // Store the encrypted envelope for later
268                            return Ok(Some((
269                                GeneratorData::Encrypted(
270                                    generator_object.clone(),
271                                ),
272                                salt,
273                            )));
274                        }
275                    }
276                } else {
277                    // No password provided, store encrypted envelope
278                    return Ok(Some((
279                        GeneratorData::Encrypted(generator_object.clone()),
280                        salt,
281                    )));
282                }
283            }
284
285            // Extract plaintext generator
286            let generator =
287                ProvenanceMarkGenerator::try_from(generator_object.clone())?;
288            return Ok(Some((GeneratorData::Decrypted(generator), salt)));
289        }
290        Ok(None)
291    }
292
293    pub fn into_envelope_opt(
294        self,
295        generator_options: XIDGeneratorOptions,
296    ) -> Envelope {
297        let mut envelope = Envelope::new(self.mark().clone());
298        if let Some((generator_data, _)) = &self.generator {
299            match generator_data {
300                GeneratorData::Encrypted(_) => {
301                    // Always preserve encrypted generators, regardless of
302                    // options
303                    let assertion_envelope =
304                        self.generator_assertion_envelope();
305                    envelope = envelope
306                        .add_assertion_envelope(assertion_envelope)
307                        .unwrap();
308                }
309                GeneratorData::Decrypted(_) => {
310                    // For decrypted generators, respect the generator_options
311                    match generator_options {
312                        XIDGeneratorOptions::Include => {
313                            let assertion_envelope =
314                                self.generator_assertion_envelope();
315                            envelope = envelope
316                                .add_assertion_envelope(assertion_envelope)
317                                .unwrap();
318                        }
319                        XIDGeneratorOptions::Elide => {
320                            let assertion_envelope =
321                                self.generator_assertion_envelope().elide();
322                            envelope = envelope
323                                .add_assertion_envelope(assertion_envelope)
324                                .unwrap();
325                        }
326                        XIDGeneratorOptions::Encrypt { method, password } => {
327                            let (generator, salt) =
328                                self.generator.clone().unwrap();
329
330                            match generator {
331                                GeneratorData::Decrypted(generator) => {
332                                    // Create an envelope with the generator
333                                    let generator_envelope =
334                                        Envelope::new(generator);
335
336                                    // Wrap and encrypt it using lock_subject
337                                    let encrypted = generator_envelope
338                                        .wrap()
339                                        .lock_subject(method, password)
340                                        .expect("Failed to encrypt generator");
341
342                                    // Create the provenanceGenerator assertion
343                                    // with the encrypted envelope
344                                    let assertion_envelope =
345                                        Envelope::new_assertion(
346                                            PROVENANCE_GENERATOR,
347                                            encrypted,
348                                        )
349                                        .add_salt_instance(salt);
350
351                                    envelope = envelope
352                                        .add_assertion_envelope(
353                                            assertion_envelope,
354                                        )
355                                        .unwrap();
356                                }
357                                GeneratorData::Encrypted(
358                                    encrypted_envelope,
359                                ) => {
360                                    // Already encrypted - we can't re-encrypt
361                                    // without decrypting first. Just preserve
362                                    // the
363                                    // existing encrypted envelope.
364                                    let assertion_envelope =
365                                        Envelope::new_assertion(
366                                            PROVENANCE_GENERATOR,
367                                            encrypted_envelope,
368                                        )
369                                        .add_salt_instance(salt);
370
371                                    envelope = envelope
372                                        .add_assertion_envelope(
373                                            assertion_envelope,
374                                        )
375                                        .unwrap();
376                                }
377                            }
378                        }
379                        XIDGeneratorOptions::Omit => {
380                            // Omit decrypted generators
381                        }
382                    }
383                }
384            }
385        }
386
387        envelope
388    }
389}
390
391impl EnvelopeEncodable for Provenance {
392    fn into_envelope(self) -> Envelope {
393        self.into_envelope_opt(XIDGeneratorOptions::Omit)
394    }
395}
396
397impl TryFrom<&Envelope> for Provenance {
398    type Error = Error;
399
400    fn try_from(envelope: &Envelope) -> Result<Self> {
401        Self::try_from_envelope(envelope, None)
402    }
403}
404
405impl TryFrom<Envelope> for Provenance {
406    type Error = Error;
407
408    fn try_from(envelope: Envelope) -> Result<Self> {
409        Provenance::try_from(&envelope)
410    }
411}
412
413impl Provenance {
414    /// Try to extract a `Provenance` from an envelope, optionally providing a
415    /// password to decrypt an encrypted generator.
416    ///
417    /// If the generator is encrypted and no password is provided, the
418    /// `Provenance` will be created without the generator (it will be
419    /// `None`).
420    pub fn try_from_envelope(
421        envelope: &Envelope,
422        password: Option<&[u8]>,
423    ) -> Result<Self> {
424        let mark = ProvenanceMark::try_from(envelope.subject().try_leaf()?)?;
425        let generator = Provenance::extract_optional_generator_with_password(
426            envelope, password,
427        )?;
428
429        Ok(Self { mark, generator })
430    }
431}