Skip to main content

bitwarden_auth/registration/
post_keys_for_user_password_registration.rs

1//! Initializes new password-based cryptographic state for a user
2//! and posts the state to the server
3use 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/// Open-organization-invite data to include on the register-finish payload.
18#[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    /// The organization the registrant is joining via the open invite link.
27    pub organization_id: OrganizationId,
28    // TODO: retrofit to a tagged newtype once KM or AC introduce one for
29    // invite-link codes. Bitwarden convention prefers tagged/branded ID types
30    // over raw UUIDs on FFI-exposed structs (see `OrganizationId`), but no
31    // shared type exists for invite-link codes today, so we accept the code as
32    // a `String` and parse to `Uuid` at the SDK boundary.
33    /// The bearer code from the shared invite URL. Must be a UUID.
34    pub code: String,
35}
36
37// TODO PM-41828: consider annotating every `Option<T>` field below with
38// `#[cfg_attr(feature = "uniffi", uniffi(default = None))]` and
39// `#[cfg_attr(feature = "wasm", tsify(optional))]`
40// Doing so makes each field optional in the generated
41// Kotlin/Swift/TypeScript bindings, so future additive `Option<T>` fields land as non-breaking
42// changes on mobile and clients (rather than forcing a coordinated PR across every consumer
43// repo whenever a field is added).
44/// Request parameters for master password registration
45#[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    /// Email for the account being initialized
54    pub email: String,
55    /// Salt for master password hashing
56    pub salt: String,
57    /// Master password for the account
58    pub master_password: String,
59    /// Optional hint for the master password
60    pub master_password_hint: Option<String>,
61    /// Optional token for email verification
62    pub email_verification_token: Option<String>,
63    /// Optional token for sales-assisted trial/registration
64    pub sales_assisted_token: Option<String>,
65    /// Optional organization user ID for organization invitations
66    pub organization_user_id: Option<OrganizationId>,
67    /// Optional direct organization invite token for joining an organization
68    pub org_invite_token: Option<String>,
69    /// Optional token for sponsored free family plan
70    pub org_sponsored_free_family_plan_token: Option<String>,
71    /// Optional token for accepting emergency access invitation
72    pub accept_emergency_access_invite_token: Option<String>,
73    /// Optional emergency access ID for accepting emergency access invitation
74    pub accept_emergency_access_id: Option<UserId>,
75    /// Optional provider invite token for joining as a provider
76    pub provider_invite_token: Option<String>,
77    /// Optional provider user ID for provider invitations
78    pub provider_user_id: Option<UserId>,
79    /// Optional open-organization-invite identifiers when finishing registration with an open
80    /// organization invite link in client state.
81    pub open_org_invite: Option<RegistrationFinishOpenOrgInviteData>,
82}
83
84/// Result of user master password registration process.
85#[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    /// The account cryptographic state of the user
94    pub account_cryptographic_state: WrappedAccountCryptographicState,
95    /// The master password unlock data
96    pub master_password_unlock: MasterPasswordUnlockData,
97    /// The decrypted user key. This can be used to get the consuming client to an unlocked state.
98    pub user_key: B64,
99}
100
101#[cfg_attr(feature = "wasm", wasm_bindgen)]
102impl RegistrationClient {
103    /// Initializes new password-based cryptographic state for a user
104    /// and posts the state to the server
105    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        // TODO remove deprecated fields below with https://bitwarden.atlassian.net/browse/PM-27326
166        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                        // standard user entity information
264                        assert_eq!(req.email, Some(test_email.to_string()));
265                        assert_eq!(req.master_password_hint, Some(test_hint.to_string()));
266
267                        // verifying new cryptographic data structures
268                        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                        // verify old cryptographic structures aren't set
323                        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                        // verify master password registration specific information
330                        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            &registration_client,
367            &identity_client,
368            request,
369        )
370        .await;
371
372        assert!(result.is_ok());
373
374        // check that mock expectations were met
375        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            &registration_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        // check that mock expectations were met
426        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            &registration_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}