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