Skip to main content

bitwarden_organization_crypto/
invite_key_bundle.rs

1use std::str::FromStr;
2
3use bitwarden_crypto::{
4    BitwardenLegacyKeyBytes, EncString, KeySlotIds, KeyStoreContext, SymmetricCryptoKey,
5};
6use bitwarden_encoding::{B64, B64Url, FromStrVisitor};
7use serde::{Deserialize, Serialize};
8use subtle::{Choice, ConstantTimeEq};
9use thiserror::Error;
10
11/// Errors that can occur when creating an invite key envelope
12#[derive(Debug, Error)]
13pub enum InviteKeyBundleError {
14    /// Decoding the encrypted InviteKeyEnvelope failed
15    #[error("Decoding failed")]
16    DecodingFailed,
17    /// The key wrapping failed while using the provided organization key
18    #[error("Unable to seal invite key with org key")]
19    KeySealingFailed,
20    /// The key unsealing failed while using the provided organization key
21    #[error("Unable to unseal invite key with org key")]
22    KeyUnsealingFailed,
23    /// The key_id was not found in the key context store
24    #[error("Missing Key for Id: {0}")]
25    MissingKeyId(String),
26}
27
28#[cfg(feature = "wasm")]
29#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
30const TS_INVITE_KEY_DATA: &'static str = r#"
31export type InviteKeyData = Tagged<string, "InviteKeyData">;
32"#;
33
34/// Struct for holding the Invite Key's raw byte data. Supports WASM bindings,
35/// automatically using base64Url encoding for both `wasm-bindgen` and `tsify`.
36///
37/// To manually encode as a `base64URL` string:
38/// ```ignore
39/// let key = SymmetricCryptoKey::try_from(...);
40/// String::from(&InviteKeyData(key));
41/// ```
42/// Also supports serde serialization/deserialization using the base64Url format
43#[derive(Clone)]
44pub struct InviteKeyData(SymmetricCryptoKey);
45
46impl ConstantTimeEq for InviteKeyData {
47    fn ct_eq(&self, other: &InviteKeyData) -> Choice {
48        self.0.ct_eq(&other.0)
49    }
50}
51
52impl PartialEq for InviteKeyData {
53    fn eq(&self, other: &Self) -> bool {
54        self.ct_eq(other).into()
55    }
56}
57
58impl std::fmt::Debug for InviteKeyData {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        self.0.fmt(f)
61    }
62}
63
64impl From<&InviteKeyData> for String {
65    fn from(key_data: &InviteKeyData) -> Self {
66        B64Url::from(key_data.0.to_encoded().as_ref()).to_string()
67    }
68}
69
70impl FromStr for InviteKeyData {
71    type Err = InviteKeyBundleError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let data = B64Url::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
75        Ok(InviteKeyData(
76            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(data.as_bytes()))
77                .map_err(|_| InviteKeyBundleError::DecodingFailed)?,
78        ))
79    }
80}
81
82impl<'de> Deserialize<'de> for InviteKeyData {
83    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84    where
85        D: serde::Deserializer<'de>,
86    {
87        deserializer.deserialize_str(FromStrVisitor::new())
88    }
89}
90
91impl Serialize for InviteKeyData {
92    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
93    where
94        S: serde::Serializer,
95    {
96        serializer.serialize_str(&String::from(self))
97    }
98}
99
100#[cfg(feature = "wasm")]
101#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
102const TS_INVITE: &'static str = r#"
103export type Invite = Tagged<string, "Invite">;
104"#;
105
106/// Cryptographic invite for an organization.
107#[derive(Debug, Clone)]
108pub struct Invite {
109    organization_key_wrapped_invite_key: EncString,
110    // Milestone 3:
111    // invite_key_wrapped_organization_key: Option<EncString>
112}
113
114/// Wire format for [`Invite`]. This is what's serialized by serde
115#[derive(Serialize, Deserialize)]
116struct InviteData {
117    organization_key_wrapped_invite_key: EncString,
118}
119
120impl From<&Invite> for InviteData {
121    fn from(envelope: &Invite) -> Self {
122        InviteData {
123            organization_key_wrapped_invite_key: envelope
124                .organization_key_wrapped_invite_key
125                .clone(),
126        }
127    }
128}
129
130impl From<&Invite> for String {
131    fn from(key_data: &Invite) -> Self {
132        let mut buf = Vec::new();
133        ciborium::ser::into_writer(&InviteData::from(key_data), &mut buf)
134            .expect("CBOR serialization of InviteKeyEnvelope never fails");
135        B64::from(buf).to_string()
136    }
137}
138
139impl FromStr for Invite {
140    type Err = InviteKeyBundleError;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        let bytes = B64::try_from(s)
144            .map_err(|_| InviteKeyBundleError::DecodingFailed)?
145            .into_bytes();
146        let data: InviteData = ciborium::de::from_reader(bytes.as_slice())
147            .map_err(|_| InviteKeyBundleError::DecodingFailed)?;
148        Ok(Invite {
149            organization_key_wrapped_invite_key: data.organization_key_wrapped_invite_key,
150        })
151    }
152}
153
154impl<'de> Deserialize<'de> for Invite {
155    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156    where
157        D: serde::Deserializer<'de>,
158    {
159        deserializer.deserialize_str(FromStrVisitor::new())
160    }
161}
162
163impl Serialize for Invite {
164    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165    where
166        S: serde::Serializer,
167    {
168        serializer.serialize_str(&String::from(self))
169    }
170}
171
172impl Invite {
173    /// Given a correct organization key, unseals the `InviteKeyEnvelope`,
174    /// returning the `InviteKeyData` sealed inside.
175    pub fn unseal<Ids: KeySlotIds>(
176        &self,
177        organization_key: Ids::Symmetric,
178        ctx: &mut KeyStoreContext<Ids>,
179    ) -> Result<InviteKeyData, InviteKeyBundleError> {
180        let key_id = ctx
181            .unwrap_symmetric_key(organization_key, &self.organization_key_wrapped_invite_key)
182            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
183
184        #[allow(deprecated)]
185        Ok(InviteKeyData(
186            ctx.dangerous_get_symmetric_key(key_id)
187                .map_err(|_| InviteKeyBundleError::MissingKeyId(format!("{key_id:?}")))?
188                .clone(),
189        ))
190    }
191
192    #[allow(unused)]
193    fn enable_confirmation<Ids: KeySlotIds>(
194        &mut self,
195        _organization_key: Ids::Symmetric,
196        _ctx: &mut KeyStoreContext<Ids>,
197    ) -> Result<(), InviteKeyBundleError> {
198        unimplemented!("Confirmation is not yet supported in this version of the crate");
199    }
200
201    #[allow(unused)]
202    fn disable_confirmation(&mut self) {
203        unimplemented!("Confirmation is not yet supported in this version of the crate");
204    }
205
206    #[allow(unused)]
207    fn supports_confirmation() -> bool {
208        false
209    }
210
211    #[allow(unused)]
212    fn unseal_organization_key<Ids: KeySlotIds>(
213        &self,
214        _invite_key: &InviteKeyData,
215        _target_key_slot: Ids::Symmetric,
216        _ctx: &mut KeyStoreContext<impl KeySlotIds>,
217    ) -> Result<SymmetricCryptoKey, InviteKeyBundleError> {
218        unimplemented!("Confirmation is not yet supported in this version of the crate");
219    }
220
221    #[allow(unused)]
222    fn update_organization_key<Ids: KeySlotIds>(
223        &mut self,
224        _old_organization_key: Ids::Symmetric,
225        _new_organization_key: Ids::Symmetric,
226        _ctx: &mut KeyStoreContext<Ids>,
227    ) -> Result<Invite, InviteKeyBundleError> {
228        unimplemented!(
229            "Organization key rotation is not yet supported in this version of the crate"
230        );
231    }
232}
233
234/// A struct for holding the invitation key and the invite
235#[derive(Debug)]
236pub struct InviteBundle {
237    // The unencrypted invite key. IMPORTANT: This must never be sent to the server
238    invite_key: InviteKeyData,
239    // The cryptographic invite
240    invite: Invite,
241}
242
243impl InviteBundle {
244    /// Generates a brand new invitation key and wraps it with the provided
245    /// organization key.
246    pub fn make<Ids: KeySlotIds>(
247        organization_key: Ids::Symmetric,
248        ctx: &mut KeyStoreContext<Ids>,
249    ) -> Result<Self, InviteKeyBundleError> {
250        let key_id =
251            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
252
253        #[allow(deprecated)]
254        let raw_key_data = InviteKeyData(
255            ctx.dangerous_get_symmetric_key(key_id)
256                .map_err(|_| InviteKeyBundleError::MissingKeyId(format!("{key_id:?}")))?
257                .clone(),
258        );
259
260        let sealed_key_envelope =
261            Invite {
262                organization_key_wrapped_invite_key: ctx
263                    .wrap_symmetric_key(organization_key, key_id)
264                    .map_err(|_| InviteKeyBundleError::KeySealingFailed)?,
265            };
266
267        Ok(Self {
268            invite_key: raw_key_data,
269            invite: sealed_key_envelope,
270        })
271    }
272
273    /// Get the raw invite key bytes using `InviteKeyData`
274    /// CRITICAL: this data MUST NOT be sent to the server
275    ///
276    /// This can be base64url encoded for URL use only:
277    /// ```ignore
278    /// let key: &InviteKeyData = bundle.dangerous_get_raw_invite_key();
279    /// let key_bytes: String = String::from(key);
280    /// ```
281    pub fn dangerous_get_raw_invite_key(&self) -> &InviteKeyData {
282        &self.invite_key
283    }
284
285    /// Gets the sealed invite key (wrapped using the organization key)
286    pub fn get_envelope(&self) -> &Invite {
287        &self.invite
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey, key_slot_ids};
294    use bitwarden_encoding::{B64, B64Url};
295
296    use crate::invite_key_bundle::{Invite, InviteBundle, InviteKeyData};
297
298    #[test]
299    fn test_basic_invitation_envelope_bundle() {
300        let key_store = KeyStore::<TestIds>::default();
301        let mut ctx = key_store.context_mut();
302
303        let local_org_key_id = ctx.generate_symmetric_key();
304        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
305            .unwrap();
306
307        let key1 = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
308        let key2 = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
309
310        assert_ne!(key1.invite_key.0, key2.invite_key.0);
311    }
312
313    #[test]
314    fn test_envelope_unseals_to_raw_bytes() {
315        let key_store = KeyStore::<TestIds>::default();
316        let mut ctx = key_store.context_mut();
317
318        let local_org_key_id = ctx.generate_symmetric_key();
319        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
320            .unwrap();
321
322        let key = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
323
324        let unsealed_key = ctx
325            .unwrap_symmetric_key(
326                TestSymmKey::Organization,
327                &key.invite.organization_key_wrapped_invite_key,
328            )
329            .unwrap();
330
331        #[allow(deprecated)]
332        let unsealed_key = ctx
333            .dangerous_get_symmetric_key(unsealed_key)
334            .unwrap()
335            .clone();
336
337        assert_eq!(key.dangerous_get_raw_invite_key().0, unsealed_key);
338    }
339
340    #[test]
341    fn test_envelope_unseals_to_same_key_as_raw_data() {
342        let key_store = KeyStore::<TestIds>::default();
343        let mut ctx = key_store.context_mut();
344
345        let local_org_key_id = ctx.generate_symmetric_key();
346        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
347            .unwrap();
348
349        let key = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
350
351        let raw_key_id = ctx.add_local_symmetric_key(key.dangerous_get_raw_invite_key().0.clone());
352
353        let unsealed_key = ctx
354            .unwrap_symmetric_key(
355                TestSymmKey::Organization,
356                &key.invite.organization_key_wrapped_invite_key,
357            )
358            .unwrap();
359
360        #[allow(deprecated)]
361        let raw_key = ctx.dangerous_get_symmetric_key(raw_key_id).unwrap().clone();
362        #[allow(deprecated)]
363        let unsealed = ctx
364            .dangerous_get_symmetric_key(unsealed_key)
365            .unwrap()
366            .clone();
367        assert_eq!(raw_key, unsealed);
368    }
369
370    #[test]
371    fn test_envelope_round_trip_unseals_to_key_data() {
372        let key_store = KeyStore::<TestIds>::default();
373        let mut ctx = key_store.context_mut();
374
375        let local_org_key_id = ctx.generate_symmetric_key();
376        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
377            .unwrap();
378
379        let key_bundle = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
380        let raw_key_data = key_bundle.dangerous_get_raw_invite_key();
381        let sealed_key_envelope = key_bundle.get_envelope();
382        let unsealed_raw_key_data = sealed_key_envelope
383            .unseal(TestSymmKey::Organization, &mut ctx)
384            .unwrap();
385
386        assert_eq!(raw_key_data, &unsealed_raw_key_data);
387
388        let internal_unwrapped_key_id = ctx
389            .unwrap_symmetric_key(
390                TestSymmKey::Organization,
391                &key_bundle.invite.organization_key_wrapped_invite_key,
392            )
393            .unwrap();
394
395        #[allow(deprecated)]
396        let internal_unwrapped_key = ctx
397            .dangerous_get_symmetric_key(internal_unwrapped_key_id)
398            .unwrap()
399            .clone();
400
401        let b64url_encoded_unsealed_key =
402            B64Url::from(internal_unwrapped_key.to_encoded().as_ref());
403
404        assert_eq!(
405            String::from(&unsealed_raw_key_data),
406            b64url_encoded_unsealed_key.to_string()
407        );
408    }
409
410    #[test]
411    fn test_envelope_string_round_trip() {
412        let key_store = KeyStore::<TestIds>::default();
413        let mut ctx = key_store.context_mut();
414
415        let local_org_key_id = ctx.generate_symmetric_key();
416        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
417            .unwrap();
418
419        let bundle = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
420        let envelope = bundle.get_envelope();
421
422        let encoded = String::from(envelope);
423        let decoded: Invite = encoded.parse().unwrap();
424
425        assert_eq!(String::from(&decoded), encoded);
426
427        // The custom serde impls delegate to the string round-trip.
428        let json = serde_json::to_string(envelope).unwrap();
429        let from_json: Invite = serde_json::from_str(&json).unwrap();
430        assert_eq!(String::from(&from_json), encoded);
431    }
432
433    #[test]
434    fn test_into_base64_url() {
435        let data = b"+/=Hello, World!AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
436        let key =
437            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(data.to_vec())).unwrap();
438
439        let encoded = String::from(&InviteKeyData(key));
440
441        assert_eq!(
442            encoded,
443            "Ky89SGVsbG8sIFdvcmxkIUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ"
444        );
445        assert!(!encoded.contains('+'));
446        assert!(!encoded.contains('/'));
447        assert!(!encoded.contains('='));
448
449        let decoded = B64Url::try_from(encoded.as_str()).unwrap();
450        assert_eq!(decoded.as_bytes(), data);
451    }
452
453    // Test vectors captured from `generate_test_vectors`. These freeze a real sealed
454    // envelope so that backward compatibility (old data must remain decryptable) is
455    // verified by `test_invite_key_envelope_test_vector`.
456    const TEST_VECTOR_ORG_KEY: &str =
457        "KGP9Nc2/91w+42Z9VzY0m7h18avuZcq4ICM8Rhdc3BD92LbWS2TQkVBzavvUM684WKXiC22NJi2EwaiDW4YTAA==";
458    const TEST_VECTOR_ENVELOPE: &str = "oXgjb3JnYW5pemF0aW9uX2tleV93cmFwcGVkX2ludml0ZV9rZXl4tDIuNlRoNk1FZUJ1L0h0V0FIbFBEN01mZz09fG5FUkx1b3NjelRVMUpSbm81Y2dMYW51cnczd2gxb3dJM2QrdmUrbVJTT1g0M2ZkcE9KRmU1aFhzazhKcXVXcXlIMFBTT0ZHVGFVNkFDK0h6L3plQUF1RTFLZEpSVDRBTVFVVSs0NXJOR3hrPXxpM0tQeGIvWmRIL3ZBcGJENUVQUVplNWFXZ011RDMvWm4xNmFmNzgveFdJPQ==";
459    const TEST_VECTOR_INVITE_KEY: &str = "pQEEAlD8Oee8YLwqCdiV6AmNkSKkAzoAARFvBIQDBAUGIFgg7bQ4KpPzD2wLsWK-eCtFYhO5-rXEQaWzaxSlX7egtC4B";
460
461    #[test]
462    #[ignore = "Manual test to generate test vectors"]
463    fn generate_test_vectors() {
464        let key_store = KeyStore::<TestIds>::default();
465        let mut ctx = key_store.context_mut();
466
467        let local_org_key_id = ctx.generate_symmetric_key();
468        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
469            .unwrap();
470
471        let bundle = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
472
473        #[allow(deprecated)]
474        let org_key = ctx
475            .dangerous_get_symmetric_key(TestSymmKey::Organization)
476            .unwrap();
477
478        println!(
479            "const TEST_VECTOR_ORG_KEY: &str = \"{}\";",
480            B64::from(org_key.to_encoded())
481        );
482        println!(
483            "const TEST_VECTOR_ENVELOPE: &str = \"{}\";",
484            String::from(bundle.get_envelope())
485        );
486        println!(
487            "const TEST_VECTOR_INVITE_KEY: &str = \"{}\";",
488            String::from(bundle.dangerous_get_raw_invite_key())
489        );
490    }
491
492    #[test]
493    fn test_invite_key_envelope_test_vector() {
494        let key_store = KeyStore::<TestIds>::default();
495        let mut ctx = key_store.context_mut();
496
497        let org_key =
498            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
499        let org_key_id = ctx.add_local_symmetric_key(org_key);
500
501        let envelope: Invite = TEST_VECTOR_ENVELOPE.parse().unwrap();
502        let unsealed = envelope.unseal(org_key_id, &mut ctx).unwrap();
503
504        assert_eq!(String::from(&unsealed), TEST_VECTOR_INVITE_KEY);
505    }
506
507    #[test]
508    #[ignore = "Manual test to verify debug format"]
509    fn test_debug() {
510        let key_store = KeyStore::<TestIds>::default();
511        let mut ctx = key_store.context_mut();
512
513        let org_key_id = ctx.generate_symmetric_key();
514        ctx.persist_symmetric_key(org_key_id, TestSymmKey::Organization)
515            .unwrap();
516
517        let bundle = InviteBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
518        // Exercises both the `InviteKeyData` and `InviteKeyEnvelope` `Debug` impls.
519        println!("{bundle:?}");
520    }
521
522    key_slot_ids! {
523        #[symmetric]
524        pub enum TestSymmKey {
525            Organization,
526            #[local]
527            Local(LocalId),
528        }
529
530        #[private]
531        pub enum TestPrivateKey {
532            A(u8),
533            B,
534            #[local]
535            C(LocalId),
536        }
537
538        #[signing]
539        pub enum TestSigningKey {
540            A(u8),
541            B,
542            #[local]
543            C(LocalId),
544        }
545
546       pub TestIds => TestSymmKey, TestPrivateKey, TestSigningKey;
547    }
548}