Skip to main content

bitwarden_emergency_access/
view_vault_items.rs

1use bitwarden_api_api::models::EmergencyAccessViewResponseModel;
2use bitwarden_core::{ApiError, MissingFieldError, key_management::PrivateKeySlotId, require};
3use bitwarden_crypto::{CryptoError, Decryptable, UnsignedSharedKey};
4use bitwarden_error::bitwarden_error;
5use bitwarden_vault::{Cipher, DecryptCipherResult, VaultParseError};
6use thiserror::Error;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::prelude::wasm_bindgen;
9
10use crate::{EmergencyAccessClient, EmergencyAccessId};
11
12/// Errors returned when viewing a grantor's vault items through emergency access.
13#[bitwarden_error(flat)]
14#[derive(Debug, Error)]
15pub enum EmergencyAccessViewError {
16    /// The request to the server failed.
17    #[error(transparent)]
18    Api(#[from] ApiError),
19    /// A required field was missing from the server response.
20    #[error(transparent)]
21    MissingField(#[from] MissingFieldError),
22    /// The grantor key is malformed or could not be decapsulated with the current user's private
23    /// key.
24    #[error(transparent)]
25    Crypto(#[from] CryptoError),
26    /// A cipher in the server response could not be parsed.
27    #[error(transparent)]
28    VaultParse(#[from] VaultParseError),
29}
30
31/// A grantor's vault items shared through emergency access.
32struct EmergencyAccessViewData {
33    /// The grantor's user key, encapsulated to the current user's public key.
34    grantor_key: UnsignedSharedKey,
35    ciphers: Vec<Cipher>,
36}
37
38impl TryFrom<EmergencyAccessViewResponseModel> for EmergencyAccessViewData {
39    type Error = EmergencyAccessViewError;
40
41    fn try_from(response: EmergencyAccessViewResponseModel) -> Result<Self, Self::Error> {
42        let grantor_key = require!(response.key_encrypted).parse()?;
43
44        let ciphers = response
45            .ciphers
46            .unwrap_or_default()
47            .into_iter()
48            .map(Cipher::try_from)
49            .collect::<Result<_, _>>()?;
50
51        Ok(Self {
52            grantor_key,
53            ciphers,
54        })
55    }
56}
57
58#[cfg_attr(feature = "wasm", wasm_bindgen)]
59impl EmergencyAccessClient {
60    /// Fetches and decrypts the grantor's vault items of an approved view-only emergency access.
61    pub async fn view_vault_items(
62        &self,
63        emergency_access_id: EmergencyAccessId,
64    ) -> Result<DecryptCipherResult, EmergencyAccessViewError> {
65        let response = self
66            .api_configurations
67            .api_client
68            .emergency_access_api()
69            .view_ciphers(emergency_access_id.into())
70            .await?;
71
72        let data = EmergencyAccessViewData::try_from(response)?;
73
74        // The server returns the grantor's user key encapsulated to the current user's public key;
75        // it only lives in the key store while the ciphers are decrypted. Ciphers that fail to
76        // decrypt are returned in `failures`.
77        let mut ctx = self.key_store.context();
78        let grantor_key_id = data
79            .grantor_key
80            .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)?;
81
82        let mut successes = Vec::with_capacity(data.ciphers.len());
83        let mut failures = Vec::new();
84        for cipher in data.ciphers {
85            match cipher.decrypt(&mut ctx, grantor_key_id) {
86                Ok(view) => successes.push(view),
87                Err(_) => failures.push(cipher),
88            }
89        }
90
91        Ok(DecryptCipherResult {
92            successes,
93            failures,
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel};
101    use bitwarden_core::{Client, client::test_accounts::test_bitwarden_com_account_v2};
102    use bitwarden_vault::VaultClientExt;
103
104    use super::*;
105    use crate::EmergencyAccessClientExt;
106
107    // Recorded with the grantor `test_bitwarden_com_account` and the grantee
108    // `test_bitwarden_com_account_v2`. Never regenerate these to make a test pass: a vector that
109    // stops decrypting is a backward-compatibility break. Ciphers are in the server's
110    // `CipherResponseModel` shape.
111
112    /// Grantor user key encapsulated to the grantee's public key, as the server returns it.
113    const TEST_VECTOR_GRANTOR_KEY: &str = "4.bbiAjKYUjktIP1PggRJ+ha+O8M0LxWCNkv2bir5QHIYQnAwtx9X3Cta4j0JFnDS2Zy/UFAzRMCpdLR1DgupZ31lZvAhThC86hDvMMa0h84d7o41Rx9tCvqW1FcziuAA0UEr+Gfkc0+qX5yPFEn2eJ2U+ft9J7rSTTDeha1QIMhYnTf2sD2O0nSWSSPoFNLLg2WKoe9uXC5w+MemcK6x6ogG2T77fDr5trXnK7SoWamAWjhK65bYxYrSIpJOtdCR0O9ZIqiITg0lS0I6ACbiFBpHyo+wb+ZUCieUWrNaBjNHHES2XqEWSVVgz1XL2Q0Vh9XNwwPZLbIWZDxbz8FWqeQ==";
114    /// Grantor user key encapsulated to the grantor's own public key.
115    const TEST_VECTOR_WRONG_GRANTOR_KEY: &str = "4.PBhgsvKoOu8weZWhRAjPoSrucZneEh0Nc+R3xATSuJQk0GyS+xAagJX3Kh7EuPM2pU2OjGbGJeutDpNbp1EKWyYnPzbgXkMi8VU7dnbT8FmidRr/BcrNdcaNzyzJ06GPnL+wPH047iEhzP6DK7prjd3ufEtRp+x9ZCHPnN/vqzoWJD8AZfbC1c8jQFJSaTV/DfkKrF5rfTH3qoQYUNdfL5HOsbTHM8HTMsR18qtj4w638le1ejsjFFgAihcCdlLRsu6okGWCV6LZK3LyjfzXr/WliJUPV3/lySCHEyZoefK52v3TJ8Xy22Tqo+Y86IA/N/UA58c6tV/Iu1Gv3KQsPA==";
116    /// Login "legacy" (password "pass"), legacy field-level encryption under the grantor user key.
117    const TEST_VECTOR_LEGACY_CIPHER: &str = r#"{"id":null,"organizationId":null,"folderId":null,"key":null,"name":"2.464q0KbAxs3hIhCJ6O/Xxw==|Kwcifl1PAd2KsD4o6nlFdQ==|HrJmneai/21EDb9OmTTQct6b3/nn3ivCIU5SqW0/in4=","notes":null,"type":1,"login":{"username":"2.8e+rqQTBrICEiROAaFShsg==|LlnpGmz/4Vz+/qvVvKeFHA==|1a5pAef2hhJfNFhj45Nok8Boq+wfLC70s3ss/Bbze0s=","password":"2.SPnq6YStUb41/JwNrW8gtA==|12wZdM4ieSJJZnBHo3KWlg==|WIleZuG8RdHQNV3DCWWUL3phwDU83W+7igU2E2LYSJA=","passwordRevisionDate":null,"uris":null,"totp":null,"autofillOnPageLoad":null,"fido2Credentials":null},"identity":null,"card":null,"secureNote":null,"sshKey":null,"bankAccount":null,"driversLicense":null,"passport":null,"favorite":false,"reprompt":0,"organizationUseTotp":false,"edit":true,"permissions":null,"viewPassword":true,"attachments":null,"fields":null,"passwordHistory":null,"creationDate":"2024-01-01T00:00:00Z","deletedDate":null,"revisionDate":"2024-01-01T00:00:00Z","archivedDate":null,"data":null}"#;
118    /// Login "blob" (password "pass"), blob encryption under the grantor user key.
119    const TEST_VECTOR_BLOB_CIPHER: &str = r#"{"id":null,"organizationId":null,"folderId":null,"key":"2.Mup/pT+Qs7w8ypIQHNDPeg==|MwjPTr4GrhCD1QA5fntk3XzCduTHKeY8YFEpDceoolLboLE+7rb8rTGWqf2QJVAUxqgtCejT6J1w55YormzjYEP1pTbP1onXYNTnyeyjk1s=|W35zL1xvXDRD9YB0QFV98lAi/qAp+MvAgII1upUrujc=","name":null,"notes":null,"type":1,"login":null,"identity":null,"card":null,"secureNote":null,"sshKey":null,"bankAccount":null,"driversLicense":null,"passport":null,"favorite":false,"reprompt":0,"organizationUseTotp":false,"edit":true,"permissions":null,"viewPassword":true,"attachments":null,"fields":null,"passwordHistory":null,"creationDate":"2024-01-01T00:00:00Z","deletedDate":null,"revisionDate":"2024-01-01T00:00:00Z","archivedDate":null,"data":"{\"format_version\":1,\"wrapped_cek\":\"2.tI8VAPnduRNp5mqrhGAozQ==|NYgnD7bpK8lRRKm8Xp66YSdpETrLDYZhfDDLTCCmm+NReNPwUEH+PNCWiDmupXA25MHipVR+taGO5+fQgDUDMB+wYZhki3QWvigxV+EO9LA=|UdjlfQgWLtkdHEykgWl05c2Km0OJDaXq56LRSveiRp0=\",\"envelope\":\"g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUNb+5HvtZQFf1MDlluqcjhk6AAE4gQI6AAE4gAGhBUzcWa1lfmjYjNurpWhYl8LLSI7A4ZZjiUj92GFVFiH4CyzpSWdrEpmYv9e7MKsupI+nJJbO8BrPGkSepord8QKA0TNDbkPtVNpwgqkSDlKJ0Kx2nLGMA4S/RdfouKJZRpyv6hUV4gMRUF2zC8DWUhIQTBXm81l6b2aMGe0zLe9Jd4ZTxNREt0PIot8+dE4559EPpfN/DPl7VeXZNHKJzGIW56fXYac=\"}"}"#;
120    /// Login "other", blob encryption under a key unrelated to the grantor.
121    const TEST_VECTOR_OTHER_KEY_CIPHER: &str = r#"{"id":null,"organizationId":null,"folderId":null,"key":"2.hpRJrOTB+2Nmiy5CgVujjA==|XwGLovy+DP7oVIUw6uS/FO3tKjVxloggdd3SS/hyXkl4jOTz4wj+sKtNRfQBClZ0gxhA9xB2G4H3k/FSVr3ZKN8I31AQvJaFmYJb/QnKRok=|xAJygGDtWjsDDEwEIsBQOqk8dsfWmdpFvJchbA+CXLs=","name":null,"notes":null,"type":1,"login":null,"identity":null,"card":null,"secureNote":null,"sshKey":null,"bankAccount":null,"driversLicense":null,"passport":null,"favorite":false,"reprompt":0,"organizationUseTotp":false,"edit":true,"permissions":null,"viewPassword":true,"attachments":null,"fields":null,"passwordHistory":null,"creationDate":"2024-01-01T00:00:00Z","deletedDate":null,"revisionDate":"2024-01-01T00:00:00Z","archivedDate":null,"data":"{\"format_version\":1,\"wrapped_cek\":\"2.5blC2aAA8CSG9+0TGwM8kQ==|N8agG47rDOKXfad8IRXiBYn4sdSqIRblOBG3cTmF4MNatMWMyNQ1eiBI3ESwXFGckSWXDWDiIYjcA5PtSTuZD3TMHbEViA35ETPyZ3p7TQk=|+JAq5PcDmGn33DroTbFHNVk2foKW0zD+0ciiZgK2R+4=\",\"envelope\":\"g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUH+IGveCBNLVKkWxFC6Cj8A6AAE4gQI6AAE4gAGhBUwhbDAGkN2AhbXVNQtYmOEmXbdcL1B2nsuoI906sAHORuyQu19HsuQzu/bZiOyuHGR/f5Vi0EOhVa0+AhHZoUGI29BE+PgJOnVjuHk25z8j7XHqiw4qt+CKcbJRR6o2RC5QUgEvFl1M9+3P9wuVkijs7hu8CaCjcyyGGA3KnN3vJ5EXP4oqtteamB1jyHx9PxXV2ZuCQVDsFb6bMoO/bnBhTSuLe22x\"}"}"#;
122
123    const TEST_EMERGENCY_ACCESS_ID: &str = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
124
125    fn cipher_response(json: &str) -> CipherResponseModel {
126        serde_json::from_str(json).unwrap()
127    }
128
129    /// Creates the grantee client whose API returns the given view response.
130    async fn grantee(key_encrypted: Option<&str>, ciphers: &[&str]) -> Client {
131        let response = EmergencyAccessViewResponseModel {
132            object: None,
133            key_encrypted: key_encrypted.map(str::to_owned),
134            ciphers: Some(ciphers.iter().map(|c| cipher_response(c)).collect()),
135        };
136
137        let api_client = ApiClient::new_mocked(move |mock| {
138            mock.emergency_access_api
139                .expect_view_ciphers()
140                .withf(|id| id.to_string() == TEST_EMERGENCY_ACCESS_ID)
141                .returning(move |_| Ok(response.clone()))
142                .once();
143        });
144
145        Client::init_test_account_with_api_client(test_bitwarden_com_account_v2(), api_client).await
146    }
147
148    async fn view(client: &Client) -> Result<DecryptCipherResult, EmergencyAccessViewError> {
149        client
150            .emergency_access()
151            .view_vault_items(TEST_EMERGENCY_ACCESS_ID.parse().unwrap())
152            .await
153    }
154
155    #[tokio::test]
156    async fn decrypts_legacy_and_blob_vault_items_with_grantor_key() {
157        let client = grantee(
158            Some(TEST_VECTOR_GRANTOR_KEY),
159            &[TEST_VECTOR_LEGACY_CIPHER, TEST_VECTOR_BLOB_CIPHER],
160        )
161        .await;
162
163        // The grantee cannot decrypt with their own user key.
164        let blob = Cipher::try_from(cipher_response(TEST_VECTOR_BLOB_CIPHER)).unwrap();
165        assert!(client.vault().ciphers().decrypt(blob).await.is_err());
166
167        let result = view(&client).await.unwrap();
168
169        let names: Vec<_> = result.successes.iter().map(|c| c.name.as_str()).collect();
170        assert_eq!(names, ["legacy", "blob"]);
171        assert!(result.failures.is_empty());
172        for view in &result.successes {
173            assert_eq!(
174                view.login.as_ref().unwrap().password.as_deref(),
175                Some("pass")
176            );
177        }
178    }
179
180    #[tokio::test]
181    async fn reports_vault_items_under_other_keys_as_failures() {
182        // Blob decryption fails hard, unlike lenient legacy decryption which nulls out
183        // undecryptable fields.
184        let client = grantee(
185            Some(TEST_VECTOR_GRANTOR_KEY),
186            &[TEST_VECTOR_OTHER_KEY_CIPHER, TEST_VECTOR_LEGACY_CIPHER],
187        )
188        .await;
189
190        let result = view(&client).await.unwrap();
191
192        assert_eq!(result.successes.len(), 1);
193        assert_eq!(result.successes[0].name, "legacy");
194        assert_eq!(result.failures.len(), 1);
195    }
196
197    #[tokio::test]
198    async fn fails_when_grantor_key_is_not_for_current_user() {
199        let client = grantee(
200            Some(TEST_VECTOR_WRONG_GRANTOR_KEY),
201            &[TEST_VECTOR_LEGACY_CIPHER],
202        )
203        .await;
204
205        let result = view(&client).await;
206
207        assert!(matches!(result, Err(EmergencyAccessViewError::Crypto(_))));
208    }
209
210    #[tokio::test]
211    async fn fails_when_grantor_key_is_malformed() {
212        let client = grantee(Some("not a key"), &[TEST_VECTOR_LEGACY_CIPHER]).await;
213
214        let result = view(&client).await;
215
216        assert!(matches!(result, Err(EmergencyAccessViewError::Crypto(_))));
217    }
218
219    #[tokio::test]
220    async fn fails_when_grantor_key_is_missing() {
221        let client = grantee(None, &[TEST_VECTOR_LEGACY_CIPHER]).await;
222
223        let result = view(&client).await;
224
225        assert!(matches!(
226            result,
227            Err(EmergencyAccessViewError::MissingField(_))
228        ));
229    }
230}