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::{
8 InviteKeyBundle, InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope,
9};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12#[cfg(feature = "wasm")]
13use tsify::Tsify;
14#[cfg(feature = "wasm")]
15use wasm_bindgen::prelude::wasm_bindgen;
16
17#[bitwarden_error(flat)]
19#[derive(Debug, Error)]
20pub enum OrganizationInviteCryptoBundleError {
21 #[error("Key bundle generation failed: {0}")]
23 BundleGenerationFailed(#[from] InviteKeyBundleError),
24 #[error("Failed to unseal invite key: {0}")]
26 UnsealingFailed(InviteKeyBundleError),
27}
28
29#[derive(Clone, Serialize, Deserialize)]
35#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
36#[serde(rename_all = "camelCase")]
37pub struct OrganizationInviteCryptoBundle {
38 #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyData"))]
40 pub invite_key: InviteKeyData,
41 #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyEnvelope"))]
43 pub sealed_invite_key_envelope: InviteKeyEnvelope,
44}
45
46#[cfg_attr(feature = "wasm", wasm_bindgen)]
48#[derive(FromClient)]
49pub struct InviteLinkClient {
50 pub(crate) key_store: KeyStore<KeySlotIds>,
51}
52
53#[cfg_attr(feature = "wasm", wasm_bindgen)]
54impl InviteLinkClient {
55 pub fn generate_invite_crypto_bundle(
65 &self,
66 organization_id: OrganizationId,
67 ) -> Result<OrganizationInviteCryptoBundle, OrganizationInviteCryptoBundleError> {
68 let mut ctx = self.key_store.context();
69 let org_key = SymmetricKeySlotId::Organization(organization_id);
70 let bundle = InviteKeyBundle::make(org_key, &mut ctx)?;
71 Ok(OrganizationInviteCryptoBundle {
72 invite_key: bundle.dangerous_get_raw_invite_key().clone(),
73 sealed_invite_key_envelope: bundle.get_sealed_invite_key_envelope().clone(),
74 })
75 }
76
77 pub fn unseal_invite_key(
80 &self,
81 organization_id: OrganizationId,
82 sealed_invite_key_envelope: InviteKeyEnvelope,
83 ) -> Result<InviteKeyData, OrganizationInviteCryptoBundleError> {
84 let mut ctx = self.key_store.context();
85 let org_key = SymmetricKeySlotId::Organization(organization_id);
86 sealed_invite_key_envelope
87 .unseal(org_key, &mut ctx)
88 .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)
89 }
90}
91
92pub trait InviteLinkClientExt {
94 fn invite_link(&self) -> InviteLinkClient;
96}
97
98impl InviteLinkClientExt for Client {
99 fn invite_link(&self) -> InviteLinkClient {
100 InviteLinkClient::from_client(self)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use bitwarden_core::key_management::create_test_crypto_with_user_and_org_key;
107 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
108
109 use super::*;
110
111 fn make_client(org_id: OrganizationId) -> InviteLinkClient {
112 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
113 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
114 let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
115 InviteLinkClient { key_store }
116 }
117
118 #[test]
119 fn generate_invite_crypto_bundle_returns_non_empty_fields() {
120 let org_id = OrganizationId::new_v4();
121 let client = make_client(org_id);
122
123 let bundle = client.generate_invite_crypto_bundle(org_id).unwrap();
124
125 assert!(!String::from(&bundle.invite_key).is_empty());
126 assert!(!String::from(&bundle.sealed_invite_key_envelope).is_empty());
127 }
128
129 #[test]
130 fn envelope_unseals_to_raw_invite_key() {
131 let org_id = OrganizationId::new_v4();
132 let client = make_client(org_id);
133
134 let bundle = client.generate_invite_crypto_bundle(org_id).unwrap();
135 let unsealed = client
136 .unseal_invite_key(org_id, bundle.sealed_invite_key_envelope.clone())
137 .unwrap();
138
139 assert_eq!(bundle.invite_key, unsealed);
140 }
141
142 #[test]
143 fn two_calls_produce_different_invite_keys() {
144 let org_id = OrganizationId::new_v4();
145 let client = make_client(org_id);
146
147 let bundle1 = client.generate_invite_crypto_bundle(org_id).unwrap();
148 let bundle2 = client.generate_invite_crypto_bundle(org_id).unwrap();
149
150 assert_ne!(
151 String::from(&bundle1.invite_key),
152 String::from(&bundle2.invite_key)
153 );
154 }
155
156 #[test]
157 fn sealed_envelope_serializes_as_encstring_text_format() {
158 let org_id = OrganizationId::new_v4();
161 let client = make_client(org_id);
162
163 let bundle = client.generate_invite_crypto_bundle(org_id).unwrap();
164 let envelope_str = String::from(&bundle.sealed_invite_key_envelope);
165
166 assert!(
167 envelope_str.parse::<bitwarden_crypto::EncString>().is_ok(),
168 "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope_str}"
169 );
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.generate_invite_crypto_bundle(org_id).unwrap();
179 let result = client.unseal_invite_key(other_org_id, bundle.sealed_invite_key_envelope);
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.generate_invite_crypto_bundle(other_org_id);
194
195 assert!(matches!(
196 result,
197 Err(OrganizationInviteCryptoBundleError::BundleGenerationFailed(
198 _
199 ))
200 ));
201 }
202}