1use 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#[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 pub org_id: OrganizationId,
30 pub org_public_key: B64,
33 pub organization_sso_identifier: String,
35 pub user_id: UserId,
37 pub salt: String,
39 pub master_password: String,
41 pub master_password_hint: Option<String>,
43 pub reset_password_enroll: bool,
45}
46
47#[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 pub account_cryptographic_state: WrappedAccountCryptographicState,
58 pub master_password_unlock: MasterPasswordUnlockData,
60 pub user_key: B64,
62}
63
64#[cfg_attr(feature = "wasm", wasm_bindgen)]
65impl RegistrationClient {
66 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 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 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 (®istration_crypto_result.master_password_unlock_data).into(),
103 )),
104 master_password_authentication: Some(Box::new(
105 (®istration_crypto_result.master_password_authentication_data).into(),
106 )),
107 master_password_hint: request.master_password_hint,
108 org_identifier: request.organization_sso_identifier,
109 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 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 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 ®istration_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 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 ®istration_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 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 ®istration_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 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 ®istration_client,
462 &api_client,
463 request,
464 )
465 .await;
466
467 assert!(result.is_ok());
468
469 if let ApiClient::Mock(mut mock) = api_client {
471 mock.accounts_api.checkpoint();
472 mock.organization_users_api.checkpoint();
473 }
474 }
475}