1use std::num::NonZeroU32;
2
3use bitwarden_api_api::models::{
4 EmergencyAccessPasswordRequestModel, EmergencyAccessTakeoverResponseModel, KdfType,
5};
6use bitwarden_core::{
7 ApiError, MissingFieldError,
8 key_management::{
9 MasterPasswordAuthenticationData, MasterPasswordError, MasterPasswordUnlockData,
10 PrivateKeySlotId,
11 },
12 require,
13};
14use bitwarden_crypto::{CryptoError, Kdf, UnsignedSharedKey};
15use bitwarden_error::bitwarden_error;
16use thiserror::Error;
17
18use crate::{EmergencyAccessClient, EmergencyAccessId};
19
20#[bitwarden_error(flat)]
22#[derive(Debug, Error)]
23pub enum EmergencyAccessTakeoverError {
24 #[error(transparent)]
26 Api(#[from] ApiError),
27 #[error(transparent)]
29 MissingField(#[from] MissingFieldError),
30 #[error(transparent)]
33 Crypto(#[from] CryptoError),
34 #[error(transparent)]
36 MasterPassword(#[from] MasterPasswordError),
37}
38
39struct EmergencyAccessTakeoverData {
41 grantor_key: UnsignedSharedKey,
42 kdf: Kdf,
43 salt: Option<String>,
45}
46
47impl TryFrom<EmergencyAccessTakeoverResponseModel> for EmergencyAccessTakeoverData {
48 type Error = EmergencyAccessTakeoverError;
49
50 fn try_from(response: EmergencyAccessTakeoverResponseModel) -> Result<Self, Self::Error> {
51 let grantor_key = require!(response.key_encrypted).parse()?;
52
53 let iterations = require!(response.kdf_iterations);
54 let kdf = match require!(response.kdf) {
55 KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
56 iterations: kdf_parse_nonzero_u32(iterations)?,
57 },
58 KdfType::Argon2id => Kdf::Argon2id {
59 iterations: kdf_parse_nonzero_u32(iterations)?,
60 memory: kdf_parse_nonzero_u32(require!(response.kdf_memory))?,
61 parallelism: kdf_parse_nonzero_u32(require!(response.kdf_parallelism))?,
62 },
63 KdfType::__Unknown(_) => return Err(MasterPasswordError::KdfMalformed.into()),
64 };
65
66 Ok(Self {
67 grantor_key,
68 kdf,
69 salt: response.salt,
70 })
71 }
72}
73
74#[bitwarden_ffi::wasm_export]
75impl EmergencyAccessClient {
76 pub async fn takeover(
83 &self,
84 emergency_access_id: EmergencyAccessId,
85 new_password: String,
86 grantor_email: String,
87 ) -> Result<(), EmergencyAccessTakeoverError> {
88 let api = self.api_configurations.api_client.emergency_access_api();
92
93 let response = api.takeover(emergency_access_id.into()).await?;
94 let data = EmergencyAccessTakeoverData::try_from(response)?;
95
96 let salt = data
99 .salt
100 .unwrap_or_else(|| grantor_email.trim().to_lowercase());
101
102 let request = {
105 let mut ctx = self.key_store.context();
106 let grantor_key = data
107 .grantor_key
108 .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)?;
109
110 let authentication_data =
111 MasterPasswordAuthenticationData::derive(&new_password, &data.kdf, &salt)?;
112 let unlock_data = MasterPasswordUnlockData::derive(
113 &new_password,
114 &data.kdf,
115 &salt,
116 grantor_key,
117 &ctx,
118 )?;
119
120 EmergencyAccessPasswordRequestModel {
121 new_master_password_hash: None,
122 key: None,
123 unlock_data: Some(Box::new((&unlock_data).into())),
124 authentication_data: Some(Box::new((&authentication_data).into())),
125 }
126 };
127
128 api.password(emergency_access_id.into(), Some(request))
129 .await?;
130
131 Ok(())
132 }
133}
134
135fn kdf_parse_nonzero_u32(value: i32) -> Result<NonZeroU32, MasterPasswordError> {
136 u32::try_from(value)
137 .ok()
138 .and_then(NonZeroU32::new)
139 .ok_or(MasterPasswordError::KdfMalformed)
140}
141
142#[cfg(test)]
143mod tests {
144 use std::sync::{Arc, Mutex};
145
146 use bitwarden_api_api::models::KdfRequestModel;
147 use bitwarden_core::{Client, client::test_accounts::test_bitwarden_com_account_v2};
148 use bitwarden_crypto::EncString;
149
150 use super::*;
151 use crate::{
152 EmergencyAccessClientExt,
153 test_support::{
154 TEST_VECTOR_GRANTOR_KEY, TEST_VECTOR_WRONG_GRANTOR_KEY, api_error, is_test_id,
155 test_client, test_id,
156 },
157 };
158
159 const TEST_NEW_PASSWORD: &str = "new master password";
160 const TEST_GRANTOR_EMAIL: &str = " [email protected] ";
161 const TEST_SALT: &str = "server-salt";
162 const TEST_ITERATIONS: i32 = 600_000;
163
164 fn response(
165 key_encrypted: Option<&str>,
166 salt: Option<&str>,
167 ) -> EmergencyAccessTakeoverResponseModel {
168 EmergencyAccessTakeoverResponseModel {
169 object: None,
170 kdf: Some(KdfType::PBKDF2_SHA256),
171 kdf_iterations: Some(TEST_ITERATIONS),
172 kdf_memory: None,
173 kdf_parallelism: None,
174 key_encrypted: key_encrypted.map(str::to_owned),
175 salt: salt.map(str::to_owned),
176 }
177 }
178
179 fn pbkdf2() -> Kdf {
180 Kdf::PBKDF2 {
181 iterations: NonZeroU32::new(TEST_ITERATIONS as u32).unwrap(),
182 }
183 }
184
185 async fn takeover(
187 response: EmergencyAccessTakeoverResponseModel,
188 ) -> (
189 Client,
190 Result<(), EmergencyAccessTakeoverError>,
191 Option<EmergencyAccessPasswordRequestModel>,
192 ) {
193 let sent = Arc::new(Mutex::new(None));
194 let captured = sent.clone();
195
196 let client = test_client(test_bitwarden_com_account_v2(), move |mock| {
197 mock.emergency_access_api
198 .expect_takeover()
199 .withf(is_test_id)
200 .returning(move |_| Ok(response.clone()))
201 .once();
202 mock.emergency_access_api
203 .expect_password()
204 .withf(|id, _| is_test_id(id))
205 .returning(move |_, request| {
206 *captured.lock().unwrap() = request;
207 Ok(())
208 });
209 })
210 .await;
211
212 let result = client
213 .emergency_access()
214 .takeover(
215 test_id(),
216 TEST_NEW_PASSWORD.to_owned(),
217 TEST_GRANTOR_EMAIL.to_owned(),
218 )
219 .await;
220
221 let request = sent.lock().unwrap().take();
222 (client, result, request)
223 }
224
225 fn assert_sets_new_password(
227 client: &Client,
228 request: EmergencyAccessPasswordRequestModel,
229 salt: &str,
230 ) {
231 let expected_kdf = KdfRequestModel {
232 kdf_type: KdfType::PBKDF2_SHA256,
233 iterations: TEST_ITERATIONS,
234 memory: None,
235 parallelism: None,
236 };
237
238 assert_eq!(request.new_master_password_hash, None);
240 assert_eq!(request.key, None);
241
242 let authentication = request.authentication_data.unwrap();
243 assert_eq!(*authentication.kdf, expected_kdf);
244 assert_eq!(authentication.salt, salt);
245 assert_eq!(
246 authentication.master_password_authentication_hash,
247 MasterPasswordAuthenticationData::derive(TEST_NEW_PASSWORD, &pbkdf2(), salt)
248 .unwrap()
249 .master_password_authentication_hash
250 .to_string()
251 );
252
253 let unlock = request.unlock_data.unwrap();
254 assert_eq!(*unlock.kdf, expected_kdf);
255 assert_eq!(unlock.salt, salt);
256
257 let unlock_data = MasterPasswordUnlockData {
260 kdf: pbkdf2(),
261 master_key_wrapped_user_key: unlock
262 .master_key_wrapped_user_key
263 .parse::<EncString>()
264 .unwrap(),
265 salt: unlock.salt,
266 contained_key_id: None,
267 };
268 let recorded_key: UnsignedSharedKey = TEST_VECTOR_GRANTOR_KEY.parse().unwrap();
269
270 let key_store = client.internal.get_key_store();
271 let mut ctx = key_store.context();
272 let unwrapped = unlock_data
273 .unwrap_to_context(TEST_NEW_PASSWORD, &mut ctx)
274 .unwrap();
275 let recorded = recorded_key
276 .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)
277 .unwrap();
278
279 #[allow(deprecated)]
280 let (unwrapped, recorded) = (
281 ctx.dangerous_get_symmetric_key(unwrapped).unwrap(),
282 ctx.dangerous_get_symmetric_key(recorded).unwrap(),
283 );
284 assert_eq!(unwrapped, recorded);
285 }
286
287 #[tokio::test]
288 async fn sets_new_password_with_server_salt() {
289 let (client, result, request) =
290 takeover(response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))).await;
291
292 result.unwrap();
293 assert_sets_new_password(&client, request.unwrap(), TEST_SALT);
294 }
295
296 #[tokio::test]
297 async fn falls_back_to_email_salt() {
298 let (client, result, request) =
299 takeover(response(Some(TEST_VECTOR_GRANTOR_KEY), None)).await;
300
301 result.unwrap();
302 assert_sets_new_password(&client, request.unwrap(), "[email protected]");
303 }
304
305 #[tokio::test]
306 async fn fails_when_grantor_key_is_not_for_current_user() {
307 let (_, result, request) = takeover(response(
308 Some(TEST_VECTOR_WRONG_GRANTOR_KEY),
309 Some(TEST_SALT),
310 ))
311 .await;
312
313 assert!(matches!(
314 result,
315 Err(EmergencyAccessTakeoverError::Crypto(_))
316 ));
317 assert!(request.is_none());
318 }
319
320 #[tokio::test]
321 async fn fails_when_grantor_key_is_malformed() {
322 let (_, result, request) = takeover(response(Some("not a key"), Some(TEST_SALT))).await;
323
324 assert!(matches!(
325 result,
326 Err(EmergencyAccessTakeoverError::Crypto(_))
327 ));
328 assert!(request.is_none());
329 }
330
331 #[tokio::test]
332 async fn fails_when_grantor_key_is_missing() {
333 let (_, result, request) = takeover(response(None, Some(TEST_SALT))).await;
334
335 assert!(matches!(
336 result,
337 Err(EmergencyAccessTakeoverError::MissingField(_))
338 ));
339 assert!(request.is_none());
340 }
341
342 #[tokio::test]
343 async fn fails_when_kdf_is_missing() {
344 let response = EmergencyAccessTakeoverResponseModel {
345 kdf: None,
346 ..response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))
347 };
348
349 let (_, result, request) = takeover(response).await;
350
351 assert!(matches!(
352 result,
353 Err(EmergencyAccessTakeoverError::MissingField(_))
354 ));
355 assert!(request.is_none());
356 }
357
358 #[tokio::test]
359 async fn fails_when_kdf_is_malformed() {
360 let response = EmergencyAccessTakeoverResponseModel {
361 kdf_iterations: Some(0),
362 ..response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))
363 };
364
365 let (_, result, request) = takeover(response).await;
366
367 assert!(matches!(
368 result,
369 Err(EmergencyAccessTakeoverError::MasterPassword(
370 MasterPasswordError::KdfMalformed
371 ))
372 ));
373 assert!(request.is_none());
374 }
375
376 #[tokio::test]
377 async fn fails_when_takeover_request_fails() {
378 let client = test_client(test_bitwarden_com_account_v2(), |mock| {
379 mock.emergency_access_api
380 .expect_takeover()
381 .returning(|_| api_error())
382 .once();
383 mock.emergency_access_api.expect_password().never();
384 })
385 .await;
386
387 let result = client
388 .emergency_access()
389 .takeover(
390 test_id(),
391 TEST_NEW_PASSWORD.to_owned(),
392 TEST_GRANTOR_EMAIL.to_owned(),
393 )
394 .await;
395
396 assert!(matches!(result, Err(EmergencyAccessTakeoverError::Api(_))));
397 }
398
399 #[tokio::test]
400 async fn fails_when_password_request_fails() {
401 let response = response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT));
402 let client = test_client(test_bitwarden_com_account_v2(), move |mock| {
403 mock.emergency_access_api
404 .expect_takeover()
405 .returning(move |_| Ok(response.clone()))
406 .once();
407 mock.emergency_access_api
408 .expect_password()
409 .returning(|_, _| api_error())
410 .once();
411 })
412 .await;
413
414 let result = client
415 .emergency_access()
416 .takeover(
417 test_id(),
418 TEST_NEW_PASSWORD.to_owned(),
419 TEST_GRANTOR_EMAIL.to_owned(),
420 )
421 .await;
422
423 assert!(matches!(result, Err(EmergencyAccessTakeoverError::Api(_))));
424 }
425
426 #[test]
427 fn parses_argon2id_kdf() {
428 let response = EmergencyAccessTakeoverResponseModel {
429 kdf: Some(KdfType::Argon2id),
430 kdf_iterations: Some(3),
431 kdf_memory: Some(64),
432 kdf_parallelism: Some(4),
433 ..response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))
434 };
435
436 let data = EmergencyAccessTakeoverData::try_from(response).unwrap();
437
438 assert_eq!(
439 data.kdf,
440 Kdf::Argon2id {
441 iterations: NonZeroU32::new(3).unwrap(),
442 memory: NonZeroU32::new(64).unwrap(),
443 parallelism: NonZeroU32::new(4).unwrap(),
444 }
445 );
446 }
447
448 #[test]
449 fn rejects_argon2id_kdf_without_memory() {
450 let response = EmergencyAccessTakeoverResponseModel {
451 kdf: Some(KdfType::Argon2id),
452 kdf_parallelism: Some(4),
453 ..response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))
454 };
455
456 let result = EmergencyAccessTakeoverData::try_from(response);
457
458 assert!(matches!(
459 result,
460 Err(EmergencyAccessTakeoverError::MissingField(_))
461 ));
462 }
463
464 #[test]
465 fn rejects_unknown_kdf() {
466 let response = EmergencyAccessTakeoverResponseModel {
467 kdf: Some(KdfType::__Unknown(9)),
468 ..response(Some(TEST_VECTOR_GRANTOR_KEY), Some(TEST_SALT))
469 };
470
471 let result = EmergencyAccessTakeoverData::try_from(response);
472
473 assert!(matches!(
474 result,
475 Err(EmergencyAccessTakeoverError::MasterPassword(
476 MasterPasswordError::KdfMalformed
477 ))
478 ));
479 }
480}