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
13use crate::registration::{RegistrationClient, RegistrationError};
14
15/// Open-organization-invite data to include on the register-finish payload.
16#[bitwarden_ffi::wasm_record]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
18#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
19pub struct RegistrationFinishOpenOrgInviteData {
20    /// The organization the registrant is joining via the open invite link.
21    pub organization_id: OrganizationId,
22    // TODO: retrofit to a tagged newtype once KM or AC introduce one for
23    // invite-link codes. Bitwarden convention prefers tagged/branded ID types
24    // over raw UUIDs on FFI-exposed structs (see `OrganizationId`), but no
25    // shared type exists for invite-link codes today, so we accept the code as
26    // a `String` and parse to `Uuid` at the SDK boundary.
27    /// The bearer code from the shared invite URL. Must be a UUID.
28    pub code: String,
29}
30
31// TODO PM-41828: consider annotating every `Option<T>` field below with
32// `#[cfg_attr(feature = "uniffi", uniffi(default = None))]` and
33// `#[cfg_attr(feature = "wasm", tsify(optional))]`
34// Doing so makes each field optional in the generated
35// Kotlin/Swift/TypeScript bindings, so future additive `Option<T>` fields land as non-breaking
36// changes on mobile and clients (rather than forcing a coordinated PR across every consumer
37// repo whenever a field is added).
38/// Request parameters for master password registration
39#[bitwarden_ffi::wasm_record]
40#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
41#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
42pub struct UserMasterPasswordRegistrationRequest {
43    /// Email for the account being initialized
44    pub email: String,
45    /// Salt for master password hashing
46    pub salt: String,
47    /// Master password for the account
48    pub master_password: String,
49    /// Optional hint for the master password
50    pub master_password_hint: Option<String>,
51    /// Optional token for email verification
52    pub email_verification_token: Option<String>,
53    /// Optional token for sales-assisted trial/registration
54    pub sales_assisted_token: Option<String>,
55    /// Optional organization user ID for organization invitations
56    pub organization_user_id: Option<OrganizationId>,
57    /// Optional direct organization invite token for joining an organization
58    pub org_invite_token: Option<String>,
59    /// Optional token for sponsored free family plan
60    pub org_sponsored_free_family_plan_token: Option<String>,
61    /// Optional token for accepting emergency access invitation
62    pub accept_emergency_access_invite_token: Option<String>,
63    /// Optional emergency access ID for accepting emergency access invitation
64    pub accept_emergency_access_id: Option<UserId>,
65    /// Optional provider invite token for joining as a provider
66    pub provider_invite_token: Option<String>,
67    /// Optional provider user ID for provider invitations
68    pub provider_user_id: Option<UserId>,
69    /// Optional open-organization-invite identifiers when finishing registration with an open
70    /// organization invite link in client state.
71    pub open_org_invite: Option<RegistrationFinishOpenOrgInviteData>,
72}
73
74/// Result of user master password registration process.
75#[bitwarden_ffi::wasm_record]
76#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
77#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
78pub struct UserMasterPasswordRegistrationResponse {
79    /// The account cryptographic state of the user
80    pub account_cryptographic_state: WrappedAccountCryptographicState,
81    /// The master password unlock data
82    pub master_password_unlock: MasterPasswordUnlockData,
83    /// The decrypted user key. This can be used to get the consuming client to an unlocked state.
84    pub user_key: B64,
85}
86
87#[bitwarden_ffi::wasm_export]
88impl RegistrationClient {
89    /// Initializes new password-based cryptographic state for a user
90    /// and posts the state to the server
91    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        // TODO remove deprecated fields below with https://bitwarden.atlassian.net/browse/PM-27326
152        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                        // standard user entity information
250                        assert_eq!(req.email, Some(test_email.to_string()));
251                        assert_eq!(req.master_password_hint, Some(test_hint.to_string()));
252
253                        // verifying new cryptographic data structures
254                        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                        // verify old cryptographic structures aren't set
309                        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                        // verify master password registration specific information
316                        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            &registration_client,
353            &identity_client,
354            request,
355        )
356        .await;
357
358        assert!(result.is_ok());
359
360        // check that mock expectations were met
361        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            &registration_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        // check that mock expectations were met
412        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            &registration_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}