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