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/// Struct for holding the Invite Key's raw byte data. Supports WASM bindings,
29/// automatically using base64Url encoding for both `wasm-bindgen` and `tsify`.
30///
31/// To manually encode as a `base64URL` string:
32/// ```ignore
33/// let key = SymmetricCryptoKey::try_from(...);
34/// String::from(&InviteKeyData(key));
35/// ```
36/// Also supports serde serialization/deserialization using the base64Url format
37#[derive(Clone)]
38pub struct InviteKeyData(SymmetricCryptoKey);
39
40impl ConstantTimeEq for InviteKeyData {
41    fn ct_eq(&self, other: &InviteKeyData) -> Choice {
42        self.0.ct_eq(&other.0)
43    }
44}
45
46impl PartialEq for InviteKeyData {
47    fn eq(&self, other: &Self) -> bool {
48        self.ct_eq(other).into()
49    }
50}
51
52impl std::fmt::Debug for InviteKeyData {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        self.0.fmt(f)
55    }
56}
57
58impl From<&InviteKeyData> for String {
59    fn from(key_data: &InviteKeyData) -> Self {
60        B64Url::from(key_data.0.to_encoded().as_ref()).to_string()
61    }
62}
63
64impl FromStr for InviteKeyData {
65    type Err = InviteKeyBundleError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        let data = B64Url::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
69        Ok(InviteKeyData(
70            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(data.as_bytes()))
71                .map_err(|_| InviteKeyBundleError::DecodingFailed)?,
72        ))
73    }
74}
75
76impl<'de> Deserialize<'de> for InviteKeyData {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where
79        D: serde::Deserializer<'de>,
80    {
81        deserializer.deserialize_str(FromStrVisitor::new())
82    }
83}
84
85impl Serialize for InviteKeyData {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: serde::Serializer,
89    {
90        serializer.serialize_str(&String::from(self))
91    }
92}
93
94/// Struct for holding the wrapped invite key data. Currently supports encstring
95/// but the inner type must remain private as it may be extended in the future.
96pub struct InviteKeyEnvelope(EncString);
97
98impl From<&InviteKeyEnvelope> for String {
99    fn from(key_data: &InviteKeyEnvelope) -> Self {
100        B64::from(
101            key_data
102                .0
103                .to_buffer()
104                .expect("`to_buffer` never fails for `EncString`"),
105        )
106        .to_string()
107    }
108}
109
110impl FromStr for InviteKeyEnvelope {
111    type Err = InviteKeyBundleError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        let data = B64::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
115        Ok(InviteKeyEnvelope(
116            EncString::from_buffer(data.as_bytes())
117                .map_err(|_| InviteKeyBundleError::DecodingFailed)?,
118        ))
119    }
120}
121
122impl<'de> Deserialize<'de> for InviteKeyEnvelope {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: serde::Deserializer<'de>,
126    {
127        deserializer.deserialize_str(FromStrVisitor::new())
128    }
129}
130
131impl Serialize for InviteKeyEnvelope {
132    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133    where
134        S: serde::Serializer,
135    {
136        serializer.serialize_str(&String::from(self))
137    }
138}
139
140impl InviteKeyEnvelope {
141    /// Given a correct organization key, unseals the `InviteKeyEnvelope`,
142    /// returning the `InviteKeyData` sealed inside.
143    pub fn unseal<Ids: KeySlotIds>(
144        &self,
145        organization_key: Ids::Symmetric,
146        ctx: &mut KeyStoreContext<Ids>,
147    ) -> Result<InviteKeyData, InviteKeyBundleError> {
148        let key_id = ctx
149            .unwrap_symmetric_key(organization_key, &self.0)
150            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
151
152        #[allow(deprecated)]
153        Ok(InviteKeyData(
154            ctx.dangerous_get_symmetric_key(key_id)
155                .map_err(|_| InviteKeyBundleError::MissingKeyId(format!("{key_id:?}")))?
156                .clone(),
157        ))
158    }
159}
160
161/// A struct for holding the invitation key and the invitation key sealed by
162/// the organization key
163pub struct InviteKeyBundle {
164    raw_key_data: InviteKeyData,
165    sealed_key_envelope: InviteKeyEnvelope,
166}
167
168impl InviteKeyBundle {
169    /// Generates a brand new invitation key and wraps it with the provided
170    /// organization key.
171    pub fn make<Ids: KeySlotIds>(
172        organization_key: Ids::Symmetric,
173        ctx: &mut KeyStoreContext<Ids>,
174    ) -> Result<Self, InviteKeyBundleError> {
175        let key_id =
176            ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
177
178        #[allow(deprecated)]
179        let raw_key_data = InviteKeyData(
180            ctx.dangerous_get_symmetric_key(key_id)
181                .map_err(|_| InviteKeyBundleError::MissingKeyId(format!("{key_id:?}")))?
182                .clone(),
183        );
184
185        let sealed_key_envelope = InviteKeyEnvelope(
186            ctx.wrap_symmetric_key(organization_key, key_id)
187                .map_err(|_| InviteKeyBundleError::KeySealingFailed)?,
188        );
189
190        Ok(Self {
191            raw_key_data,
192            sealed_key_envelope,
193        })
194    }
195
196    /// Get the raw invite key bytes using `InviteKeyData`
197    /// CRITICAL: this data MUST NOT be sent to the server
198    ///
199    /// This can be base64url encoded for URL use only:
200    /// ```ignore
201    /// let key: &InviteKeyData = bundle.dangerous_get_raw_invite_key();
202    /// let key_bytes: String = String::from(key);
203    /// ```
204    pub fn dangerous_get_raw_invite_key(&self) -> &InviteKeyData {
205        &self.raw_key_data
206    }
207
208    /// Gets the sealed invite key (wrapped using the organization key)
209    pub fn get_sealed_invite_key_envelope(&self) -> &InviteKeyEnvelope {
210        &self.sealed_key_envelope
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey, key_slot_ids};
217    use bitwarden_encoding::B64Url;
218
219    use crate::invite_key_bundle::{InviteKeyBundle, InviteKeyData};
220
221    #[test]
222    fn test_basic_invitation_envelope_bundle() {
223        let key_store = KeyStore::<TestIds>::default();
224        let mut ctx = key_store.context_mut();
225
226        let local_org_key_id = ctx.generate_symmetric_key();
227        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
228            .unwrap();
229
230        let key1 = InviteKeyBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
231        let key2 = InviteKeyBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
232
233        assert_ne!(key1.raw_key_data.0, key2.raw_key_data.0);
234    }
235
236    #[test]
237    fn test_envelope_unseals_to_raw_bytes() {
238        let key_store = KeyStore::<TestIds>::default();
239        let mut ctx = key_store.context_mut();
240
241        let local_org_key_id = ctx.generate_symmetric_key();
242        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
243            .unwrap();
244
245        let key = InviteKeyBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
246
247        let unsealed_key = ctx
248            .unwrap_symmetric_key(TestSymmKey::Organization, &key.sealed_key_envelope.0)
249            .unwrap();
250
251        #[allow(deprecated)]
252        let unsealed_key = ctx
253            .dangerous_get_symmetric_key(unsealed_key)
254            .unwrap()
255            .clone();
256
257        assert_eq!(key.dangerous_get_raw_invite_key().0, unsealed_key);
258    }
259
260    #[test]
261    fn test_envelope_unseals_to_same_key_as_raw_data() {
262        let key_store = KeyStore::<TestIds>::default();
263        let mut ctx = key_store.context_mut();
264
265        let local_org_key_id = ctx.generate_symmetric_key();
266        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
267            .unwrap();
268
269        let key = InviteKeyBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
270
271        let raw_key_id = ctx.add_local_symmetric_key(key.dangerous_get_raw_invite_key().0.clone());
272
273        let unsealed_key = ctx
274            .unwrap_symmetric_key(TestSymmKey::Organization, &key.sealed_key_envelope.0)
275            .unwrap();
276
277        ctx.assert_symmetric_keys_equal(raw_key_id, unsealed_key);
278    }
279
280    #[test]
281    fn test_envelope_round_trip_unseals_to_key_data() {
282        let key_store = KeyStore::<TestIds>::default();
283        let mut ctx = key_store.context_mut();
284
285        let local_org_key_id = ctx.generate_symmetric_key();
286        ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
287            .unwrap();
288
289        let key_bundle = InviteKeyBundle::make(TestSymmKey::Organization, &mut ctx).unwrap();
290        let raw_key_data = key_bundle.dangerous_get_raw_invite_key();
291        let sealed_key_envelope = key_bundle.get_sealed_invite_key_envelope();
292        let unsealed_raw_key_data = sealed_key_envelope
293            .unseal(TestSymmKey::Organization, &mut ctx)
294            .unwrap();
295
296        assert_eq!(raw_key_data, &unsealed_raw_key_data);
297
298        let internal_unwrapped_key_id = ctx
299            .unwrap_symmetric_key(TestSymmKey::Organization, &key_bundle.sealed_key_envelope.0)
300            .unwrap();
301
302        #[allow(deprecated)]
303        let internal_unwrapped_key = ctx
304            .dangerous_get_symmetric_key(internal_unwrapped_key_id)
305            .unwrap()
306            .clone();
307
308        let b64url_encoded_unsealed_key =
309            B64Url::from(internal_unwrapped_key.to_encoded().as_ref());
310
311        assert_eq!(
312            String::from(&unsealed_raw_key_data),
313            b64url_encoded_unsealed_key.to_string()
314        );
315    }
316
317    #[test]
318    fn test_into_base64_url() {
319        let data = b"+/=Hello, World!AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
320        let key =
321            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(data.to_vec())).unwrap();
322
323        let encoded = String::from(&InviteKeyData(key));
324
325        assert_eq!(
326            encoded,
327            "Ky89SGVsbG8sIFdvcmxkIUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ"
328        );
329        assert!(!encoded.contains('+'));
330        assert!(!encoded.contains('/'));
331        assert!(!encoded.contains('='));
332
333        let decoded = B64Url::try_from(encoded.as_str()).unwrap();
334        assert_eq!(decoded.as_bytes(), data);
335    }
336
337    key_slot_ids! {
338        #[symmetric]
339        pub enum TestSymmKey {
340            Organization,
341            #[local]
342            Local(LocalId),
343        }
344
345        #[private]
346        pub enum TestPrivateKey {
347            A(u8),
348            B,
349            #[local]
350            C(LocalId),
351        }
352
353        #[signing]
354        pub enum TestSigningKey {
355            A(u8),
356            B,
357            #[local]
358            C(LocalId),
359        }
360
361       pub TestIds => TestSymmKey, TestPrivateKey, TestSigningKey;
362    }
363}