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/// Request parameters for master password registration
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 UserMasterPasswordRegistrationRequest {
26    /// Email for the account being initialized
27    pub email: String,
28    /// Salt for master password hashing
29    pub salt: String,
30    /// Master password for the account
31    pub master_password: String,
32    /// Optional hint for the master password
33    pub master_password_hint: Option<String>,
34    /// Optional token for email verification
35    pub email_verification_token: Option<String>,
36    /// Optional token for sales-assisted trial/registration
37    pub sales_assisted_token: Option<String>,
38    /// Optional organization user ID for organization invitations
39    pub organization_user_id: Option<OrganizationId>,
40    /// Optional organization invite token for joining an organization
41    pub org_invite_token: Option<String>,
42    /// Optional token for sponsored free family plan
43    pub org_sponsored_free_family_plan_token: Option<String>,
44    /// Optional token for accepting emergency access invitation
45    pub accept_emergency_access_invite_token: Option<String>,
46    /// Optional emergency access ID for accepting emergency access invitation
47    pub accept_emergency_access_id: Option<UserId>,
48    /// Optional provider invite token for joining as a provider
49    pub provider_invite_token: Option<String>,
50    /// Optional provider user ID for provider invitations
51    pub provider_user_id: Option<UserId>,
52}
53
54/// Result of user master password registration process.
55#[cfg_attr(
56    feature = "wasm",
57    derive(tsify::Tsify),
58    tsify(into_wasm_abi, from_wasm_abi)
59)]
60#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
61#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
62pub struct UserMasterPasswordRegistrationResponse {
63    /// The account cryptographic state of the user
64    pub account_cryptographic_state: WrappedAccountCryptographicState,
65    /// The master password unlock data
66    pub master_password_unlock: MasterPasswordUnlockData,
67    /// The decrypted user key. This can be used to get the consuming client to an unlocked state.
68    pub user_key: B64,
69}
70
71#[cfg_attr(feature = "wasm", wasm_bindgen)]
72impl RegistrationClient {
73    /// Initializes new password-based cryptographic state for a user
74    /// and posts the state to the server
75    pub async fn post_keys_for_user_password_registration(
76        &self,
77        request: UserMasterPasswordRegistrationRequest,
78    ) -> Result<UserMasterPasswordRegistrationResponse, RegistrationError> {
79        let client = &self.client.internal;
80        let identity_client = &client.get_api_configurations().identity_client;
81        internal_post_keys_for_user_password_registration(self, identity_client, request).await
82    }
83}
84
85async fn internal_post_keys_for_user_password_registration(
86    registration_client: &RegistrationClient,
87    identity_client: &bitwarden_api_identity::apis::ApiClient,
88    request: UserMasterPasswordRegistrationRequest,
89) -> Result<UserMasterPasswordRegistrationResponse, RegistrationError> {
90    let make_crypto_response = registration_client
91        .client
92        .crypto()
93        .make_user_password_registration(request.master_password, request.salt)
94        .map_err(|_| RegistrationError::Crypto)?;
95    let account_keys = Some(Box::new(
96        internal_account_keys_from_api_model(&make_crypto_response.account_keys_request)
97            .map_err(|_| RegistrationError::Crypto)?,
98    ));
99
100    let api_request = RegisterFinishRequestModel {
101        email: Some(request.email),
102        master_password_hint: request.master_password_hint,
103        master_password_unlock: Some(Box::new(
104            (&make_crypto_response.master_password_unlock_data).into(),
105        )),
106        master_password_authentication: Some(Box::new(
107            (&make_crypto_response.master_password_authentication_data).into(),
108        )),
109        account_keys,
110        email_verification_token: request.email_verification_token,
111        sales_assisted_token: request.sales_assisted_token,
112        organization_user_id: request.organization_user_id.map(Into::into),
113        org_invite_token: (request.org_invite_token),
114        org_sponsored_free_family_plan_token: (request.org_sponsored_free_family_plan_token),
115        accept_emergency_access_invite_token: (request.accept_emergency_access_invite_token),
116        accept_emergency_access_id: request.accept_emergency_access_id.map(Into::into),
117        provider_invite_token: (request.provider_invite_token),
118        provider_user_id: request.provider_user_id.map(Into::into),
119        // TODO remove deprecated fields below with https://bitwarden.atlassian.net/browse/PM-27326
120        kdf: None,
121        kdf_memory: None,
122        kdf_parallelism: None,
123        kdf_iterations: None,
124        master_password_hash: None,
125        user_symmetric_key: None,
126        user_asymmetric_keys: None,
127    };
128
129    identity_client
130        .accounts_api()
131        .post_register_finish(Some(api_request))
132        .await
133        .map_err(|e| {
134            error!("Failed to post account keys: {e:?}");
135            RegistrationError::Api
136        })?;
137
138    Ok(UserMasterPasswordRegistrationResponse {
139        account_cryptographic_state: make_crypto_response.account_cryptographic_state,
140        master_password_unlock: make_crypto_response.master_password_unlock_data,
141        user_key: make_crypto_response.user_key.to_encoded().to_vec().into(),
142    })
143}
144
145fn internal_account_keys_from_api_model(
146    input_model: &bitwarden_api_api::models::AccountKeysRequestModel,
147) -> Result<bitwarden_api_identity::models::AccountKeysRequestModel, RegistrationError> {
148    let public_key_encryption_key_pair =
149        input_model
150            .public_key_encryption_key_pair
151            .as_deref()
152            .map(|pair| {
153                Box::new(
154                    bitwarden_api_identity::models::PublicKeyEncryptionKeyPairRequestModel {
155                        wrapped_private_key: pair.wrapped_private_key.clone(),
156                        public_key: pair.public_key.clone(),
157                        signed_public_key: pair.signed_public_key.clone(),
158                    },
159                )
160            });
161
162    let signature_key_pair = input_model.signature_key_pair.as_deref().map(|pair| {
163        Box::new(
164            bitwarden_api_identity::models::SignatureKeyPairRequestModel {
165                signature_algorithm: pair.signature_algorithm.clone(),
166                wrapped_signing_key: pair.wrapped_signing_key.clone(),
167                verifying_key: pair.verifying_key.clone(),
168            },
169        )
170    });
171
172    let security_state = input_model.security_state.as_deref().map(|state| {
173        Box::new(bitwarden_api_identity::models::SecurityStateModel {
174            security_state: state.security_state.clone(),
175            security_version: state.security_version,
176        })
177    });
178
179    let user_key_encrypted_account_private_key =
180        input_model.user_key_encrypted_account_private_key.clone();
181
182    let account_public_key = input_model.account_public_key.clone();
183
184    Ok(bitwarden_api_identity::models::AccountKeysRequestModel {
185        public_key_encryption_key_pair,
186        signature_key_pair,
187        security_state,
188        user_key_encrypted_account_private_key,
189        account_public_key,
190    })
191}
192
193#[cfg(test)]
194mod tests {
195    use bitwarden_api_identity::{
196        apis::ApiClient as IdentityApiClient, models::RegisterFinishResponseModel,
197    };
198    use bitwarden_core::Client;
199
200    use super::*;
201
202    #[tokio::test]
203    async fn test_post_user_password_registration_success() {
204        let client = Client::new(None);
205        let registration_client = RegistrationClient::new(client);
206
207        let test_email = "[email protected]";
208        let test_hint = "test hint";
209        let test_password = "test-password-123";
210
211        let identity_client = IdentityApiClient::new_mocked(|mock| {
212            mock.accounts_api
213                .expect_post_register_finish()
214                .once()
215                .withf(|body| {
216                    if let Some(req) = body {
217                        // standard user entity information
218                        assert_eq!(req.email, Some(test_email.to_string()));
219                        assert_eq!(req.master_password_hint, Some(test_hint.to_string()));
220
221                        // verifying new cryptographic data structures
222                        assert!(req.account_keys.is_some());
223                        let account_keys = req.account_keys.as_ref().unwrap();
224                        assert!(
225                            account_keys
226                                .user_key_encrypted_account_private_key
227                                .is_some()
228                        );
229                        assert!(account_keys.account_public_key.is_some());
230                        assert!(account_keys.public_key_encryption_key_pair.is_some());
231                        let public_key_encryption_key_pair = account_keys
232                            .public_key_encryption_key_pair
233                            .as_ref()
234                            .unwrap();
235                        assert!(public_key_encryption_key_pair.public_key.is_some());
236                        assert!(public_key_encryption_key_pair.signed_public_key.is_some());
237                        assert!(public_key_encryption_key_pair.wrapped_private_key.is_some());
238                        assert!(account_keys.signature_key_pair.is_some());
239                        let signature_key_pair = account_keys.signature_key_pair.as_ref().unwrap();
240                        assert_eq!(
241                            signature_key_pair.signature_algorithm,
242                            Some("mldsa44".to_string())
243                        );
244                        assert!(signature_key_pair.verifying_key.is_some());
245                        assert!(signature_key_pair.wrapped_signing_key.is_some());
246                        assert!(account_keys.security_state.is_some());
247                        let security_state = account_keys.security_state.as_ref().unwrap();
248                        assert!(security_state.security_state.is_some());
249                        assert_eq!(security_state.security_version, 2);
250                        assert!(req.master_password_unlock.is_some());
251                        let master_password_unlock = req.master_password_unlock.as_ref().unwrap();
252                        assert_eq!(master_password_unlock.salt, test_email.to_string());
253                        assert_eq!(
254                            master_password_unlock.kdf,
255                            Box::new(bitwarden_api_identity::models::KdfRequestModel {
256                                kdf_type: bitwarden_api_identity::models::KdfType::Argon2id,
257                                iterations: 6,
258                                memory: Some(32),
259                                parallelism: Some(4),
260                            })
261                        );
262                        assert!(req.master_password_authentication.is_some());
263                        let master_password_authentication =
264                            req.master_password_authentication.as_ref().unwrap();
265                        assert_eq!(master_password_authentication.salt, test_email.to_string());
266                        assert_eq!(
267                            master_password_authentication.kdf,
268                            Box::new(bitwarden_api_identity::models::KdfRequestModel {
269                                kdf_type: bitwarden_api_identity::models::KdfType::Argon2id,
270                                iterations: 6,
271                                memory: Some(32),
272                                parallelism: Some(4),
273                            })
274                        );
275
276                        // verify old cryptographic structures aren't set
277                        assert!(req.user_asymmetric_keys.is_none());
278                        assert!(req.kdf.is_none());
279                        assert!(req.kdf_iterations.is_none());
280                        assert!(req.kdf_memory.is_none());
281                        assert!(req.kdf_parallelism.is_none());
282
283                        // verify master password registration specific information
284                        assert!(req.email_verification_token.is_none());
285                        assert!(req.sales_assisted_token.is_none());
286                        assert!(req.organization_user_id.is_none());
287                        assert!(req.org_invite_token.is_none());
288                        assert!(req.org_sponsored_free_family_plan_token.is_none());
289                        assert!(req.accept_emergency_access_invite_token.is_none());
290                        assert!(req.accept_emergency_access_id.is_none());
291                        assert!(req.provider_invite_token.is_none());
292                        assert!(req.provider_user_id.is_none());
293                        true
294                    } else {
295                        false
296                    }
297                })
298                .returning(move |_body| Ok(RegisterFinishResponseModel { object: None }));
299        });
300
301        let request = UserMasterPasswordRegistrationRequest {
302            email: test_email.to_string(),
303            salt: test_email.to_string(),
304            master_password: test_password.to_string(),
305            master_password_hint: Some(test_hint.to_string()),
306            email_verification_token: None,
307            sales_assisted_token: None,
308            organization_user_id: None,
309            org_invite_token: None,
310            org_sponsored_free_family_plan_token: None,
311            accept_emergency_access_invite_token: None,
312            accept_emergency_access_id: None,
313            provider_invite_token: None,
314            provider_user_id: None,
315        };
316
317        let result = internal_post_keys_for_user_password_registration(
318            &registration_client,
319            &identity_client,
320            request,
321        )
322        .await;
323
324        assert!(result.is_ok());
325
326        // check that mock expectations were met
327        if let IdentityApiClient::Mock(mut mock) = identity_client {
328            mock.accounts_api.checkpoint();
329        }
330    }
331
332    #[tokio::test]
333    async fn test_post_user_password_registration_failure() {
334        let client = Client::new(None);
335        let registration_client = RegistrationClient::new(client);
336
337        let test_email = "[email protected]";
338        let test_hint = "test hint";
339        let test_password = "test-password-123";
340
341        let identity_client = IdentityApiClient::new_mocked(|mock| {
342            mock.accounts_api
343                .expect_post_register_finish()
344                .once()
345                .returning(move |_body| {
346                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
347                });
348        });
349
350        let request = UserMasterPasswordRegistrationRequest {
351            email: test_email.to_string(),
352            salt: test_email.to_string(),
353            master_password: test_password.to_string(),
354            master_password_hint: Some(test_hint.to_string()),
355            email_verification_token: None,
356            sales_assisted_token: None,
357            organization_user_id: None,
358            org_invite_token: None,
359            org_sponsored_free_family_plan_token: None,
360            accept_emergency_access_invite_token: None,
361            accept_emergency_access_id: None,
362            provider_invite_token: None,
363            provider_user_id: None,
364        };
365
366        let result = internal_post_keys_for_user_password_registration(
367            &registration_client,
368            &identity_client,
369            request,
370        )
371        .await;
372
373        assert!(result.is_err());
374        assert!(matches!(result.unwrap_err(), RegistrationError::Api));
375
376        // check that mock expectations were met
377        if let IdentityApiClient::Mock(mut mock) = identity_client {
378            mock.accounts_api.checkpoint();
379        }
380    }
381}