Skip to main content

bitwarden_emergency_access/
confirm.rs

1use bitwarden_api_api::models::OrganizationUserConfirmRequestModel;
2use bitwarden_core::{ApiError, key_management::SymmetricKeySlotId};
3use bitwarden_crypto::{CryptoError, PublicKey, SpkiPublicKeyBytes, UnsignedSharedKey};
4use bitwarden_encoding::B64;
5use bitwarden_error::bitwarden_error;
6use thiserror::Error;
7
8use crate::{EmergencyAccessClient, EmergencyAccessId};
9
10/// Errors returned when confirming an emergency contact.
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum EmergencyAccessConfirmError {
14    /// The request to the server failed.
15    #[error(transparent)]
16    Api(#[from] ApiError),
17    /// The grantee public key is malformed, or the user key could not be encapsulated to it.
18    #[error(transparent)]
19    Crypto(#[from] CryptoError),
20}
21
22#[bitwarden_ffi::wasm_export]
23impl EmergencyAccessClient {
24    /// Shares the current user's user key with the grantee. Step 3 of the setup.
25    ///
26    /// `grantee_public_key` is the base64 SPKI DER public key the user verified through the
27    /// grantee's fingerprint. It is never fetched here, so an unverified key can't slip in.
28    ///
29    /// Called by the grantor.
30    pub async fn confirm(
31        &self,
32        emergency_access_id: EmergencyAccessId,
33        grantee_public_key: B64,
34    ) -> Result<(), EmergencyAccessConfirmError> {
35        // TODO: Use the trust log / KM APIs instead of a caller-supplied key.
36        let grantee_public_key =
37            PublicKey::from_der(&SpkiPublicKeyBytes::from(&grantee_public_key))?;
38
39        // Encapsulate inside the key store so the user key never leaves it.
40        let key = UnsignedSharedKey::encapsulate(
41            SymmetricKeySlotId::User,
42            &grantee_public_key,
43            &self.key_store.context(),
44        )?;
45
46        // The server reuses the organization confirm request model; only the key applies.
47        let request = OrganizationUserConfirmRequestModel {
48            key: key.to_string(),
49            default_user_collection_name: None,
50        };
51
52        self.api_configurations
53            .api_client
54            .emergency_access_api()
55            .confirm(emergency_access_id.into(), Some(request))
56            .await?;
57
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use std::sync::{Arc, Mutex};
65
66    use bitwarden_core::{
67        Client,
68        client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
69        key_management::PrivateKeySlotId,
70    };
71
72    use super::*;
73    use crate::{
74        EmergencyAccessClientExt,
75        test_support::{TEST_VECTOR_GRANTOR_KEY, api_error, is_test_id, test_client, test_id},
76    };
77
78    /// Base64 SPKI DER public key of the given client, as the clients pass it.
79    fn public_key_of(client: &Client) -> B64 {
80        let public_key = client
81            .internal
82            .get_key_store()
83            .context()
84            .get_public_key(PrivateKeySlotId::UserPrivateKey)
85            .unwrap();
86
87        public_key.to_der().unwrap().into()
88    }
89
90    #[tokio::test]
91    async fn shares_user_key_with_grantee() {
92        let grantee = Client::init_test_account(test_bitwarden_com_account_v2()).await;
93
94        let sent_key = Arc::new(Mutex::new(None));
95        let captured = sent_key.clone();
96        let grantor = test_client(test_bitwarden_com_account(), move |mock| {
97            mock.emergency_access_api
98                .expect_confirm()
99                .withf(|id, _| is_test_id(id))
100                .returning(move |_, request| {
101                    *captured.lock().unwrap() = request.map(|r| r.key);
102                    Ok(())
103                })
104                .once();
105        })
106        .await;
107
108        grantor
109            .emergency_access()
110            .confirm(test_id(), public_key_of(&grantee))
111            .await
112            .unwrap();
113
114        // The grantee decapsulates the same key as the recorded grantor key vector: the
115        // grantor's user key.
116        let sent_key: UnsignedSharedKey = sent_key.lock().unwrap().take().unwrap().parse().unwrap();
117        let recorded_key: UnsignedSharedKey = TEST_VECTOR_GRANTOR_KEY.parse().unwrap();
118
119        let key_store = grantee.internal.get_key_store();
120        let mut ctx = key_store.context();
121        let sent = sent_key
122            .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)
123            .unwrap();
124        let recorded = recorded_key
125            .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)
126            .unwrap();
127
128        #[allow(deprecated)]
129        let (sent, recorded) = (
130            ctx.dangerous_get_symmetric_key(sent).unwrap(),
131            ctx.dangerous_get_symmetric_key(recorded).unwrap(),
132        );
133        assert_eq!(sent, recorded);
134    }
135
136    #[tokio::test]
137    async fn fails_when_public_key_is_malformed() {
138        let grantor = test_client(test_bitwarden_com_account(), |mock| {
139            mock.emergency_access_api.expect_confirm().never();
140        })
141        .await;
142
143        let result = grantor
144            .emergency_access()
145            .confirm(test_id(), B64::from(b"not a key".as_slice()))
146            .await;
147
148        assert!(matches!(
149            result,
150            Err(EmergencyAccessConfirmError::Crypto(_))
151        ));
152    }
153
154    #[tokio::test]
155    async fn fails_when_request_fails() {
156        let grantee = Client::init_test_account(test_bitwarden_com_account_v2()).await;
157        let grantor = test_client(test_bitwarden_com_account(), |mock| {
158            mock.emergency_access_api
159                .expect_confirm()
160                .returning(|_, _| api_error())
161                .once();
162        })
163        .await;
164
165        let result = grantor
166            .emergency_access()
167            .confirm(test_id(), public_key_of(&grantee))
168            .await;
169
170        assert!(matches!(result, Err(EmergencyAccessConfirmError::Api(_))));
171    }
172}