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