bitwarden_organization_invite_link/
invite_link_client.rs1use bitwarden_core::{
2 Client, FromClient, OrganizationId,
3 key_management::{KeySlotIds, SymmetricKeySlotId},
4};
5use bitwarden_crypto::KeyStore;
6use bitwarden_error::bitwarden_error;
7use bitwarden_organization_crypto::{Invite, InviteBundle, InviteKeyBundleError, InviteKeyData};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10#[cfg(feature = "wasm")]
11use tsify::Tsify;
12#[cfg(feature = "wasm")]
13use wasm_bindgen::prelude::wasm_bindgen;
14
15#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum OrganizationInviteCryptoBundleError {
19 #[error("Key bundle generation failed: {0}")]
21 BundleGenerationFailed(#[from] InviteKeyBundleError),
22 #[error("Failed to unseal invite key: {0}")]
24 UnsealingFailed(InviteKeyBundleError),
25}
26
27#[derive(Clone, Serialize, Deserialize)]
33#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
34#[serde(rename_all = "camelCase")]
35pub struct OrganizationInviteCryptoBundle {
36 #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyData"))]
38 pub invite_key: InviteKeyData,
39 #[cfg_attr(feature = "wasm", tsify(type = "Invite"))]
41 pub invite: Invite,
42}
43
44#[cfg_attr(feature = "wasm", wasm_bindgen)]
46#[derive(FromClient)]
47pub struct InviteLinkClient {
48 pub(crate) key_store: KeyStore<KeySlotIds>,
49}
50
51#[cfg_attr(feature = "wasm", wasm_bindgen)]
52impl InviteLinkClient {
53 pub fn make_invite(
63 &self,
64 organization_id: OrganizationId,
65 ) -> Result<OrganizationInviteCryptoBundle, OrganizationInviteCryptoBundleError> {
66 let mut ctx = self.key_store.context();
67 let org_key = SymmetricKeySlotId::Organization(organization_id);
68 let bundle = InviteBundle::make(org_key, &mut ctx)?;
69 Ok(OrganizationInviteCryptoBundle {
70 invite_key: bundle.dangerous_get_raw_invite_key().clone(),
71 invite: bundle.get_envelope().clone(),
72 })
73 }
74
75 pub fn get_invite_key(
78 &self,
79 organization_id: OrganizationId,
80 invite: Invite,
81 ) -> Result<InviteKeyData, OrganizationInviteCryptoBundleError> {
82 let mut ctx = self.key_store.context();
83 let org_key = SymmetricKeySlotId::Organization(organization_id);
84 invite
85 .unseal(org_key, &mut ctx)
86 .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)
87 }
88}
89
90pub trait InviteLinkClientExt {
92 fn invite_link(&self) -> InviteLinkClient;
94}
95
96impl InviteLinkClientExt for Client {
97 fn invite_link(&self) -> InviteLinkClient {
98 InviteLinkClient::from_client(self)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use bitwarden_core::key_management::create_test_crypto_with_user_and_org_key;
105 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
106
107 use super::*;
108
109 fn make_client(org_id: OrganizationId) -> InviteLinkClient {
110 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
111 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
112 let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
113 InviteLinkClient { key_store }
114 }
115
116 #[test]
117 fn generate_invite_crypto_bundle_returns_non_empty_fields() {
118 let org_id = OrganizationId::new_v4();
119 let client = make_client(org_id);
120
121 let bundle = client.make_invite(org_id).unwrap();
122
123 assert!(!String::from(&bundle.invite_key).is_empty());
124 assert!(!String::from(&bundle.invite).is_empty());
125 }
126
127 #[test]
128 fn envelope_unseals_to_raw_invite_key() {
129 let org_id = OrganizationId::new_v4();
130 let client = make_client(org_id);
131
132 let bundle = client.make_invite(org_id).unwrap();
133 let unsealed = client
134 .get_invite_key(org_id, bundle.invite.clone())
135 .unwrap();
136
137 assert_eq!(bundle.invite_key, unsealed);
138 }
139
140 #[test]
141 fn two_calls_produce_different_invite_keys() {
142 let org_id = OrganizationId::new_v4();
143 let client = make_client(org_id);
144
145 let bundle1 = client.make_invite(org_id).unwrap();
146 let bundle2 = client.make_invite(org_id).unwrap();
147
148 assert_ne!(
149 String::from(&bundle1.invite_key),
150 String::from(&bundle2.invite_key)
151 );
152 }
153
154 #[test]
155 fn sealed_invite_serializes_as_stable_base64_wire_format() {
156 let org_id = OrganizationId::new_v4();
160 let client = make_client(org_id);
161
162 let bundle = client.make_invite(org_id).unwrap();
163 let invite_str = String::from(&bundle.invite);
164
165 let reparsed: Invite = invite_str
166 .parse()
167 .expect("serialized invite must parse back from its base64 wire format");
168 let unsealed = client.get_invite_key(org_id, reparsed).unwrap();
169 assert_eq!(bundle.invite_key, unsealed);
170 }
171
172 #[test]
173 fn unseal_with_wrong_organization_id_fails() {
174 let org_id = OrganizationId::new_v4();
175 let other_org_id = OrganizationId::new_v4();
176 let client = make_client(org_id);
177
178 let bundle = client.make_invite(org_id).unwrap();
179 let result = client.get_invite_key(other_org_id, bundle.invite);
180
181 assert!(matches!(
182 result,
183 Err(OrganizationInviteCryptoBundleError::UnsealingFailed(_))
184 ));
185 }
186
187 #[test]
188 fn generate_with_unknown_organization_id_fails() {
189 let org_id = OrganizationId::new_v4();
190 let other_org_id = OrganizationId::new_v4();
191 let client = make_client(org_id);
192
193 let result = client.make_invite(other_org_id);
194
195 assert!(matches!(
196 result,
197 Err(OrganizationInviteCryptoBundleError::BundleGenerationFailed(
198 _
199 ))
200 ));
201 }
202}