1use bitwarden_api_identity::models::RegisterFinishRequestModel;
4use bitwarden_core::{
5 OrganizationId, UserId,
6 key_management::{
7 MasterPasswordUnlockData, account_cryptographic_state::WrappedAccountCryptographicState,
8 },
9};
10use bitwarden_encoding::B64;
11use tracing::error;
12
13use crate::registration::{RegistrationClient, RegistrationError};
14
15#[bitwarden_ffi::wasm_record]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
18#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
19pub struct RegistrationFinishOpenOrgInviteData {
20 pub organization_id: OrganizationId,
22 pub code: String,
29}
30
31#[bitwarden_ffi::wasm_record]
40#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
41#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
42pub struct UserMasterPasswordRegistrationRequest {
43 pub email: String,
45 pub salt: String,
47 pub master_password: String,
49 pub master_password_hint: Option<String>,
51 pub email_verification_token: Option<String>,
53 pub sales_assisted_token: Option<String>,
55 pub organization_user_id: Option<OrganizationId>,
57 pub org_invite_token: Option<String>,
59 pub org_sponsored_free_family_plan_token: Option<String>,
61 pub accept_emergency_access_invite_token: Option<String>,
63 pub accept_emergency_access_id: Option<UserId>,
65 pub provider_invite_token: Option<String>,
67 pub provider_user_id: Option<UserId>,
69 pub open_org_invite: Option<RegistrationFinishOpenOrgInviteData>,
72}
73
74#[bitwarden_ffi::wasm_record]
76#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
77#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
78pub struct UserMasterPasswordRegistrationResponse {
79 pub account_cryptographic_state: WrappedAccountCryptographicState,
81 pub master_password_unlock: MasterPasswordUnlockData,
83 pub user_key: B64,
85}
86
87#[bitwarden_ffi::wasm_export]
88impl RegistrationClient {
89 pub async fn post_keys_for_user_password_registration(
92 &self,
93 request: UserMasterPasswordRegistrationRequest,
94 ) -> Result<UserMasterPasswordRegistrationResponse, RegistrationError> {
95 let client = &self.client.internal;
96 let identity_client = &client.get_api_configurations().identity_client;
97 internal_post_keys_for_user_password_registration(self, identity_client, request).await
98 }
99}
100
101async fn internal_post_keys_for_user_password_registration(
102 registration_client: &RegistrationClient,
103 identity_client: &bitwarden_api_identity::apis::ApiClient,
104 request: UserMasterPasswordRegistrationRequest,
105) -> Result<UserMasterPasswordRegistrationResponse, RegistrationError> {
106 let make_crypto_response = registration_client
107 .client
108 .crypto()
109 .make_user_password_registration(request.master_password, request.salt)
110 .map_err(|_| RegistrationError::Crypto)?;
111 let account_keys = Some(Box::new(
112 internal_account_keys_from_api_model(&make_crypto_response.account_keys_request)
113 .map_err(|_| RegistrationError::Crypto)?,
114 ));
115
116 let open_org_invite = request
117 .open_org_invite
118 .map(|d| {
119 let code = uuid::Uuid::parse_str(&d.code).map_err(|_| {
120 RegistrationError::InvalidInput("open_org_invite.code must be a UUID".into())
121 })?;
122 Ok::<_, RegistrationError>(Box::new(
123 bitwarden_api_identity::models::OpenOrgInviteRequestModel {
124 organization_id: d.organization_id.into(),
125 code,
126 },
127 ))
128 })
129 .transpose()?;
130
131 let api_request = RegisterFinishRequestModel {
132 email: Some(request.email),
133 master_password_hint: request.master_password_hint,
134 master_password_unlock: Some(Box::new(
135 (&make_crypto_response.master_password_unlock_data).into(),
136 )),
137 master_password_authentication: Some(Box::new(
138 (&make_crypto_response.master_password_authentication_data).into(),
139 )),
140 account_keys,
141 email_verification_token: request.email_verification_token,
142 sales_assisted_token: request.sales_assisted_token,
143 organization_user_id: request.organization_user_id.map(Into::into),
144 org_invite_token: (request.org_invite_token),
145 org_sponsored_free_family_plan_token: (request.org_sponsored_free_family_plan_token),
146 accept_emergency_access_invite_token: (request.accept_emergency_access_invite_token),
147 accept_emergency_access_id: request.accept_emergency_access_id.map(Into::into),
148 provider_invite_token: (request.provider_invite_token),
149 provider_user_id: request.provider_user_id.map(Into::into),
150 open_org_invite,
151 kdf: None,
153 kdf_memory: None,
154 kdf_parallelism: None,
155 kdf_iterations: None,
156 master_password_hash: None,
157 user_symmetric_key: None,
158 user_asymmetric_keys: None,
159 };
160
161 identity_client
162 .accounts_api()
163 .post_register_finish(Some(api_request))
164 .await
165 .map_err(|e| {
166 error!("Failed to post account keys: {e:?}");
167 RegistrationError::Api
168 })?;
169
170 Ok(UserMasterPasswordRegistrationResponse {
171 account_cryptographic_state: make_crypto_response.account_cryptographic_state,
172 master_password_unlock: make_crypto_response.master_password_unlock_data,
173 user_key: make_crypto_response.user_key.to_encoded().to_vec().into(),
174 })
175}
176
177fn internal_account_keys_from_api_model(
178 input_model: &bitwarden_api_api::models::AccountKeysRequestModel,
179) -> Result<bitwarden_api_identity::models::AccountKeysRequestModel, RegistrationError> {
180 let public_key_encryption_key_pair =
181 input_model
182 .public_key_encryption_key_pair
183 .as_deref()
184 .map(|pair| {
185 Box::new(
186 bitwarden_api_identity::models::PublicKeyEncryptionKeyPairRequestModel {
187 wrapped_private_key: pair.wrapped_private_key.clone(),
188 public_key: pair.public_key.clone(),
189 signed_public_key: pair.signed_public_key.clone(),
190 },
191 )
192 });
193
194 let signature_key_pair = input_model.signature_key_pair.as_deref().map(|pair| {
195 Box::new(
196 bitwarden_api_identity::models::SignatureKeyPairRequestModel {
197 signature_algorithm: pair.signature_algorithm.clone(),
198 wrapped_signing_key: pair.wrapped_signing_key.clone(),
199 verifying_key: pair.verifying_key.clone(),
200 },
201 )
202 });
203
204 let security_state = input_model.security_state.as_deref().map(|state| {
205 Box::new(bitwarden_api_identity::models::SecurityStateModel {
206 security_state: state.security_state.clone(),
207 security_version: state.security_version,
208 })
209 });
210
211 let user_key_encrypted_account_private_key =
212 input_model.user_key_encrypted_account_private_key.clone();
213
214 let account_public_key = input_model.account_public_key.clone();
215
216 Ok(bitwarden_api_identity::models::AccountKeysRequestModel {
217 public_key_encryption_key_pair,
218 signature_key_pair,
219 security_state,
220 user_key_encrypted_account_private_key,
221 account_public_key,
222 })
223}
224
225#[cfg(test)]
226mod tests {
227 use bitwarden_api_identity::{
228 apis::ApiClient as IdentityApiClient, models::RegisterFinishResponseModel,
229 };
230 use bitwarden_core::Client;
231
232 use super::*;
233
234 #[tokio::test]
235 async fn test_post_user_password_registration_success() {
236 let client = Client::new(None);
237 let registration_client = RegistrationClient::new(client);
238
239 let test_email = "[email protected]";
240 let test_hint = "test hint";
241 let test_password = "test-password-123";
242
243 let identity_client = IdentityApiClient::new_mocked(|mock| {
244 mock.accounts_api
245 .expect_post_register_finish()
246 .once()
247 .withf(|body| {
248 if let Some(req) = body {
249 assert_eq!(req.email, Some(test_email.to_string()));
251 assert_eq!(req.master_password_hint, Some(test_hint.to_string()));
252
253 assert!(req.account_keys.is_some());
255 let account_keys = req.account_keys.as_ref().unwrap();
256 assert!(
257 account_keys
258 .user_key_encrypted_account_private_key
259 .is_some()
260 );
261 assert!(account_keys.account_public_key.is_some());
262 assert!(account_keys.public_key_encryption_key_pair.is_some());
263 let public_key_encryption_key_pair = account_keys
264 .public_key_encryption_key_pair
265 .as_ref()
266 .unwrap();
267 assert!(public_key_encryption_key_pair.public_key.is_some());
268 assert!(public_key_encryption_key_pair.signed_public_key.is_some());
269 assert!(public_key_encryption_key_pair.wrapped_private_key.is_some());
270 assert!(account_keys.signature_key_pair.is_some());
271 let signature_key_pair = account_keys.signature_key_pair.as_ref().unwrap();
272 assert_eq!(
273 signature_key_pair.signature_algorithm,
274 Some("mldsa44".to_string())
275 );
276 assert!(signature_key_pair.verifying_key.is_some());
277 assert!(signature_key_pair.wrapped_signing_key.is_some());
278 assert!(account_keys.security_state.is_some());
279 let security_state = account_keys.security_state.as_ref().unwrap();
280 assert!(security_state.security_state.is_some());
281 assert_eq!(security_state.security_version, 2);
282 assert!(req.master_password_unlock.is_some());
283 let master_password_unlock = req.master_password_unlock.as_ref().unwrap();
284 assert_eq!(master_password_unlock.salt, test_email.to_string());
285 assert_eq!(
286 master_password_unlock.kdf,
287 Box::new(bitwarden_api_identity::models::KdfRequestModel {
288 kdf_type: bitwarden_api_identity::models::KdfType::Argon2id,
289 iterations: 6,
290 memory: Some(32),
291 parallelism: Some(4),
292 })
293 );
294 assert!(req.master_password_authentication.is_some());
295 let master_password_authentication =
296 req.master_password_authentication.as_ref().unwrap();
297 assert_eq!(master_password_authentication.salt, test_email.to_string());
298 assert_eq!(
299 master_password_authentication.kdf,
300 Box::new(bitwarden_api_identity::models::KdfRequestModel {
301 kdf_type: bitwarden_api_identity::models::KdfType::Argon2id,
302 iterations: 6,
303 memory: Some(32),
304 parallelism: Some(4),
305 })
306 );
307
308 assert!(req.user_asymmetric_keys.is_none());
310 assert!(req.kdf.is_none());
311 assert!(req.kdf_iterations.is_none());
312 assert!(req.kdf_memory.is_none());
313 assert!(req.kdf_parallelism.is_none());
314
315 assert!(req.email_verification_token.is_none());
317 assert!(req.sales_assisted_token.is_none());
318 assert!(req.organization_user_id.is_none());
319 assert!(req.org_invite_token.is_none());
320 assert!(req.org_sponsored_free_family_plan_token.is_none());
321 assert!(req.accept_emergency_access_invite_token.is_none());
322 assert!(req.accept_emergency_access_id.is_none());
323 assert!(req.provider_invite_token.is_none());
324 assert!(req.provider_user_id.is_none());
325 assert!(req.open_org_invite.is_none());
326 true
327 } else {
328 false
329 }
330 })
331 .returning(move |_body| Ok(RegisterFinishResponseModel { object: None }));
332 });
333
334 let request = UserMasterPasswordRegistrationRequest {
335 email: test_email.to_string(),
336 salt: test_email.to_string(),
337 master_password: test_password.to_string(),
338 master_password_hint: Some(test_hint.to_string()),
339 email_verification_token: None,
340 sales_assisted_token: None,
341 organization_user_id: None,
342 org_invite_token: None,
343 org_sponsored_free_family_plan_token: None,
344 accept_emergency_access_invite_token: None,
345 accept_emergency_access_id: None,
346 provider_invite_token: None,
347 provider_user_id: None,
348 open_org_invite: None,
349 };
350
351 let result = internal_post_keys_for_user_password_registration(
352 ®istration_client,
353 &identity_client,
354 request,
355 )
356 .await;
357
358 assert!(result.is_ok());
359
360 if let IdentityApiClient::Mock(mut mock) = identity_client {
362 mock.accounts_api.checkpoint();
363 }
364 }
365
366 #[tokio::test]
367 async fn test_post_user_password_registration_failure() {
368 let client = Client::new(None);
369 let registration_client = RegistrationClient::new(client);
370
371 let test_email = "[email protected]";
372 let test_hint = "test hint";
373 let test_password = "test-password-123";
374
375 let identity_client = IdentityApiClient::new_mocked(|mock| {
376 mock.accounts_api
377 .expect_post_register_finish()
378 .once()
379 .returning(move |_body| {
380 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
381 });
382 });
383
384 let request = UserMasterPasswordRegistrationRequest {
385 email: test_email.to_string(),
386 salt: test_email.to_string(),
387 master_password: test_password.to_string(),
388 master_password_hint: Some(test_hint.to_string()),
389 email_verification_token: None,
390 sales_assisted_token: None,
391 organization_user_id: None,
392 org_invite_token: None,
393 org_sponsored_free_family_plan_token: None,
394 accept_emergency_access_invite_token: None,
395 accept_emergency_access_id: None,
396 provider_invite_token: None,
397 provider_user_id: None,
398 open_org_invite: None,
399 };
400
401 let result = internal_post_keys_for_user_password_registration(
402 ®istration_client,
403 &identity_client,
404 request,
405 )
406 .await;
407
408 assert!(result.is_err());
409 assert!(matches!(result.unwrap_err(), RegistrationError::Api));
410
411 if let IdentityApiClient::Mock(mut mock) = identity_client {
413 mock.accounts_api.checkpoint();
414 }
415 }
416
417 #[tokio::test]
418 async fn test_post_user_password_registration_with_open_org_invite_success() {
419 let client = Client::new(None);
420 let registration_client = RegistrationClient::new(client);
421
422 let test_email = "[email protected]";
423 let test_hint = "test hint";
424 let test_password = "test-password-123";
425 let test_org_id = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
426 let test_code = "9e0a4c2d-4c9f-4d3b-9a8b-2f7f2b6c4e1a";
427
428 let identity_client = IdentityApiClient::new_mocked(|mock| {
429 mock.accounts_api
430 .expect_post_register_finish()
431 .once()
432 .withf(move |body| {
433 let req = body.as_ref().expect("body must be present");
434 let invite = req
435 .open_org_invite
436 .as_ref()
437 .expect("open_org_invite must be set");
438 assert_eq!(
439 invite.organization_id,
440 uuid::Uuid::parse_str(test_org_id).unwrap()
441 );
442 assert_eq!(invite.code, uuid::Uuid::parse_str(test_code).unwrap());
443 true
444 })
445 .returning(move |_body| Ok(RegisterFinishResponseModel { object: None }));
446 });
447
448 let request = UserMasterPasswordRegistrationRequest {
449 email: test_email.to_string(),
450 salt: test_email.to_string(),
451 master_password: test_password.to_string(),
452 master_password_hint: Some(test_hint.to_string()),
453 email_verification_token: None,
454 sales_assisted_token: None,
455 organization_user_id: None,
456 org_invite_token: None,
457 org_sponsored_free_family_plan_token: None,
458 accept_emergency_access_invite_token: None,
459 accept_emergency_access_id: None,
460 provider_invite_token: None,
461 provider_user_id: None,
462 open_org_invite: Some(RegistrationFinishOpenOrgInviteData {
463 organization_id: test_org_id.parse().unwrap(),
464 code: test_code.to_string(),
465 }),
466 };
467
468 let result = internal_post_keys_for_user_password_registration(
469 ®istration_client,
470 &identity_client,
471 request,
472 )
473 .await;
474
475 assert!(result.is_ok());
476
477 if let IdentityApiClient::Mock(mut mock) = identity_client {
478 mock.accounts_api.checkpoint();
479 }
480 }
481}