Skip to main content

bitwarden_emergency_access/
takeover.rs

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/// Errors returned when taking over a grantor's account through emergency access.
21#[bitwarden_error(flat)]
22#[derive(Debug, Error)]
23pub enum EmergencyAccessTakeoverError {
24    /// The request to the server failed.
25    #[error(transparent)]
26    Api(#[from] ApiError),
27    /// A required field was missing from the server response.
28    #[error(transparent)]
29    MissingField(#[from] MissingFieldError),
30    /// The grantor key is malformed or could not be decapsulated with the current user's private
31    /// key.
32    #[error(transparent)]
33    Crypto(#[from] CryptoError),
34    /// The grantor's KDF is malformed, or the new master password data could not be derived.
35    #[error(transparent)]
36    MasterPassword(#[from] MasterPasswordError),
37}
38
39/// The takeover response parsed: the grantor key, KDF and salt.
40struct EmergencyAccessTakeoverData {
41    grantor_key: UnsignedSharedKey,
42    kdf: Kdf,
43    /// `None` on servers that predate the salt field.
44    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    /// Sets a new master password on the grantor's account of an approved takeover emergency
77    /// access.
78    ///
79    /// `grantor_email` is used as salt when the server doesn't return one.
80    ///
81    /// Called by the grantee.
82    pub async fn takeover(
83        &self,
84        emergency_access_id: EmergencyAccessId,
85        new_password: String,
86        grantor_email: String,
87    ) -> Result<(), EmergencyAccessTakeoverError> {
88        // The server returns the grantor's user key encapsulated to the current user's public key,
89        // and the grantor's KDF. The new master password is derived with that KDF and wraps the
90        // grantor's user key, which only lives in the key store while doing so.
91        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        // Servers that predate the salt field use the email-derived salt.
97        // TODO: PM-32059 - drop the fallback once the salt is decoupled from the email.
98        let salt = data
99            .salt
100            .unwrap_or_else(|| grantor_email.trim().to_lowercase());
101
102        // The key store context must not be held across the await below, so it is scoped here.
103        // TODO: Move this crypto into a dedicated bitwarden-emergency-access-crypto crate.
104        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    /// Takes over as the grantee and returns the result and the password request sent, if any.
186    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    /// Asserts the request sets the new password with `salt` and wraps the grantor's user key.
226    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        // Only the new authentication and unlock data are sent, like the clients did.
239        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        // The new password unwraps the same key as the recorded grantor key vector: the
258        // grantor's user key.
259        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}