bitwarden_organization_crypto/
invite_key_bundle.rs1use 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#[derive(Debug, Error)]
13pub enum InviteKeyBundleError {
14 #[error("Decoding failed")]
16 DecodingFailed,
17 #[error("Unable to seal invite key with org key")]
19 KeySealingFailed,
20 #[error("Unable to unseal invite key with org key")]
22 KeyUnsealingFailed,
23 #[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#[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#[derive(Debug, Clone)]
108pub struct Invite {
109 organization_key_wrapped_invite_key: EncString,
110 }
113
114#[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 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#[derive(Debug)]
236pub struct InviteBundle {
237 invite_key: InviteKeyData,
239 invite: Invite,
241}
242
243impl InviteBundle {
244 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 pub fn dangerous_get_raw_invite_key(&self) -> &InviteKeyData {
282 &self.invite_key
283 }
284
285 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 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 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 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}