1use std::str::FromStr;
33
34use bitwarden_crypto::{
35 CoseKeyThumbprint, CoseKeyThumbprintExt, EncString, KeySlotIds, KeyStoreContext,
36 generate_versioned_sealable,
37 safe::{
38 DataEnvelope, DataEnvelopeNamespace, HighEntropySecret, HighEntropySecretSource,
39 KeyEncryptionKey, SealableData, SealableVersionedData, SecretProtectedKeyEnvelope,
40 SecretProtectedKeyEnvelopeNamespace, SymmetricKeyEnvelope, SymmetricKeyEnvelopeNamespace,
41 },
42};
43use bitwarden_encoding::{B64Url, FromStrVisitor};
44use bitwarden_sensitive_value::{Sensitive, SensitiveSlice};
45use rand::Rng;
46use serde::{Deserialize, Serialize};
47use subtle::{Choice, ConstantTimeEq};
48use thiserror::Error;
49use zeroize::Zeroizing;
50
51const INVITE_SECRET_LEN: usize = 32;
54
55#[derive(Debug, Error)]
57pub enum InviteKeyBundleError {
58 #[error("Decoding failed")]
60 DecodingFailed,
61 #[error("Unable to seal invite")]
63 KeySealingFailed,
64 #[error("Unable to unseal invite")]
66 KeyUnsealingFailed,
67 #[error("Missing Key for Id: {0}")]
69 MissingKeyId(String),
70 #[error("Invalid organization private key")]
73 InvalidPrivateKey,
74 #[error("Confirmation is not enabled on this invite")]
76 ConfirmationNotEnabled,
77}
78
79#[cfg(feature = "wasm")]
80#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
81const TS_INVITE_SECRET: &'static str = r#"
82export type InviteSecret = Tagged<string, "InviteSecret">;
83"#;
84
85#[derive(Clone)]
89pub struct InviteSecret(Zeroizing<[u8; INVITE_SECRET_LEN]>);
90
91impl InviteSecret {
92 fn make() -> Self {
94 let mut bytes = Zeroizing::new([0u8; INVITE_SECRET_LEN]);
95 bitwarden_random::rng().fill_bytes(bytes.as_mut_slice());
96 InviteSecret(bytes)
97 }
98}
99
100impl ConstantTimeEq for InviteSecret {
101 fn ct_eq(&self, other: &InviteSecret) -> Choice {
102 self.0.as_slice().ct_eq(other.0.as_slice())
103 }
104}
105
106impl PartialEq for InviteSecret {
107 fn eq(&self, other: &Self) -> bool {
108 self.ct_eq(other).into()
109 }
110}
111
112impl std::fmt::Debug for InviteSecret {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("InviteSecret").finish()
116 }
117}
118
119impl HighEntropySecretSource for InviteSecret {
122 fn provide_high_entropy_bytes(&self) -> SensitiveSlice<'_> {
123 Sensitive::from(self.0.as_slice())
124 }
125}
126
127impl From<&InviteSecret> for String {
128 fn from(secret: &InviteSecret) -> Self {
129 B64Url::from(secret.0.as_slice()).to_string()
130 }
131}
132
133impl FromStr for InviteSecret {
134 type Err = InviteKeyBundleError;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 let data = B64Url::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
138 let bytes: [u8; INVITE_SECRET_LEN] = data
139 .as_bytes()
140 .try_into()
141 .map_err(|_| InviteKeyBundleError::DecodingFailed)?;
142 Ok(InviteSecret(Zeroizing::new(bytes)))
143 }
144}
145
146impl<'de> Deserialize<'de> for InviteSecret {
147 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
148 where
149 D: serde::Deserializer<'de>,
150 {
151 deserializer.deserialize_str(FromStrVisitor::new())
152 }
153}
154
155impl Serialize for InviteSecret {
156 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157 where
158 S: serde::Serializer,
159 {
160 serializer.serialize_str(&String::from(self))
161 }
162}
163
164#[derive(Serialize, Deserialize, Debug, PartialEq)]
170struct InviteDataV1 {
171 public_key_thumbprint: [u8; 32],
173 invite_secret: [u8; INVITE_SECRET_LEN],
175}
176impl SealableData for InviteDataV1 {}
177
178generate_versioned_sealable!(
179 InviteData,
180 DataEnvelopeNamespace::OrganizationInvite,
181 [
182 InviteDataV1 => "1",
183 ]
184);
185
186#[cfg(feature = "wasm")]
187#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
188const TS_INVITE: &'static str = r#"
189export type Invite = Tagged<string, "Invite">;
190"#;
191
192#[derive(Clone, Serialize, Deserialize)]
195pub struct Invite {
196 sealed_invite_data: DataEnvelope,
199 invite_key_sealed_invite_data_cek: SymmetricKeyEnvelope,
202 invite_secret_sealed_invite_key: SecretProtectedKeyEnvelope,
205 invite_key_sealed_organization_key: Option<SymmetricKeyEnvelope>,
211 organization_key_sealed_invite_key: EncString,
214}
215
216impl From<&Invite> for String {
217 fn from(invite: &Invite) -> Self {
218 serde_json::to_string(invite).expect("JSON serialization of Invite never fails")
219 }
220}
221
222impl FromStr for Invite {
223 type Err = InviteKeyBundleError;
224
225 fn from_str(s: &str) -> Result<Self, Self::Err> {
226 serde_json::from_str(s).map_err(|_| InviteKeyBundleError::DecodingFailed)
227 }
228}
229
230impl std::fmt::Debug for Invite {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.debug_struct("Invite")
235 .field("sealed_invite_data", &self.sealed_invite_data)
236 .field(
237 "invite_key_sealed_invite_data_cek",
238 &self.invite_key_sealed_invite_data_cek,
239 )
240 .field(
241 "invite_secret_sealed_invite_key",
242 &self.invite_secret_sealed_invite_key,
243 )
244 .field(
245 "invite_key_sealed_organization_key",
246 &self.invite_key_sealed_organization_key,
247 )
248 .field(
249 "organization_key_sealed_invite_key",
250 &self.organization_key_sealed_invite_key,
251 )
252 .finish()
253 }
254}
255
256impl Invite {
257 pub fn unseal_invite_key_with_organization_key<Ids: KeySlotIds>(
259 &self,
260 organization_key: Ids::Symmetric,
261 ctx: &mut KeyStoreContext<Ids>,
262 ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
263 ctx.unwrap_symmetric_key(organization_key, &self.organization_key_sealed_invite_key)
264 .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
265 }
266
267 pub fn unseal_invite_key_with_invite_secret<Ids: KeySlotIds>(
269 &self,
270 invite_secret: &InviteSecret,
271 ctx: &mut KeyStoreContext<Ids>,
272 ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
273 let secret = HighEntropySecret::from(invite_secret.clone());
274 self.invite_secret_sealed_invite_key
275 .unseal(
276 &secret,
277 SecretProtectedKeyEnvelopeNamespace::OrganizationInvite,
278 ctx,
279 )
280 .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
281 }
282
283 fn unseal_invite_data<Ids: KeySlotIds>(
285 &self,
286 invite_key: Ids::Symmetric,
287 ctx: &mut KeyStoreContext<Ids>,
288 ) -> Result<InviteDataV1, InviteKeyBundleError> {
289 let cek = self
291 .invite_key_sealed_invite_data_cek
292 .unseal(
293 invite_key,
294 SymmetricKeyEnvelopeNamespace::OrganizationInvite,
295 ctx,
296 )
297 .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
298 let data: InviteData = self
299 .sealed_invite_data
300 .unseal(cek, ctx)
301 .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
302 let InviteData::InviteDataV1(data) = data;
303 Ok(data)
304 }
305
306 pub fn get_invite_secret<Ids: KeySlotIds>(
308 &self,
309 invite_key: Ids::Symmetric,
310 ctx: &mut KeyStoreContext<Ids>,
311 ) -> Result<InviteSecret, InviteKeyBundleError> {
312 let data = self.unseal_invite_data(invite_key, ctx)?;
313 Ok(InviteSecret(Zeroizing::new(data.invite_secret)))
314 }
315
316 pub fn get_public_key_thumbprint<Ids: KeySlotIds>(
318 &self,
319 invite_key: Ids::Symmetric,
320 ctx: &mut KeyStoreContext<Ids>,
321 ) -> Result<CoseKeyThumbprint, InviteKeyBundleError> {
322 let data = self.unseal_invite_data(invite_key, ctx)?;
323 Ok(CoseKeyThumbprint::from_bytes(data.public_key_thumbprint))
324 }
325
326 pub fn supports_confirmation(&self) -> bool {
329 self.invite_key_sealed_organization_key.is_some()
330 }
331
332 pub fn unseal_organization_key<Ids: KeySlotIds>(
335 &self,
336 invite_key: Ids::Symmetric,
337 ctx: &mut KeyStoreContext<Ids>,
338 ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
339 self.invite_key_sealed_organization_key
340 .as_ref()
341 .ok_or(InviteKeyBundleError::ConfirmationNotEnabled)?
342 .unseal(
343 invite_key,
344 SymmetricKeyEnvelopeNamespace::OrganizationInvite,
345 ctx,
346 )
347 .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
348 }
349
350 pub fn enable_confirmation<Ids: KeySlotIds>(
352 &mut self,
353 organization_key: Ids::Symmetric,
354 ctx: &mut KeyStoreContext<Ids>,
355 ) -> Result<(), InviteKeyBundleError> {
356 let invite_key = self.unseal_invite_key_with_organization_key(organization_key, ctx)?;
357 let envelope = SymmetricKeyEnvelope::seal(
358 organization_key,
359 invite_key,
360 SymmetricKeyEnvelopeNamespace::OrganizationInvite,
361 ctx,
362 )
363 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
364 self.invite_key_sealed_organization_key = Some(envelope);
365 Ok(())
366 }
367
368 pub fn disable_confirmation(&mut self) {
370 self.invite_key_sealed_organization_key = None;
371 }
372
373 pub fn make_for_private_key<Ids: KeySlotIds>(
383 organization_key: Ids::Symmetric,
384 wrapped_organization_private_key: &EncString,
385 ctx: &mut KeyStoreContext<Ids>,
386 ) -> Result<(InviteSecret, Invite), InviteKeyBundleError> {
387 let private_key_id = ctx
389 .unwrap_private_key(organization_key, wrapped_organization_private_key)
390 .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?;
391 let thumbprint = ctx
392 .get_public_key(private_key_id)
393 .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?
394 .thumbprint()
395 .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?;
396
397 let invite_secret = InviteSecret::make();
398 let invite_key = KeyEncryptionKey::make(ctx);
399
400 let invite_data: InviteData = InviteDataV1 {
404 public_key_thumbprint: *thumbprint.as_bytes(),
405 invite_secret: *invite_secret.0,
406 }
407 .into();
408 let (sealed_invite_data, invite_data_cek) = DataEnvelope::seal(invite_data, ctx)
409 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
410 let invite_key_sealed_invite_data_cek = SymmetricKeyEnvelope::seal(
411 invite_data_cek,
412 invite_key,
413 SymmetricKeyEnvelopeNamespace::OrganizationInvite,
414 ctx,
415 )
416 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
417
418 let secret = HighEntropySecret::from(invite_secret.clone());
420 let invite_secret_sealed_invite_key = SecretProtectedKeyEnvelope::seal(
421 invite_key,
422 &secret,
423 SecretProtectedKeyEnvelopeNamespace::OrganizationInvite,
424 ctx,
425 )
426 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
427
428 let invite_key_sealed_organization_key = Some(
432 SymmetricKeyEnvelope::seal(
433 organization_key,
434 invite_key,
435 SymmetricKeyEnvelopeNamespace::OrganizationInvite,
436 ctx,
437 )
438 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?,
439 );
440
441 let organization_key_sealed_invite_key = ctx
443 .wrap_symmetric_key(organization_key, invite_key)
444 .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
445
446 Ok((
447 invite_secret,
448 Invite {
449 sealed_invite_data,
450 invite_key_sealed_invite_data_cek,
451 invite_secret_sealed_invite_key,
452 invite_key_sealed_organization_key,
453 organization_key_sealed_invite_key,
454 },
455 ))
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use bitwarden_crypto::{
462 CoseKeyThumbprintExt, EncString, KeyStore, KeyStoreContext, PublicKeyEncryptionAlgorithm,
463 SymmetricCryptoKey, key_slot_ids,
464 };
465 use bitwarden_encoding::{B64, B64Url};
466
467 use crate::invite::{Invite, InviteKeyBundleError, InviteSecret};
468
469 const TEST_VECTOR_ORG_KEY: &str =
473 "KGP9Nc2/91w+42Z9VzY0m7h18avuZcq4ICM8Rhdc3BD92LbWS2TQkVBzavvUM684WKXiC22NJi2EwaiDW4YTAA==";
474 const TEST_VECTOR_WRAPPED_PRIVATE_KEY: &str = "2.bi9TWF/zrujUg1y+v8ECtQ==|kEMkuRt42j65YZnPEc4bLOT0/WDZwWSNJNGlUMdr/LRF3qi/vCnZ7eT0+7MruTccmyoAjKmsXdoBcufrOdPBUguFQn1LQMGqHqCyyB3SIijOlLyOOmxWYqoLUjihy8o8URGWjrAGWZnkeYWHTlFZP09Fag2xCwiQ/qS32Q+qTGGHDs0FiwjplcPkW9knlmgCXbuyPqDnEYoa0Qs/CC1hUCzFFrWs7QkE+5eLaNHxuBPpsrY6y795kEu9ve38F3piY9b6lQpc/iPGv8Zfh1isI7Mpy1zMwntXGSHjUOy17nPxCqkgYufuNGnwGNwsGjkLAl7e7bD39SYfEpTDaRUgmTl8UrZDx274e6Um1LLvokf3HiL1tboJ9/TW8IiMuAdrb3PLTH6Sep8lqZ3WhNADfMMJle9kCojHp6XSB14in1JqP0636exYeJu+FhUC1TfrthuQN2QDQ8LAvgZR7YvzkTJX3Sc5jP7m/yCmCbHhIqIAaGqJsRwAee0EMsKcALz3/akVoyjHU2LHD3dzQnyMyszyRNYViBNYAM9qBN2DqwWRDOtM171xNVJcTFsAh4mBSLiLOlDsXqLqHVKu2VJNE1XhTQ5Szubqefa/Or7nfxXcxDvivqZ7d5NfDFEskUMqh5yq1KoLK0oK5c+KvY/COIZr/kct+qtfZsXo3w5xJnPOqrAKGm+9CF4OINpLM8Z3csdZf9l5XjlmO1kIuBbquQZ0EZCHzD/GEfXRGB8BEkugdTfpnTDtmmuAJXkIY6t6e6pRUU9u4/sl+U7Iuh22fOA59SuQOElr5Lxre+hQBrRfJS3tSMEtMjYhVmltrngH+SLRxMxH/evbe2uvNaaxlFJe6EK1vqchyTX6nM8Z+2Yjb5pOAzrKQYqwwmVys9IHfjXhybqv10gFpDXBE/eq8u9xs9LbQQ03EbveQUtqdh/ms8SxLOQA9Sm9JwHEL0Zni+8NdAa5orDYzOi53bQLgrs+uUldgB/KOW2goTnKGm4YTbMAXEbum8pST8EXB9jNDXyofyN8IQUoLRvVkEgzbSPBS1sYpkkKdLZy3ojOCKnMnHXIVtzJojFckiutbj6d927X5w51E/RDMoAdylGRVKnmqLKysFRqL+pZK8Cyo+ECr7notG8kr7pVnofzigjKZS9qkRmqEa9bju1GgI20g4cxro8/0O0XnU1o0Mx46qH3niORY59i5bdMwaDS6H2c6+rmf9bIFwwgyAZvHlVdcGNoBGPR3ZXHThwI1OmWSslLWVW0IaS4utB1jL4zvHPCqh/ButA8HeRmU/NYSfaqb9YXyzn+C7ED15MWXkYmZzeE38HHxhqs12+oj+WFcg4d5/e2UcccuVi36SWhA0xWk8Kk2D6e1Pz1lmaw20vpb0eUq4AS5ZnMmWTEiKORFeGNTIROq9vuPuitLrREedu9PGjf5aeKcNqlq2nr7fOaxyi2ocKs7pLqVBUH1G7zHpCo3Rt1+o18guXFHT56vQFfkzoUNXiS6TRM4Mkl3s0TyGgBhxZkNJleTI6y4xhfH1iathBnLcfelLbxZhDB1wh3RXowS32jpM/J4pSUuNEmSLSqRQtRJY41BG00nYY02qEbakkgk6pS6a0/CWphyLfHAzUWbabOhqR1iPN9/ZiecjI=|eoklmBw+LYy2NNwjLOuA4+szKH5SzLGPlrhIJ/vfmW4=";
475 const TEST_VECTOR_INVITE: &str = r#"{"sealed_invite_data":"g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUPw60HtcnAwO6kRKd7MnQz86AAE4gQI6AAE4gAKhBUz+fzObmLDLKRDeBJJYxX9qAxhXIe1Ri1CJw3ojv7WUEwBpVWeMEGTK7HHbp9WnTDjK849psMx6EOpQ7B/BYGiO28Zn0tjtQ85zwShii4FRK39mtJFE8XxV46hxW9+LlH6EPt4yfzHVkTNZ3vgOiSXcs6zyKXIhQaz6yh8eyA653m4DJEHV00JjdQ/jSfTEfyDf6LiflGntDZ7gSYlQMRxuS71yVP9Mgm79yFY4aIw703G/HUjvIEt4XlWCLzvaLDmegpDz9z4eQCVk8v8JM1fS7BsA","invite_key_sealed_invite_data_cek":"g1g+pgE6AAEReQMYZQRQ7raDdes1HnAgiuiZWXyS9ToAARVcUPw60HtcnAwO6kRKd7MnQz86AAE4gQM6AAE4gAKhBVgYvxSL4XPZLwEXHPnEWweqeeKjyH9r85/ZWE3rsZy1MLmzkFOCJa8a0o20b1P3IH6c5NYtT3v1a50+X0GmgDKiMf/omjhwRRQ7ua6wK0O2JBlhM4JIE8jmtsyMd46mFDbURK1kGRvl7Q==","invite_secret_sealed_invite_key":"hFgopQEDAxhlOgABFVxQ7raDdes1HnAgiuiZWXyS9ToAATiBBjoAATiAAaEFTJvCToVTNSe1uWbD5FhUf061kAAj1sMGWjJ8IWPV2e2xk5Z+ownZ1RqUXG2jv9h2vnsEFZ2yglvKaSCGTsOuvOXv0ESwQk6eUtFAaZjxV/Rajyuu6YgEa712Tfb9Jz13QLGngYNAogEpM1gg1MsuVokUsK8WkqggHRdJ4jvzFsbN/bP4g0l+j6f97qb2","invite_key_sealed_organization_key":"g1hKpQE6AAEReQN4ImFwcGxpY2F0aW9uL3guYml0d2FyZGVuLmxlZ2FjeS1rZXkEUO62g3XrNR5wIIromVl8kvU6AAE4gQM6AAE4gAKhBVgYoth3hg+yUTLXF4ksaeT42IWGKuTv27B4WFAKV8Z7uKGZNHOONGgZQLbQMozgYX9tseuet413M314W0sV3cBKZIRSEZfj3NvHU8tE/6b2oGxPQIPKP3Tyhl84zhI4uG+Mo5WKkvtdPonD0A==","organization_key_sealed_invite_key":"2.XjZXnAwXK/cXCmHNgCPQzw==|WEGa37JPdNWVnMPWlfinyvZdXpMW8kpTBypXPWf/abbG4/+6vp/WJQmVlkVEflEkuZwjKWSkMWJPcACGBoabeBHgzqpkSYf3kyQb7VhePHQ=|kZLDCLGm1nCJhlVuNahFiZOecy5tKG2fXGCNZZzbDjk="}"#;
476 const TEST_VECTOR_INVITE_SECRET: &str = "Ttas45_CvZi1yoFJ3bMCHx0DAAGGxDi-1BhHCutwDjI";
477
478 fn load_test_vectors(ctx: &mut KeyStoreContext<'_, TestIds>) -> EncString {
482 let org_key =
483 SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
484 let local_org_key_id = ctx.add_local_symmetric_key(org_key);
485 ctx.persist_symmetric_key(local_org_key_id, TestSymmKey::Organization)
486 .unwrap();
487 TEST_VECTOR_WRAPPED_PRIVATE_KEY.parse().unwrap()
488 }
489
490 #[test]
491 fn test_basic_invitation_bundle() {
492 let key_store = KeyStore::<TestIds>::default();
493 let mut ctx = key_store.context_mut();
494 let wrapped_private_key = load_test_vectors(&mut ctx);
495
496 let (secret1, _) =
497 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
498 .unwrap();
499 let (secret2, _) =
500 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
501 .unwrap();
502
503 assert_ne!(secret1, secret2);
504 }
505
506 #[test]
507 fn test_admin_recovers_invite_secret() {
508 let key_store = KeyStore::<TestIds>::default();
509 let mut ctx = key_store.context_mut();
510 let wrapped_private_key = load_test_vectors(&mut ctx);
511
512 let (invite_secret, invite) =
513 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
514 .unwrap();
515
516 let invite_key = invite
517 .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
518 .unwrap();
519 let recovered = invite.get_invite_secret(invite_key, &mut ctx).unwrap();
520
521 assert_eq!(invite_secret, recovered);
522 }
523
524 #[test]
525 fn test_admin_recovers_thumbprint() {
526 let key_store = KeyStore::<TestIds>::default();
527 let mut ctx = key_store.context_mut();
528 let wrapped_private_key = load_test_vectors(&mut ctx);
529
530 let private_key_id = ctx
533 .unwrap_private_key(TestSymmKey::Organization, &wrapped_private_key)
534 .unwrap();
535 let expected = ctx
536 .get_public_key(private_key_id)
537 .unwrap()
538 .thumbprint()
539 .unwrap();
540
541 let (_, invite) =
542 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
543 .unwrap();
544
545 let invite_key = invite
546 .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
547 .unwrap();
548 let recovered = invite
549 .get_public_key_thumbprint(invite_key, &mut ctx)
550 .unwrap();
551
552 assert_eq!(recovered, expected);
553 }
554
555 #[test]
556 fn test_invitee_recovers_organization_key() {
557 let key_store = KeyStore::<TestIds>::default();
558 let mut ctx = key_store.context_mut();
559 let wrapped_private_key = load_test_vectors(&mut ctx);
560
561 let (invite_secret, invite) =
562 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
563 .unwrap();
564
565 let invite_key = invite
568 .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
569 .unwrap();
570 let recovered_org_key_id = invite
571 .unseal_organization_key(invite_key, &mut ctx)
572 .unwrap();
573
574 ctx.assert_symmetric_keys_equal(recovered_org_key_id, TestSymmKey::Organization);
575 }
576
577 #[test]
578 fn test_confirmation_toggle() {
579 let key_store = KeyStore::<TestIds>::default();
580 let mut ctx = key_store.context_mut();
581 let wrapped_private_key = load_test_vectors(&mut ctx);
582
583 let (invite_secret, mut invite) =
584 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
585 .unwrap();
586
587 assert!(invite.supports_confirmation());
589
590 invite.disable_confirmation();
593 assert!(!invite.supports_confirmation());
594 let invite_key = invite
595 .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
596 .unwrap();
597 assert!(matches!(
598 invite.unseal_organization_key(invite_key, &mut ctx),
599 Err(InviteKeyBundleError::ConfirmationNotEnabled)
600 ));
601
602 invite
604 .enable_confirmation(TestSymmKey::Organization, &mut ctx)
605 .unwrap();
606 assert!(invite.supports_confirmation());
607 let invite_key = invite
608 .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
609 .unwrap();
610 let recovered_org_key_id = invite
611 .unseal_organization_key(invite_key, &mut ctx)
612 .unwrap();
613 ctx.assert_symmetric_keys_equal(recovered_org_key_id, TestSymmKey::Organization);
614 }
615
616 #[test]
617 fn test_invite_string_round_trip() {
618 let key_store = KeyStore::<TestIds>::default();
619 let mut ctx = key_store.context_mut();
620 let wrapped_private_key = load_test_vectors(&mut ctx);
621
622 let (invite_secret, invite) =
623 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
624 .unwrap();
625
626 let encoded = String::from(&invite);
627 let decoded: Invite = encoded.parse().unwrap();
628 assert_eq!(String::from(&decoded), encoded);
629
630 let invite_key = decoded
632 .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
633 .unwrap();
634 let recovered = decoded.get_invite_secret(invite_key, &mut ctx).unwrap();
635 assert_eq!(invite_secret, recovered);
636
637 let json = serde_json::to_string(&invite).unwrap();
639 let from_json: Invite = serde_json::from_str(&json).unwrap();
640 assert_eq!(String::from(&from_json), encoded);
641 }
642
643 #[test]
644 fn test_wrong_invite_secret_fails() {
645 let key_store = KeyStore::<TestIds>::default();
646 let mut ctx = key_store.context_mut();
647 let wrapped_private_key = load_test_vectors(&mut ctx);
648
649 let (_, invite) =
650 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
651 .unwrap();
652
653 let wrong_secret = InviteSecret(zeroize::Zeroizing::new([7u8; 32]));
655 assert!(matches!(
656 invite.unseal_invite_key_with_invite_secret(&wrong_secret, &mut ctx),
657 Err(InviteKeyBundleError::KeyUnsealingFailed)
658 ));
659 }
660
661 #[test]
662 fn test_wrong_organization_key_fails() {
663 let key_store = KeyStore::<TestIds>::default();
664 let mut ctx = key_store.context_mut();
665 let wrapped_private_key = load_test_vectors(&mut ctx);
666
667 let (_, invite) =
668 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
669 .unwrap();
670
671 let wrong_org_key = ctx.generate_symmetric_key();
673 assert!(matches!(
674 invite.unseal_invite_key_with_organization_key(wrong_org_key, &mut ctx),
675 Err(InviteKeyBundleError::KeyUnsealingFailed)
676 ));
677 }
678
679 #[test]
680 fn test_invitee_recovers_thumbprint() {
681 let key_store = KeyStore::<TestIds>::default();
682 let mut ctx = key_store.context_mut();
683 let wrapped_private_key = load_test_vectors(&mut ctx);
684
685 let private_key_id = ctx
686 .unwrap_private_key(TestSymmKey::Organization, &wrapped_private_key)
687 .unwrap();
688 let expected = ctx
689 .get_public_key(private_key_id)
690 .unwrap()
691 .thumbprint()
692 .unwrap();
693
694 let (invite_secret, invite) =
695 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
696 .unwrap();
697
698 let invite_key = invite
701 .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
702 .unwrap();
703 let recovered = invite
704 .get_public_key_thumbprint(invite_key, &mut ctx)
705 .unwrap();
706
707 assert_eq!(recovered, expected);
708 }
709
710 #[test]
711 fn test_malformed_invite_string_fails() {
712 assert!(matches!(
714 "not valid json !!!".parse::<Invite>(),
715 Err(InviteKeyBundleError::DecodingFailed)
716 ));
717
718 assert!(matches!(
720 "{}".parse::<Invite>(),
721 Err(InviteKeyBundleError::DecodingFailed)
722 ));
723 }
724
725 #[test]
726 fn test_invalid_wrapped_private_key_fails() {
727 let key_store = KeyStore::<TestIds>::default();
728 let mut ctx = key_store.context_mut();
729 let wrapped_private_key = load_test_vectors(&mut ctx);
730
731 let wrong_org_key = ctx.generate_symmetric_key();
734 assert!(matches!(
735 Invite::make_for_private_key(wrong_org_key, &wrapped_private_key, &mut ctx),
736 Err(InviteKeyBundleError::InvalidPrivateKey)
737 ));
738 }
739
740 #[test]
741 fn test_invite_secret_into_base64_url() {
742 let data: [u8; 32] = *b"+/=Hello, World!AAAAAAAAAAAAAAAA";
743 let secret = InviteSecret(zeroize::Zeroizing::new(data));
744
745 let encoded = String::from(&secret);
746
747 assert_eq!(encoded, "Ky89SGVsbG8sIFdvcmxkIUFBQUFBQUFBQUFBQUFBQUE");
748 assert!(!encoded.contains('+'));
749 assert!(!encoded.contains('/'));
750 assert!(!encoded.contains('='));
751
752 let decoded = B64Url::try_from(encoded.as_str()).unwrap();
753 assert_eq!(decoded.as_bytes(), data);
754
755 let reparsed: InviteSecret = encoded.parse().unwrap();
757 assert_eq!(reparsed, secret);
758 }
759
760 #[test]
761 #[ignore = "Manual test to generate test vectors"]
762 fn generate_test_vectors() {
763 let key_store = KeyStore::<TestIds>::default();
764 let mut ctx = key_store.context_mut();
765
766 let org_key =
767 SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
768 let org_key_id = ctx.add_local_symmetric_key(org_key);
769
770 let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
772 let wrapped_private_key = ctx.wrap_private_key(org_key_id, private_key_id).unwrap();
773
774 let (invite_secret, invite) =
775 Invite::make_for_private_key(org_key_id, &wrapped_private_key, &mut ctx).unwrap();
776
777 println!(
778 "const TEST_VECTOR_WRAPPED_PRIVATE_KEY: &str = \"{}\";",
779 wrapped_private_key.to_string()
780 );
781 println!(
782 "const TEST_VECTOR_INVITE: &str = r#\"{}\"#;",
783 String::from(&invite)
784 );
785 println!(
786 "const TEST_VECTOR_INVITE_SECRET: &str = \"{}\";",
787 String::from(&invite_secret)
788 );
789 }
790
791 #[test]
792 fn test_invite_test_vector() {
793 let key_store = KeyStore::<TestIds>::default();
794 let mut ctx = key_store.context_mut();
795
796 let org_key =
797 SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_ORG_KEY).unwrap()).unwrap();
798 let org_key_id = ctx.add_local_symmetric_key(org_key);
799
800 let invite: Invite = TEST_VECTOR_INVITE.parse().unwrap();
801 let invite_key = invite
802 .unseal_invite_key_with_organization_key(org_key_id, &mut ctx)
803 .unwrap();
804 let recovered = invite.get_invite_secret(invite_key, &mut ctx).unwrap();
805
806 assert_eq!(String::from(&recovered), TEST_VECTOR_INVITE_SECRET);
807 }
808
809 #[test]
810 #[ignore = "Manual test to verify debug format"]
811 fn test_debug() {
812 let key_store = KeyStore::<TestIds>::default();
813 let mut ctx = key_store.context_mut();
814 let wrapped_private_key = load_test_vectors(&mut ctx);
815
816 let (invite_secret, invite) =
817 Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
818 .unwrap();
819 println!("{invite_secret:?}");
820 println!("{invite:?}");
821 }
822
823 key_slot_ids! {
824 #[symmetric]
825 pub enum TestSymmKey {
826 Organization,
827 #[local]
828 Local(LocalId),
829 }
830
831 #[private]
832 pub enum TestPrivateKey {
833 A(u8),
834 B,
835 #[local]
836 C(LocalId),
837 }
838
839 #[signing]
840 pub enum TestSigningKey {
841 A(u8),
842 B,
843 #[local]
844 C(LocalId),
845 }
846
847 pub TestIds => TestSymmKey, TestPrivateKey, TestSigningKey;
848 }
849}