Skip to main content

bitwarden_auth/registration/
post_keys_for_jit_password_registration.rs

1//! Initializes a new cryptographic state for a user and posts it to the server;
2//! enrolls the user to master password unlock.
3use bitwarden_api_api::models::{
4    OrganizationUserResetPasswordEnrollmentRequestModel, SetInitialPasswordRequestModel,
5};
6use bitwarden_core::{
7    OrganizationId, UserId,
8    key_management::{
9        MasterPasswordUnlockData, account_cryptographic_state::WrappedAccountCryptographicState,
10    },
11};
12use bitwarden_encoding::B64;
13use tracing::{error, info};
14#[cfg(feature = "wasm")]
15use wasm_bindgen::prelude::*;
16
17use crate::registration::{RegistrationClient, RegistrationError};
18
19/// Request parameters for SSO JIT master password registration.
20#[cfg_attr(
21    feature = "wasm",
22    derive(tsify::Tsify),
23    tsify(into_wasm_abi, from_wasm_abi)
24)]
25#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
26#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
27pub struct JitMasterPasswordRegistrationRequest {
28    /// Organization ID to enroll in
29    pub org_id: OrganizationId,
30    /// Organization's public key for encrypting the reset password key. This should be verified by
31    /// the client and not verifying may compromise the security of the user's account.
32    pub org_public_key: B64,
33    /// Organization SSO identifier
34    pub organization_sso_identifier: String,
35    /// User ID for the account being initialized
36    pub user_id: UserId,
37    /// Salt for master password hashing, usually email
38    pub salt: String,
39    /// Master password for the account
40    pub master_password: String,
41    /// Optional hint for the master password
42    pub master_password_hint: Option<String>,
43    /// Should enroll user into admin password reset
44    pub reset_password_enroll: bool,
45}
46
47/// Result of JIT master password registration process.
48#[cfg_attr(
49    feature = "wasm",
50    derive(tsify::Tsify),
51    tsify(into_wasm_abi, from_wasm_abi)
52)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
55pub struct JitMasterPasswordRegistrationResponse {
56    /// The account cryptographic state of the user
57    pub account_cryptographic_state: WrappedAccountCryptographicState,
58    /// The master password unlock data
59    pub master_password_unlock: MasterPasswordUnlockData,
60    /// The decrypted user key.
61    pub user_key: B64,
62}
63
64#[cfg_attr(feature = "wasm", wasm_bindgen)]
65impl RegistrationClient {
66    /// Initializes a new cryptographic state for a user and posts it to the server;
67    /// enrolls the user to master password unlock.
68    pub async fn post_keys_for_jit_password_registration(
69        &self,
70        request: JitMasterPasswordRegistrationRequest,
71    ) -> Result<JitMasterPasswordRegistrationResponse, RegistrationError> {
72        let client = &self.client.internal;
73        let api_client = &client.get_api_configurations().api_client;
74        internal_post_keys_for_jit_password_registration(self, api_client, request).await
75    }
76}
77
78async fn internal_post_keys_for_jit_password_registration(
79    registration_client: &RegistrationClient,
80    api_client: &bitwarden_api_api::apis::ApiClient,
81    request: JitMasterPasswordRegistrationRequest,
82) -> Result<JitMasterPasswordRegistrationResponse, RegistrationError> {
83    // First call crypto API to get all keys
84    info!("Initializing account cryptography");
85    let registration_crypto_result = registration_client
86        .client
87        .crypto()
88        .make_user_jit_master_password_registration(
89            request.master_password,
90            request.salt,
91            request.org_public_key,
92        )
93        .map_err(|_| RegistrationError::Crypto)?;
94
95    // Post the generated keys to the API here. The user now has keys and is "registered", but
96    // has no unlock method.
97    let api_request = SetInitialPasswordRequestModel {
98        account_keys: Some(Box::new(
99            registration_crypto_result.account_keys_request.clone(),
100        )),
101        master_password_unlock: Some(Box::new(
102            (&registration_crypto_result.master_password_unlock_data).into(),
103        )),
104        master_password_authentication: Some(Box::new(
105            (&registration_crypto_result.master_password_authentication_data).into(),
106        )),
107        master_password_hint: request.master_password_hint,
108        org_identifier: request.organization_sso_identifier,
109        // TODO Deprecated fields below, to be removed with https://bitwarden.atlassian.net/browse/PM-27327
110        kdf_parallelism: None,
111        master_password_hash: None,
112        key: None,
113        keys: None,
114        kdf: None,
115        kdf_iterations: None,
116        kdf_memory: None,
117    };
118    info!("Posting user account cryptographic state to server");
119    api_client
120        .accounts_api()
121        .post_set_password(Some(api_request))
122        .await
123        .map_err(|e| {
124            error!("Failed to post account keys: {e:?}");
125            RegistrationError::Api
126        })?;
127
128    // Enroll the user for reset password using the reset password key generated above.
129    if request.reset_password_enroll {
130        info!("Enrolling into admin account recovery");
131        api_client
132            .organization_users_api()
133            .put_reset_password_enrollment(
134                request.org_id.into(),
135                request.user_id.into(),
136                Some(OrganizationUserResetPasswordEnrollmentRequestModel {
137                    reset_password_key: Some(
138                        registration_crypto_result.reset_password_key.to_string(),
139                    ),
140                    master_password_hash: Some(
141                        registration_crypto_result
142                            .master_password_authentication_data
143                            .master_password_authentication_hash
144                            .to_string(),
145                    ),
146                }),
147            )
148            .await
149            .map_err(|e| {
150                error!("Failed to enroll for reset password: {e:?}");
151                RegistrationError::Api
152            })?;
153    }
154
155    info!("User initialized!");
156    // Note: This passing out of state and keys is temporary. Once SDK state management is more
157    // mature, the account cryptographic state and keys should be set directly here.
158    Ok(JitMasterPasswordRegistrationResponse {
159        account_cryptographic_state: registration_crypto_result.account_cryptographic_state,
160        master_password_unlock: registration_crypto_result.master_password_unlock_data,
161        user_key: registration_crypto_result
162            .user_key
163            .to_encoded()
164            .to_vec()
165            .into(),
166    })
167}
168
169#[cfg(test)]
170mod tests {
171    use std::num::NonZeroU32;
172
173    use bitwarden_api_api::{
174        apis::ApiClient,
175        models::{KdfRequestModel, KdfType},
176    };
177    use bitwarden_core::Client;
178    use bitwarden_crypto::{EncString, Kdf};
179
180    use super::*;
181
182    const TEST_USER_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8";
183    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
184    const TEST_SSO_ORG_IDENTIFIER: &str = "test-org";
185
186    const TEST_ORG_PUBLIC_KEY: &[u8] = &[
187        48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
188        48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
189        115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
190        229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
191        243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
192        88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
193        76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
194        195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
195        14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
196        80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
197        211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
198        12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
199        255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
200        248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
201        201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
202    ];
203
204    #[tokio::test]
205    async fn test_post_keys_for_jit_password_registration_success() {
206        let client = Client::new(None);
207        let registration_client = RegistrationClient::new(client);
208
209        let expected_hint = "test hint";
210
211        let api_client = ApiClient::new_mocked(|mock| {
212            mock.accounts_api
213                .expect_post_set_password()
214                .once()
215                .withf(move |body| {
216                    if let Some(req) = body {
217                        assert_eq!(req.org_identifier, TEST_SSO_ORG_IDENTIFIER);
218                        assert_eq!(req.master_password_hint, Some(expected_hint.to_string()));
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, "[email protected]".to_string());
250                        assert_eq!(
251                            master_password_unlock.kdf,
252                            Box::new(KdfRequestModel {
253                                kdf_type: 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!(
263                            master_password_authentication.salt,
264                            "[email protected]".to_string()
265                        );
266                        assert_eq!(
267                            master_password_authentication.kdf,
268                            Box::new(KdfRequestModel {
269                                kdf_type: KdfType::Argon2id,
270                                iterations: 6,
271                                memory: Some(32),
272                                parallelism: Some(4),
273                            })
274                        );
275                        true
276                    } else {
277                        false
278                    }
279                })
280                .returning(move |_body| Ok(()));
281            mock.organization_users_api
282                .expect_put_reset_password_enrollment()
283                .once()
284                .withf(move |org_id, user_id, body| {
285                    assert_eq!(*org_id, uuid::uuid!(TEST_ORG_ID));
286                    assert_eq!(*user_id, uuid::uuid!(TEST_USER_ID));
287                    if let Some(enrollment_request) = body {
288                        assert!(enrollment_request.reset_password_key.is_some());
289                        assert!(enrollment_request.master_password_hash.is_some());
290                        true
291                    } else {
292                        false
293                    }
294                })
295                .returning(move |_org_id, _user_id, _body| Ok(()));
296        });
297
298        let request = JitMasterPasswordRegistrationRequest {
299            org_id: TEST_ORG_ID.parse().unwrap(),
300            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
301            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
302            user_id: TEST_USER_ID.parse().unwrap(),
303            salt: "[email protected]".to_string(),
304            master_password: "test-password-123".to_string(),
305            master_password_hint: Some(expected_hint.to_string()),
306            reset_password_enroll: true,
307        };
308
309        let result = internal_post_keys_for_jit_password_registration(
310            &registration_client,
311            &api_client,
312            request,
313        )
314        .await;
315
316        assert!(result.is_ok());
317        let result = result.unwrap();
318        assert!(matches!(
319            result.account_cryptographic_state,
320            WrappedAccountCryptographicState::V2 { .. }
321        ));
322        assert_eq!(result.master_password_unlock.salt, "[email protected]");
323        assert!(matches!(
324            result.master_password_unlock.master_key_wrapped_user_key,
325            EncString::Aes256Cbc_HmacSha256_B64 { .. }
326        ));
327        assert_eq!(
328            result.master_password_unlock.kdf,
329            Kdf::Argon2id {
330                iterations: NonZeroU32::new(6).unwrap(),
331                memory: NonZeroU32::new(32).unwrap(),
332                parallelism: NonZeroU32::new(4).unwrap(),
333            }
334        );
335
336        // Assert that the mock expectations were met
337        if let ApiClient::Mock(mut mock) = api_client {
338            mock.accounts_api.checkpoint();
339            mock.organization_users_api.checkpoint();
340        }
341    }
342
343    #[tokio::test]
344    async fn test_post_keys_for_jit_password_registration_api_failure() {
345        let client = Client::new(None);
346        let registration_client = RegistrationClient::new(client);
347
348        let api_client = ApiClient::new_mocked(|mock| {
349            mock.accounts_api
350                .expect_post_set_password()
351                .once()
352                .returning(move |_body| {
353                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
354                });
355            mock.organization_users_api
356                .expect_put_reset_password_enrollment()
357                .never();
358        });
359
360        let request = JitMasterPasswordRegistrationRequest {
361            org_id: TEST_ORG_ID.parse().unwrap(),
362            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
363            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
364            user_id: TEST_USER_ID.parse().unwrap(),
365            salt: "[email protected]".to_string(),
366            master_password: "test-password-123".to_string(),
367            master_password_hint: Some("test hint".to_string()),
368            reset_password_enroll: true,
369        };
370
371        let result = internal_post_keys_for_jit_password_registration(
372            &registration_client,
373            &api_client,
374            request,
375        )
376        .await;
377
378        assert!(result.is_err());
379        assert!(matches!(result.unwrap_err(), RegistrationError::Api));
380
381        // Assert that the mock expectations were met
382        if let ApiClient::Mock(mut mock) = api_client {
383            mock.accounts_api.checkpoint();
384            mock.organization_users_api.checkpoint();
385        }
386    }
387
388    #[tokio::test]
389    async fn test_post_keys_for_jit_password_registration_reset_password_enrollment_failure() {
390        let client = Client::new(None);
391        let registration_client = RegistrationClient::new(client);
392
393        let api_client = ApiClient::new_mocked(|mock| {
394            mock.accounts_api
395                .expect_post_set_password()
396                .once()
397                .returning(move |_body| Ok(()));
398            mock.organization_users_api
399                .expect_put_reset_password_enrollment()
400                .once()
401                .returning(move |_org_id, _user_id, _body| {
402                    Err(serde_json::Error::io(std::io::Error::other("API error")).into())
403                });
404        });
405
406        let request = JitMasterPasswordRegistrationRequest {
407            org_id: TEST_ORG_ID.parse().unwrap(),
408            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
409            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
410            user_id: TEST_USER_ID.parse().unwrap(),
411            salt: "[email protected]".to_string(),
412            master_password: "test-password-123".to_string(),
413            master_password_hint: Some("test hint".to_string()),
414            reset_password_enroll: true,
415        };
416
417        let result = internal_post_keys_for_jit_password_registration(
418            &registration_client,
419            &api_client,
420            request,
421        )
422        .await;
423
424        assert!(result.is_err());
425        assert!(matches!(result.unwrap_err(), RegistrationError::Api));
426
427        // Assert that the mock expectations were met
428        if let ApiClient::Mock(mut mock) = api_client {
429            mock.accounts_api.checkpoint();
430            mock.organization_users_api.checkpoint();
431        }
432    }
433
434    #[tokio::test]
435    async fn test_post_keys_for_jit_password_registration_reset_password_enroll_false() {
436        let client = Client::new(None);
437        let registration_client = RegistrationClient::new(client);
438
439        let api_client = ApiClient::new_mocked(|mock| {
440            mock.accounts_api
441                .expect_post_set_password()
442                .once()
443                .returning(move |_body| Ok(()));
444            mock.organization_users_api
445                .expect_put_reset_password_enrollment()
446                .never();
447        });
448
449        let request = JitMasterPasswordRegistrationRequest {
450            org_id: TEST_ORG_ID.parse().unwrap(),
451            org_public_key: TEST_ORG_PUBLIC_KEY.into(),
452            organization_sso_identifier: TEST_SSO_ORG_IDENTIFIER.to_string(),
453            user_id: TEST_USER_ID.parse().unwrap(),
454            salt: "[email protected]".to_string(),
455            master_password: "test-password-123".to_string(),
456            master_password_hint: Some("test hint".to_string()),
457            reset_password_enroll: false,
458        };
459
460        let result = internal_post_keys_for_jit_password_registration(
461            &registration_client,
462            &api_client,
463            request,
464        )
465        .await;
466
467        assert!(result.is_ok());
468
469        // Assert that the mock expectations were met
470        if let ApiClient::Mock(mut mock) = api_client {
471            mock.accounts_api.checkpoint();
472            mock.organization_users_api.checkpoint();
473        }
474    }
475}