1use std::sync::Arc;
2
3use bitwarden_api_api::models::{
4 AcceptOrganizationInviteLinkRequestModel, ConfirmOrganizationInviteLinkRequestModel,
5 CreateOrganizationInviteLinkRequestModel, GetOrganizationInviteRequestModel,
6 RefreshOrganizationInviteLinkRequestModel,
7};
8use bitwarden_core::{
9 ApiError, Client, FromClient, MissingFieldError, OrganizationId,
10 client::ApiConfigurations,
11 key_management::{KeySlotIds, PrivateKeySlotId, SymmetricKeySlotId},
12 require,
13};
14use bitwarden_crypto::{
15 CoseKeyThumbprintExt, CryptoError, EncString, KeyStore, PrimitiveEncryptable, PublicKey,
16 SpkiPublicKeyBytes, UnsignedSharedKey,
17};
18use bitwarden_encoding::B64;
19use bitwarden_error::bitwarden_error;
20use bitwarden_organization_crypto::invite::{Invite, InviteKeyBundleError, InviteSecret};
21use thiserror::Error;
22#[cfg(feature = "wasm")]
23use wasm_bindgen::prelude::wasm_bindgen;
24
25use crate::OrganizationInviteLink;
26
27#[bitwarden_error(flat)]
29#[derive(Debug, Error)]
30pub enum InviteLinkError {
31 #[error(transparent)]
33 Invite(#[from] InviteKeyBundleError),
34 #[error(transparent)]
36 Api(#[from] ApiError),
37 #[error(transparent)]
40 Crypto(#[from] CryptoError),
41 #[error(transparent)]
43 MissingField(#[from] MissingFieldError),
44 #[error("Failed to parse `{0}`")]
46 ParseFailure(&'static str),
47 #[error("Account recovery public key does not match the invite's bound organization key")]
50 RecoveryKeyMismatch,
51}
52
53#[cfg_attr(feature = "wasm", wasm_bindgen)]
55#[derive(FromClient)]
56pub struct InviteLinkClient {
57 pub(crate) key_store: KeyStore<KeySlotIds>,
58 pub(crate) api_configurations: Arc<ApiConfigurations>,
59}
60
61#[cfg_attr(feature = "wasm", wasm_bindgen)]
62impl InviteLinkClient {
63 pub async fn create_invite_link(
71 &self,
72 organization_id: OrganizationId,
73 allowed_domains: Vec<String>,
74 supports_confirmation: bool,
75 ) -> Result<OrganizationInviteLink, InviteLinkError> {
76 let invite = self
77 .make_invite(organization_id, supports_confirmation)
78 .await?;
79
80 let response = self
81 .api_configurations
82 .api_client
83 .organization_invite_links_api()
84 .create(
85 organization_id.into(),
86 Some(CreateOrganizationInviteLinkRequestModel {
87 allowed_domains,
88 invite: String::from(&invite),
89 supports_confirmation: invite.supports_confirmation(),
90 }),
91 )
92 .await?;
93
94 OrganizationInviteLink::try_from(response)
95 }
96
97 pub async fn refresh_invite_link(
100 &self,
101 organization_id: OrganizationId,
102 supports_confirmation: bool,
103 ) -> Result<OrganizationInviteLink, InviteLinkError> {
104 let invite = self
105 .make_invite(organization_id, supports_confirmation)
106 .await?;
107
108 let response = self
109 .api_configurations
110 .api_client
111 .organization_invite_links_api()
112 .refresh(
113 organization_id.into(),
114 Some(RefreshOrganizationInviteLinkRequestModel {
115 invite: String::from(&invite),
116 supports_confirmation: invite.supports_confirmation(),
117 }),
118 )
119 .await?;
120
121 OrganizationInviteLink::try_from(response)
122 }
123
124 #[cfg_attr(feature = "wasm", wasm_bindgen(unchecked_return_type = "InviteSecret"))]
127 pub fn get_invite_secret(
128 &self,
129 organization_id: OrganizationId,
130 invite: Invite,
131 ) -> Result<InviteSecret, InviteLinkError> {
132 let mut ctx = self.key_store.context();
133 let org_key = SymmetricKeySlotId::Organization(organization_id);
134 let invite_key = invite.unseal_invite_key_with_organization_key(org_key, &mut ctx)?;
135 let invite_secret = invite.get_invite_secret(invite_key, &mut ctx)?;
136 Ok(invite_secret)
137 }
138
139 pub async fn accept_and_optionally_confirm(
143 &self,
144 organization_id: OrganizationId,
145 code: String,
146 invite_secret: InviteSecret,
147 default_collection_name: String,
148 enroll_into_account_recovery: bool,
149 ) -> Result<(), InviteLinkError> {
150 let code =
151 uuid::Uuid::parse_str(&code).map_err(|_| InviteLinkError::ParseFailure("code"))?;
152
153 let recovery_public_key = if enroll_into_account_recovery {
156 let response = self
157 .api_configurations
158 .api_client
159 .organizations_api()
160 .get_public_key(&organization_id.to_string())
161 .await?;
162 Some(
163 require!(response.public_key)
164 .parse::<B64>()
165 .map_err(|_| InviteLinkError::ParseFailure("public_key"))?,
166 )
167 } else {
168 None
169 };
170
171 let invite_response = self
172 .api_configurations
173 .api_client
174 .organization_users_api()
175 .get_invite(Some(GetOrganizationInviteRequestModel {
176 organization_id: organization_id.into(),
177 code,
178 }))
179 .await?;
180
181 let invite: Invite = require!(invite_response.invite).parse()?;
182
183 let request = {
186 let mut ctx = self.key_store.context();
187
188 let invite_key =
190 invite.unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)?;
191
192 let reset_password_key = match &recovery_public_key {
197 Some(recovery_public_key) => {
198 let recovery_public_key =
199 PublicKey::from_der(&SpkiPublicKeyBytes::from(recovery_public_key))?;
200 let bound_thumbprint =
201 invite.get_public_key_thumbprint(invite_key, &mut ctx)?;
202 if bound_thumbprint != recovery_public_key.thumbprint()? {
203 return Err(InviteLinkError::RecoveryKeyMismatch);
204 }
205 Some(
206 UnsignedSharedKey::encapsulate(
207 SymmetricKeySlotId::User,
208 &recovery_public_key,
209 &ctx,
210 )?
211 .to_string(),
212 )
213 }
214 None => None,
215 };
216
217 if invite.supports_confirmation() {
218 let org_key = invite.unseal_organization_key(invite_key, &mut ctx)?;
220 let user_public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)?;
221 let org_user_key =
222 UnsignedSharedKey::encapsulate(org_key, &user_public_key, &ctx)?.to_string();
223 let default_user_collection_name = default_collection_name
224 .encrypt(&mut ctx, org_key)?
225 .to_string();
226 PendingPost::Confirm(ConfirmOrganizationInviteLinkRequestModel {
227 organization_id: organization_id.into(),
228 code,
229 org_user_key,
230 reset_password_key,
231 default_user_collection_name,
232 })
233 } else {
234 PendingPost::Accept(AcceptOrganizationInviteLinkRequestModel {
235 organization_id: organization_id.into(),
236 code,
237 reset_password_key,
238 })
239 }
240 };
241
242 let organization_users_api = self.api_configurations.api_client.organization_users_api();
243 match request {
244 PendingPost::Confirm(model) => {
245 organization_users_api
246 .confirm_invite_link(Some(model))
247 .await?
248 }
249 PendingPost::Accept(model) => {
250 organization_users_api
251 .accept_invite_link(Some(model))
252 .await?
253 }
254 }
255
256 Ok(())
257 }
258
259 async fn make_invite(
261 &self,
262 organization_id: OrganizationId,
263 supports_confirmation: bool,
264 ) -> Result<Invite, InviteLinkError> {
265 let wrapped_private_key_response = self
266 .api_configurations
267 .api_client
268 .organizations_api()
269 .get_private_key(organization_id.into())
270 .await?;
271
272 let wrapped_private_key: EncString =
273 require!(wrapped_private_key_response.private_key).parse()?;
274
275 let mut ctx = self.key_store.context();
276 let org_key = SymmetricKeySlotId::Organization(organization_id);
277 let (_, mut invite) =
278 Invite::make_for_private_key(org_key, &wrapped_private_key, &mut ctx)?;
279
280 if !supports_confirmation {
282 invite.disable_confirmation();
283 }
284
285 Ok(invite)
286 }
287}
288
289enum PendingPost {
292 Confirm(ConfirmOrganizationInviteLinkRequestModel),
293 Accept(AcceptOrganizationInviteLinkRequestModel),
294}
295
296pub trait InviteLinkClientExt {
298 fn invite_link(&self) -> InviteLinkClient;
300}
301
302impl InviteLinkClientExt for Client {
303 fn invite_link(&self) -> InviteLinkClient {
304 InviteLinkClient::from_client(self)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use bitwarden_api_api::{
311 apis::ApiClient,
312 models::{
313 OrganizationInviteLinkResponseModel, OrganizationInviteResponseModel,
314 OrganizationPrivateKeyResponseModel, OrganizationPublicKeyResponseModel,
315 },
316 };
317 use bitwarden_core::{
318 client::ApiConfigurations, key_management::create_test_crypto_with_user_and_org_key,
319 };
320 use bitwarden_crypto::{
321 PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, SymmetricKeyAlgorithm,
322 };
323
324 use super::*;
325
326 fn make_client(org_id: OrganizationId, api_client: ApiClient) -> InviteLinkClient {
327 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
328 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
329 let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
330 {
333 let mut ctx = key_store.context_mut();
334 let local = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
335 ctx.persist_private_key(local, PrivateKeySlotId::UserPrivateKey)
336 .expect("persisting the user private key should work");
337 }
338 InviteLinkClient {
339 key_store,
340 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
341 }
342 }
343
344 fn wrapped_org_private_key(client: &InviteLinkClient, org_id: OrganizationId) -> String {
347 let mut ctx = client.key_store.context();
348 let org_key = SymmetricKeySlotId::Organization(org_id);
349 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
350 ctx.wrap_private_key(org_key, private_key)
351 .unwrap()
352 .to_string()
353 }
354
355 fn echo_link_response(
358 org_id: uuid::Uuid,
359 allowed_domains: Vec<String>,
360 invite: String,
361 supports_confirmation: bool,
362 ) -> OrganizationInviteLinkResponseModel {
363 OrganizationInviteLinkResponseModel {
364 object: None,
365 id: Some(uuid::Uuid::new_v4()),
366 code: Some(uuid::Uuid::new_v4()),
367 organization_id: Some(org_id),
368 allowed_domains: Some(allowed_domains),
369 invite: Some(invite),
370 supports_confirmation: Some(supports_confirmation),
371 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
372 }
373 }
374
375 fn build_invite(
378 client: &InviteLinkClient,
379 org_id: OrganizationId,
380 ) -> (InviteSecret, Invite, B64) {
381 let mut ctx = client.key_store.context();
382 let org_key = SymmetricKeySlotId::Organization(org_id);
383 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
384 let org_public_key = B64::from(
385 ctx.get_public_key(private_key)
386 .unwrap()
387 .to_der()
388 .unwrap()
389 .as_ref(),
390 );
391 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
392 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
393 (secret, invite, org_public_key)
394 }
395
396 #[tokio::test]
401 #[ignore = "Manual test to generate integration-test fixtures"]
402 async fn generate_integration_test_fixtures() {
403 let org_id: OrganizationId = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap();
404 let core = Client::init_test_account(
405 bitwarden_core::client::test_accounts::test_bitwarden_com_account(),
406 )
407 .await;
408 let client = core.invite_link();
409
410 let mut ctx = client.key_store.context();
411 let org_key = SymmetricKeySlotId::Organization(org_id);
412 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
413 let public_key = B64::from(
414 ctx.get_public_key(private_key)
415 .unwrap()
416 .to_der()
417 .unwrap()
418 .as_ref(),
419 );
420 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
421 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
422
423 let mut no_confirmation = invite.clone();
427 no_confirmation.disable_confirmation();
428 assert!(invite.supports_confirmation() && !no_confirmation.supports_confirmation());
429
430 println!("TEST_ORG_WRAPPED_PRIVATE_KEY = {}", wrapped.to_string());
431 println!("TEST_ORG_PUBLIC_KEY = {public_key}");
432 println!("TEST_INVITE = {}", String::from(&invite));
433 println!(
434 "TEST_INVITE_NO_CONFIRMATION = {}",
435 String::from(&no_confirmation)
436 );
437 println!("TEST_INVITE_SECRET = {}", String::from(&secret));
438 }
439
440 #[tokio::test]
441 async fn create_invite_link_posts_and_returns_link_without_confirmation() {
442 let org_id = OrganizationId::new_v4();
443 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
444 let for_mock = wrapped.clone();
445 let client = make_client(
446 org_id,
447 ApiClient::new_mocked(move |mock| {
448 mock.organizations_api
449 .expect_get_private_key()
450 .returning(move |_org| {
451 Ok(OrganizationPrivateKeyResponseModel {
452 object: None,
453 private_key: for_mock.lock().unwrap().clone(),
454 })
455 })
456 .once();
457 mock.organization_invite_links_api
458 .expect_create()
459 .returning(|org, model| {
460 let model = model.unwrap();
461 Ok(echo_link_response(
462 org,
463 model.allowed_domains,
464 model.invite,
465 model.supports_confirmation,
466 ))
467 })
468 .once();
469 }),
470 );
471 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
472
473 let link = client
474 .create_invite_link(org_id, vec!["example.com".to_string()], false)
475 .await
476 .unwrap();
477
478 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
479 assert!(!String::from(&link.invite).is_empty());
480 assert!(!link.invite.supports_confirmation());
481 }
482
483 #[tokio::test]
484 async fn create_invite_link_posts_and_returns_link_with_confirmation() {
485 let org_id = OrganizationId::new_v4();
486 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
487 let for_mock = wrapped.clone();
488 let client = make_client(
489 org_id,
490 ApiClient::new_mocked(move |mock| {
491 mock.organizations_api
492 .expect_get_private_key()
493 .returning(move |_org| {
494 Ok(OrganizationPrivateKeyResponseModel {
495 object: None,
496 private_key: for_mock.lock().unwrap().clone(),
497 })
498 })
499 .once();
500 mock.organization_invite_links_api
501 .expect_create()
502 .returning(|org, model| {
503 let model = model.unwrap();
504 Ok(echo_link_response(
505 org,
506 model.allowed_domains,
507 model.invite,
508 model.supports_confirmation,
509 ))
510 })
511 .once();
512 }),
513 );
514 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
515
516 let link = client
517 .create_invite_link(org_id, vec!["example.com".to_string()], true)
518 .await
519 .unwrap();
520
521 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
522 assert!(!String::from(&link.invite).is_empty());
523 assert!(link.invite.supports_confirmation());
524 }
525
526 #[tokio::test]
527 async fn create_invite_link_two_calls_produce_different_invites() {
528 let org_id = OrganizationId::new_v4();
529 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
530 let for_mock = wrapped.clone();
531 let client = make_client(
532 org_id,
533 ApiClient::new_mocked(move |mock| {
534 mock.organizations_api
535 .expect_get_private_key()
536 .returning(move |_org| {
537 Ok(OrganizationPrivateKeyResponseModel {
538 object: None,
539 private_key: for_mock.lock().unwrap().clone(),
540 })
541 })
542 .times(2);
543 mock.organization_invite_links_api
544 .expect_create()
545 .returning(|org, model| {
546 let model = model.unwrap();
547 Ok(echo_link_response(
548 org,
549 model.allowed_domains,
550 model.invite,
551 model.supports_confirmation,
552 ))
553 })
554 .times(2);
555 }),
556 );
557 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
558
559 let link1 = client
560 .create_invite_link(org_id, vec![], false)
561 .await
562 .unwrap();
563 let link2 = client
564 .create_invite_link(org_id, vec![], false)
565 .await
566 .unwrap();
567
568 assert_ne!(String::from(&link1.invite), String::from(&link2.invite));
569 }
570
571 #[tokio::test]
572 async fn create_invite_link_with_unknown_organization_id_fails() {
573 let org_id = OrganizationId::new_v4();
574 let other_org_id = OrganizationId::new_v4();
575 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
576 let for_mock = wrapped.clone();
577 let client = make_client(
578 org_id,
579 ApiClient::new_mocked(move |mock| {
580 mock.organizations_api
581 .expect_get_private_key()
582 .returning(move |_org| {
583 Ok(OrganizationPrivateKeyResponseModel {
584 object: None,
585 private_key: for_mock.lock().unwrap().clone(),
586 })
587 })
588 .once();
589 }),
590 );
591 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
594
595 let result = client.create_invite_link(other_org_id, vec![], false).await;
596
597 assert!(matches!(result, Err(InviteLinkError::Invite(_))));
598 }
599
600 #[tokio::test]
601 async fn create_invite_link_surfaces_api_errors() {
602 let org_id = OrganizationId::new_v4();
603 let client = make_client(
604 org_id,
605 ApiClient::new_mocked(|mock| {
606 mock.organizations_api
607 .expect_get_private_key()
608 .returning(|_org| Err(std::io::Error::other("boom").into()));
609 }),
610 );
611
612 let result = client.create_invite_link(org_id, vec![], false).await;
613
614 assert!(matches!(result, Err(InviteLinkError::Api(_))));
615 }
616
617 #[tokio::test]
618 async fn get_invite_secret_round_trips_to_the_invite_secret() {
619 let org_id = OrganizationId::new_v4();
620 let client = make_client(org_id, ApiClient::new_mocked(|_| {}));
621
622 let (_secret, invite, _org_public_key) = build_invite(&client, org_id);
624 let secret = client.get_invite_secret(org_id, invite).unwrap();
625 assert!(!String::from(&secret).is_empty());
626 }
627
628 #[tokio::test]
629 async fn accept_and_confirm_succeeds_for_confirmable_invite() {
630 let org_id = OrganizationId::new_v4();
631 let recovery = Arc::new(std::sync::Mutex::new(None::<String>));
634 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
635 let recovery_mock = recovery.clone();
636 let invite_mock = invite_cell.clone();
637 let client = make_client(
638 org_id,
639 ApiClient::new_mocked(move |mock| {
640 mock.organizations_api
641 .expect_get_public_key()
642 .returning(move |_id| {
643 Ok(OrganizationPublicKeyResponseModel {
644 object: None,
645 public_key: recovery_mock.lock().unwrap().clone(),
646 })
647 })
648 .once();
649 mock.organization_users_api
650 .expect_get_invite()
651 .returning(move |_model| {
652 Ok(OrganizationInviteResponseModel {
653 invite: invite_mock.lock().unwrap().clone(),
654 })
655 })
656 .once();
657 mock.organization_users_api
658 .expect_confirm_invite_link()
659 .returning(|_model| Ok(()))
660 .once();
661 }),
662 );
663
664 let (secret, invite, org_public_key) = build_invite(&client, org_id);
665 assert!(invite.supports_confirmation());
666 *recovery.lock().unwrap() = Some(String::from(&org_public_key));
668 *invite_cell.lock().unwrap() = Some(String::from(&invite));
669
670 client
671 .accept_and_optionally_confirm(
672 org_id,
673 uuid::Uuid::new_v4().to_string(),
674 secret,
675 "Default".to_string(),
676 true,
677 )
678 .await
679 .unwrap();
680 }
681
682 #[tokio::test]
683 async fn accept_without_enrollment_confirms_without_recovery_key() {
684 let org_id = OrganizationId::new_v4();
685 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
688 let invite_mock = invite_cell.clone();
689 let client = make_client(
690 org_id,
691 ApiClient::new_mocked(move |mock| {
692 mock.organization_users_api
693 .expect_get_invite()
694 .returning(move |_model| {
695 Ok(OrganizationInviteResponseModel {
696 invite: invite_mock.lock().unwrap().clone(),
697 })
698 })
699 .once();
700 mock.organization_users_api
701 .expect_confirm_invite_link()
702 .returning(|_model| Ok(()))
703 .once();
704 }),
705 );
706
707 let (secret, invite, _org_public_key) = build_invite(&client, org_id);
708 *invite_cell.lock().unwrap() = Some(String::from(&invite));
709 client
710 .accept_and_optionally_confirm(
711 org_id,
712 uuid::Uuid::new_v4().to_string(),
713 secret,
714 "Default".to_string(),
715 false,
716 )
717 .await
718 .unwrap();
719 }
720
721 #[tokio::test]
722 async fn accept_without_confirmation_posts_acceptance() {
723 let org_id = OrganizationId::new_v4();
724 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
725 let invite_mock = invite_cell.clone();
726 let client = make_client(
727 org_id,
728 ApiClient::new_mocked(move |mock| {
729 mock.organization_users_api
730 .expect_get_invite()
731 .returning(move |_model| {
732 Ok(OrganizationInviteResponseModel {
733 invite: invite_mock.lock().unwrap().clone(),
734 })
735 })
736 .once();
737 mock.organization_users_api
738 .expect_accept_invite_link()
739 .returning(|_model| Ok(()))
740 .once();
741 }),
742 );
743
744 let (secret, mut invite, _org_public_key) = build_invite(&client, org_id);
746 invite.disable_confirmation();
747 assert!(!invite.supports_confirmation());
748 *invite_cell.lock().unwrap() = Some(String::from(&invite));
749
750 client
751 .accept_and_optionally_confirm(
752 org_id,
753 uuid::Uuid::new_v4().to_string(),
754 secret,
755 "Default".to_string(),
756 false,
757 )
758 .await
759 .unwrap();
760 }
761
762 #[tokio::test]
763 async fn accept_with_mismatched_recovery_key_fails() {
764 let org_id = OrganizationId::new_v4();
765 let recovery = Arc::new(std::sync::Mutex::new(None::<String>));
766 let invite_cell = Arc::new(std::sync::Mutex::new(None::<String>));
767 let recovery_mock = recovery.clone();
768 let invite_mock = invite_cell.clone();
769 let client = make_client(
770 org_id,
771 ApiClient::new_mocked(move |mock| {
772 mock.organizations_api
773 .expect_get_public_key()
774 .returning(move |_id| {
775 Ok(OrganizationPublicKeyResponseModel {
776 object: None,
777 public_key: recovery_mock.lock().unwrap().clone(),
778 })
779 })
780 .once();
781 mock.organization_users_api
782 .expect_get_invite()
783 .returning(move |_model| {
784 Ok(OrganizationInviteResponseModel {
785 invite: invite_mock.lock().unwrap().clone(),
786 })
787 })
788 .once();
789 }),
790 );
791
792 let (secret, invite, _org_public_key) = build_invite(&client, org_id);
793 *invite_cell.lock().unwrap() = Some(String::from(&invite));
794 let unrelated_public_key = {
797 let mut ctx = client.key_store.context();
798 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
799 B64::from(
800 ctx.get_public_key(private_key)
801 .unwrap()
802 .to_der()
803 .unwrap()
804 .as_ref(),
805 )
806 };
807 *recovery.lock().unwrap() = Some(String::from(&unrelated_public_key));
808
809 let result = client
810 .accept_and_optionally_confirm(
811 org_id,
812 uuid::Uuid::new_v4().to_string(),
813 secret,
814 "Default".to_string(),
815 true,
816 )
817 .await;
818
819 assert!(matches!(result, Err(InviteLinkError::RecoveryKeyMismatch)));
820 }
821}