Skip to main content

bitwarden_crypto/safe/
symmetric_key_envelope.rs

1//! Wrapped symmetric key envelope for sealing a symmetric key with another symmetric key.
2
3use std::str::FromStr;
4
5use bitwarden_encoding::{B64, FromStrVisitor};
6use ciborium::Value;
7use coset::{CborSerializable, CoseEncrypt0Builder, HeaderBuilder};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10#[cfg(feature = "wasm")]
11use wasm_bindgen::convert::FromWasmAbi;
12
13use crate::{
14    BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, KeyIds,
15    KeyStoreContext, SymmetricCryptoKey, XChaCha20Poly1305Key,
16    cose::{CONTAINED_KEY_ID, ContentNamespace, SafeObjectNamespace, XCHACHA20_POLY1305},
17    keys::KeyId,
18    safe::helpers::{
19        debug_fmt, extract_contained_key_id, set_safe_namespaces, validate_safe_namespaces,
20    },
21};
22
23/// Errors that can occur when sealing or unsealing a symmetric key with envelope operations.
24#[derive(Debug, Error)]
25pub enum SymmetricKeyEnvelopeError {
26    /// The wrapping key provided is incorrect or the envelope was tampered with
27    #[error("Wrong key")]
28    WrongKey,
29    /// The envelope could not be parsed correctly
30    #[error("Parsing error {0}")]
31    Parsing(String),
32    /// There is no key for the provided key id in the key store
33    #[error("Key missing error")]
34    KeyMissing,
35    /// The key store could not be written to, for example due to being read-only
36    #[error("Could not write to key store")]
37    KeyStore,
38    /// The wrong unseal method was used for the key type
39    #[error("Wrong key type")]
40    WrongKeyType,
41    /// The symmetric key envelope namespace is invalid.
42    #[error("Invalid namespace")]
43    InvalidNamespace,
44}
45
46/// A symmetric key protected by another symmetric key
47pub struct SymmetricKeyEnvelope {
48    cose_encrypt0: coset::CoseEncrypt0,
49}
50
51impl SymmetricKeyEnvelope {
52    /// Seals a symmetric key with another symmetric key from the key store.
53    ///
54    /// This should never fail, except for memory allocation errors.
55    pub fn seal<Ids: KeyIds>(
56        key_to_seal: Ids::Symmetric,
57        sealing_key: Ids::Symmetric,
58        namespace: SymmetricKeyEnvelopeNamespace,
59        ctx: &KeyStoreContext<Ids>,
60    ) -> Result<Self, SymmetricKeyEnvelopeError> {
61        // Get the keys from the key store.
62        let key_to_seal = ctx
63            .get_symmetric_key(key_to_seal)
64            .map_err(|_| SymmetricKeyEnvelopeError::KeyMissing)?;
65        let wrapping_key = ctx
66            .get_symmetric_key(sealing_key)
67            .map_err(|_| SymmetricKeyEnvelopeError::KeyMissing)?;
68
69        // For now, just XChaCha20Poly1305 is supported as wrapping key
70        let wrapping_key: &XChaCha20Poly1305Key = match wrapping_key {
71            SymmetricCryptoKey::XChaCha20Poly1305Key(key) => key,
72            _ => {
73                return Err(SymmetricKeyEnvelopeError::Parsing(
74                    "Wrapping key must be XChaCha20Poly1305".to_string(),
75                ));
76            }
77        };
78
79        let (content_format, key_bytes) = match key_to_seal.to_encoded_raw() {
80            EncodedSymmetricKey::BitwardenLegacyKey(key_bytes) => {
81                (ContentFormat::BitwardenLegacyKey, key_bytes.to_vec())
82            }
83            EncodedSymmetricKey::CoseKey(key_bytes) => (ContentFormat::CoseKey, key_bytes.to_vec()),
84        };
85
86        let mut header_builder = HeaderBuilder::from(content_format);
87
88        // Only set the contained key ID if the key has one
89        if let Some(key_id) = key_to_seal.key_id() {
90            header_builder =
91                header_builder.value(CONTAINED_KEY_ID, Value::from(Vec::from(&key_id)));
92        }
93
94        let mut protected_header = header_builder.build();
95        set_safe_namespaces(
96            &mut protected_header,
97            SafeObjectNamespace::SymmetricKeyEnvelope,
98            namespace,
99        );
100        protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
101        protected_header.key_id = wrapping_key.key_id.as_slice().into();
102
103        let cose_encrypt0 = crate::cose::encrypt_cose(
104            CoseEncrypt0Builder::new().protected(protected_header),
105            &key_bytes,
106            wrapping_key,
107        );
108
109        Ok(SymmetricKeyEnvelope { cose_encrypt0 })
110    }
111
112    /// Unseals a symmetric key from the envelope and stores it in the key store context.
113    pub fn unseal<Ids: KeyIds>(
114        &self,
115        wrapping_key: Ids::Symmetric,
116        namespace: SymmetricKeyEnvelopeNamespace,
117        ctx: &mut KeyStoreContext<Ids>,
118    ) -> Result<Ids::Symmetric, SymmetricKeyEnvelopeError> {
119        let wrapping_key_ref = ctx
120            .get_symmetric_key(wrapping_key)
121            .map_err(|_| SymmetricKeyEnvelopeError::KeyMissing)?;
122
123        let wrapping_key_inner = match wrapping_key_ref {
124            SymmetricCryptoKey::XChaCha20Poly1305Key(key) => key,
125            _ => {
126                return Err(SymmetricKeyEnvelopeError::Parsing(
127                    "Wrapping key must be XChaCha20Poly1305".to_string(),
128                ));
129            }
130        };
131
132        validate_safe_namespaces(
133            &self.cose_encrypt0.protected.header,
134            SafeObjectNamespace::SymmetricKeyEnvelope,
135            namespace,
136        )
137        .map_err(|_| SymmetricKeyEnvelopeError::InvalidNamespace)?;
138
139        // Validate the content format
140        let content_format = ContentFormat::try_from(&self.cose_encrypt0.protected.header)
141            .map_err(|_| {
142                SymmetricKeyEnvelopeError::Parsing("Invalid content format".to_string())
143            })?;
144
145        // Decrypt the key bytes
146        let key_bytes = crate::cose::decrypt_cose(&self.cose_encrypt0, wrapping_key_inner)
147            .map_err(|_| SymmetricKeyEnvelopeError::WrongKey)?;
148
149        // Reconstruct the encoded symmetric key from the content format
150        let encoded_key = match content_format {
151            ContentFormat::BitwardenLegacyKey => {
152                EncodedSymmetricKey::BitwardenLegacyKey(BitwardenLegacyKeyBytes::from(key_bytes))
153            }
154            ContentFormat::CoseKey => EncodedSymmetricKey::CoseKey(CoseKeyBytes::from(key_bytes)),
155            _ => {
156                return Err(SymmetricKeyEnvelopeError::WrongKeyType);
157            }
158        };
159
160        let key = SymmetricCryptoKey::try_from(encoded_key)
161            .map_err(|_| SymmetricKeyEnvelopeError::WrongKeyType)?;
162
163        Ok(ctx.add_local_symmetric_key(key))
164    }
165
166    /// Get the key ID of the contained key.
167    pub fn contained_key_id(&self) -> Result<Option<KeyId>, SymmetricKeyEnvelopeError> {
168        extract_contained_key_id(&self.cose_encrypt0.protected.header)
169            .map_err(|_| SymmetricKeyEnvelopeError::Parsing("Invalid contained key id".to_string()))
170    }
171}
172
173impl From<&SymmetricKeyEnvelope> for Vec<u8> {
174    fn from(val: &SymmetricKeyEnvelope) -> Self {
175        val.cose_encrypt0
176            .clone()
177            .to_vec()
178            .expect("Serialization to cose should not fail")
179    }
180}
181
182impl TryFrom<&Vec<u8>> for SymmetricKeyEnvelope {
183    type Error = coset::CoseError;
184
185    fn try_from(value: &Vec<u8>) -> Result<Self, Self::Error> {
186        let cose_encrypt0 = coset::CoseEncrypt0::from_slice(value)?;
187        Ok(SymmetricKeyEnvelope { cose_encrypt0 })
188    }
189}
190
191impl std::fmt::Debug for SymmetricKeyEnvelope {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        let mut s = f.debug_struct("SymmetricKeyEnvelope");
194
195        if !self.cose_encrypt0.protected.header.key_id.is_empty() {
196            s.field(
197                "sealing_key_id",
198                &self.cose_encrypt0.protected.header.key_id,
199            );
200        }
201
202        debug_fmt::<SymmetricKeyEnvelopeNamespace>(&mut s, &self.cose_encrypt0.protected.header);
203
204        if let Ok(Some(key_id)) = self.contained_key_id() {
205            s.field("contained_key_id", &key_id);
206        }
207
208        s.finish()
209    }
210}
211
212impl FromStr for SymmetricKeyEnvelope {
213    type Err = SymmetricKeyEnvelopeError;
214
215    fn from_str(s: &str) -> Result<Self, Self::Err> {
216        let data = B64::try_from(s).map_err(|_| {
217            SymmetricKeyEnvelopeError::Parsing(
218                "Invalid WrappedSymmetricKey Base64 encoding".to_string(),
219            )
220        })?;
221        Self::try_from(&data.into_bytes()).map_err(|_| {
222            SymmetricKeyEnvelopeError::Parsing("Failed to parse SymmetricKeyEnvelope".to_string())
223        })
224    }
225}
226
227impl From<SymmetricKeyEnvelope> for String {
228    fn from(val: SymmetricKeyEnvelope) -> Self {
229        let serialized: Vec<u8> = (&val).into();
230        B64::from(serialized).to_string()
231    }
232}
233
234impl<'de> Deserialize<'de> for SymmetricKeyEnvelope {
235    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236    where
237        D: serde::Deserializer<'de>,
238    {
239        deserializer.deserialize_str(FromStrVisitor::new())
240    }
241}
242
243impl Serialize for SymmetricKeyEnvelope {
244    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
245    where
246        S: serde::Serializer,
247    {
248        let serialized: Vec<u8> = self.into();
249        serializer.serialize_str(&B64::from(serialized).to_string())
250    }
251}
252
253#[cfg(feature = "wasm")]
254#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
255const TS_CUSTOM_TYPES: &'static str = r#"
256export type SymmetricKeyEnvelope = Tagged<string, "SymmetricKeyEnvelope">;
257"#;
258
259#[cfg(feature = "wasm")]
260impl wasm_bindgen::describe::WasmDescribe for SymmetricKeyEnvelope {
261    fn describe() {
262        <String as wasm_bindgen::describe::WasmDescribe>::describe();
263    }
264}
265
266#[cfg(feature = "wasm")]
267impl FromWasmAbi for SymmetricKeyEnvelope {
268    type Abi = <String as FromWasmAbi>::Abi;
269
270    unsafe fn from_abi(abi: Self::Abi) -> Self {
271        use wasm_bindgen::UnwrapThrowExt;
272        let string = unsafe { String::from_abi(abi) };
273        SymmetricKeyEnvelope::from_str(&string).unwrap_throw()
274    }
275}
276
277/// Content namespace for the symmetric key envelope
278#[allow(clippy::enum_variant_names)]
279#[derive(Debug, Copy, Clone, PartialEq)]
280pub enum SymmetricKeyEnvelopeNamespace {
281    /// A key used for re-hydration of the SDK
282    SessionKey = 1,
283    #[cfg(test)]
284    /// Example namespace for testing purposes.
285    ExampleNamespace = -3,
286    #[cfg(test)]
287    /// Another example namespace for testing purposes.
288    ExampleNamespace2 = -4,
289}
290
291impl SymmetricKeyEnvelopeNamespace {
292    /// Returns the numeric value of the namespace.
293    pub fn as_i64(&self) -> i64 {
294        *self as i64
295    }
296}
297
298impl TryFrom<i128> for SymmetricKeyEnvelopeNamespace {
299    type Error = SymmetricKeyEnvelopeError;
300
301    fn try_from(value: i128) -> Result<Self, Self::Error> {
302        match value {
303            1 => Ok(SymmetricKeyEnvelopeNamespace::SessionKey),
304            #[cfg(test)]
305            -3 => Ok(SymmetricKeyEnvelopeNamespace::ExampleNamespace),
306            #[cfg(test)]
307            -4 => Ok(SymmetricKeyEnvelopeNamespace::ExampleNamespace2),
308            _ => Err(SymmetricKeyEnvelopeError::InvalidNamespace),
309        }
310    }
311}
312
313impl TryFrom<i64> for SymmetricKeyEnvelopeNamespace {
314    type Error = SymmetricKeyEnvelopeError;
315
316    fn try_from(value: i64) -> Result<Self, Self::Error> {
317        Self::try_from(i128::from(value))
318    }
319}
320
321impl From<SymmetricKeyEnvelopeNamespace> for i128 {
322    fn from(value: SymmetricKeyEnvelopeNamespace) -> Self {
323        value.as_i64().into()
324    }
325}
326
327impl ContentNamespace for SymmetricKeyEnvelopeNamespace {}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::{KeyStore, SymmetricKeyAlgorithm, traits::tests::TestIds};
333
334    const TEST_VECTOR_SEALING_KEY: &str = "pQEEAlBLD8tcKNRLZaXNSr8OcwkgAzoAARFvBIQDBAUGIFgggG++dwvSRVPaPrIis1+XXXCizFYcakDZxSJP2HlJj0YB";
335    const TEST_VECTOR_KEY_TO_SEAL: &str = "pQEEAlCEjXxxMulOVJtq1CSNv1aqAzoAARFvBIQDBAUGIFggwdF1yfFVwesj1CMQlVMhm+tvjwA1pxvTnQVUmfBMlJMB";
336    const TEST_VECTOR_ENVELOPE: &str = "g1gspQE6AAERbwMYZToAATiBAzoAATiAIjoAARVcUISNfHEy6U5Um2rUJI2/VqqhBVgYnGokg0NPDLDd18K13zYsAM1SN+NkcfSJWFSEgtR3nhrJzUHRq35myF0tZh18iQx+0vJQ2BHj4lWwHLV3awLcyxdD8UBNQYgKu6nDHs1KDiFN+D48iI60aelHmLgJpNVsCBovnTxZVLbo67AIgw4=";
337
338    #[test]
339    #[ignore = "Manual test to verify debug format"]
340    fn test_debug() {
341        let key_store = KeyStore::<TestIds>::default();
342        let mut ctx = key_store.context_mut();
343        let key1 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
344        let key2 = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
345
346        let envelope = SymmetricKeyEnvelope::seal(
347            key1,
348            key2,
349            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
350            &ctx,
351        );
352        println!("{:?}", envelope);
353    }
354
355    #[test]
356    fn test_seal_unseal_symmetric() {
357        let key_store = KeyStore::<TestIds>::default();
358        let mut ctx = key_store.context_mut();
359
360        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
361        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
362
363        let envelope = SymmetricKeyEnvelope::seal(
364            key_to_seal,
365            wrapping_key,
366            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
367            &ctx,
368        )
369        .unwrap();
370
371        let unsealed_key = envelope
372            .unseal(
373                wrapping_key,
374                SymmetricKeyEnvelopeNamespace::ExampleNamespace,
375                &mut ctx,
376            )
377            .unwrap();
378
379        let unsealed_key_ref = ctx
380            .get_symmetric_key(unsealed_key)
381            .expect("Key should exist in the key store");
382
383        let original_key_ref = ctx
384            .get_symmetric_key(key_to_seal)
385            .expect("Key should exist in the key store");
386
387        assert_eq!(unsealed_key_ref, original_key_ref);
388    }
389
390    #[test]
391    fn test_contained_key_id_symmetric() {
392        let key_store = KeyStore::<TestIds>::default();
393        let mut ctx = key_store.context_mut();
394
395        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
396        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
397
398        let envelope = SymmetricKeyEnvelope::seal(
399            key_to_seal,
400            wrapping_key,
401            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
402            &ctx,
403        )
404        .unwrap();
405
406        let key_to_seal_ref = ctx
407            .get_symmetric_key(key_to_seal)
408            .expect("Key should exist in the key store");
409
410        let contained_key_id = envelope.contained_key_id().unwrap();
411
412        assert_eq!(key_to_seal_ref.key_id(), contained_key_id);
413    }
414
415    #[test]
416    fn test_string_serialization() {
417        let key_store = KeyStore::<TestIds>::default();
418        let mut ctx = key_store.context_mut();
419
420        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
421        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
422
423        let envelope = SymmetricKeyEnvelope::seal(
424            key_to_seal,
425            wrapping_key,
426            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
427            &ctx,
428        )
429        .unwrap();
430
431        let serialized: String = envelope.into();
432        let deserialized = SymmetricKeyEnvelope::from_str(&serialized).unwrap();
433
434        let unsealed_key = deserialized
435            .unseal(
436                wrapping_key,
437                SymmetricKeyEnvelopeNamespace::ExampleNamespace,
438                &mut ctx,
439            )
440            .unwrap();
441
442        let unsealed_key_ref = ctx
443            .get_symmetric_key(unsealed_key)
444            .expect("Key should exist in the key store");
445
446        let original_key_ref = ctx
447            .get_symmetric_key(key_to_seal)
448            .expect("Key should exist in the key store");
449
450        assert_eq!(unsealed_key_ref, original_key_ref);
451    }
452
453    #[test]
454    fn test_wrong_key() {
455        let key_store = KeyStore::<TestIds>::default();
456        let mut ctx = key_store.context_mut();
457
458        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
459        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
460        let wrong_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
461
462        let envelope = SymmetricKeyEnvelope::seal(
463            key_to_seal,
464            wrapping_key,
465            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
466            &ctx,
467        )
468        .unwrap();
469
470        assert!(matches!(
471            envelope.unseal(
472                wrong_key,
473                SymmetricKeyEnvelopeNamespace::ExampleNamespace,
474                &mut ctx
475            ),
476            Err(SymmetricKeyEnvelopeError::WrongKey)
477        ));
478    }
479
480    #[test]
481    fn test_wrong_namespace() {
482        let key_store = KeyStore::<TestIds>::default();
483        let mut ctx = key_store.context_mut();
484
485        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
486        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
487
488        let envelope = SymmetricKeyEnvelope::seal(
489            key_to_seal,
490            wrapping_key,
491            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
492            &ctx,
493        )
494        .unwrap();
495
496        assert!(matches!(
497            envelope.unseal(
498                wrapping_key,
499                SymmetricKeyEnvelopeNamespace::ExampleNamespace2,
500                &mut ctx
501            ),
502            Err(SymmetricKeyEnvelopeError::InvalidNamespace)
503        ));
504    }
505
506    #[test]
507    #[ignore]
508    fn generate_test_vectors() {
509        let key_store = KeyStore::<TestIds>::default();
510        let mut ctx = key_store.context_mut();
511
512        let key_to_seal = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
513        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
514
515        let envelope = SymmetricKeyEnvelope::seal(
516            key_to_seal,
517            wrapping_key,
518            SymmetricKeyEnvelopeNamespace::ExampleNamespace,
519            &ctx,
520        )
521        .unwrap();
522
523        println!(
524            "const TEST_VECTOR_SEALING_KEY: &str = \"{}\";",
525            bitwarden_encoding::B64::from(
526                ctx.get_symmetric_key(wrapping_key)
527                    .unwrap()
528                    .to_encoded()
529                    .to_vec()
530                    .as_slice()
531            )
532        );
533        println!(
534            "const TEST_VECTOR_KEY_TO_SEAL: &str = \"{}\";",
535            bitwarden_encoding::B64::from(
536                ctx.get_symmetric_key(key_to_seal)
537                    .unwrap()
538                    .to_encoded()
539                    .to_vec()
540                    .as_slice()
541            )
542        );
543        let serialized: String = envelope.into();
544        println!("const TEST_VECTOR_ENVELOPE: &str = \"{}\";", serialized);
545    }
546
547    #[test]
548    fn decrypt_test_vectors() {
549        let key_store = KeyStore::<TestIds>::default();
550        let mut ctx = key_store.context_mut();
551
552        let sealing_key = SymmetricCryptoKey::try_from(TEST_VECTOR_SEALING_KEY.to_string())
553            .expect("Failed to parse sealing key from test vector");
554        let sealed_key_test_vector =
555            SymmetricCryptoKey::try_from(TEST_VECTOR_KEY_TO_SEAL.to_string())
556                .expect("Failed to parse key to seal from test vector");
557
558        let sealing_key_id = ctx.add_local_symmetric_key(sealing_key);
559
560        let envelope = SymmetricKeyEnvelope::from_str(TEST_VECTOR_ENVELOPE).unwrap();
561
562        let unsealed_key_id = envelope
563            .unseal(
564                sealing_key_id,
565                SymmetricKeyEnvelopeNamespace::ExampleNamespace,
566                &mut ctx,
567            )
568            .unwrap();
569        let unsealed_key = ctx.get_symmetric_key(unsealed_key_id).unwrap();
570        assert_eq!(unsealed_key.to_owned(), sealed_key_test_vector.to_owned());
571    }
572}