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
8use crate::{EmergencyAccessClient, EmergencyAccessId};
9
10/// Errors returned when viewing a grantor's vault items through emergency access.
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum EmergencyAccessViewError {
14    /// The request to the server failed.
15    #[error(transparent)]
16    Api(#[from] ApiError),
17    /// A required field was missing from the server response.
18    #[error(transparent)]
19    MissingField(#[from] MissingFieldError),
20    /// The grantor key is malformed or could not be decapsulated with the current user's private
21    /// key.
22    #[error(transparent)]
23    Crypto(#[from] CryptoError),
24    /// A cipher in the server response could not be parsed.
25    #[error(transparent)]
26    VaultParse(#[from] VaultParseError),
27}
28
29/// A grantor's vault items shared through emergency access.
30struct EmergencyAccessViewData {
31    /// The grantor's user key, encapsulated to the current user's public key.
32    grantor_key: UnsignedSharedKey,
33    ciphers: Vec<Cipher>,
34}
35
36impl TryFrom<EmergencyAccessViewResponseModel> for EmergencyAccessViewData {
37    type Error = EmergencyAccessViewError;
38
39    fn try_from(response: EmergencyAccessViewResponseModel) -> Result<Self, Self::Error> {
40        let grantor_key = require!(response.key_encrypted).parse()?;
41
42        let ciphers = response
43            .ciphers
44            .unwrap_or_default()
45            .into_iter()
46            .map(Cipher::try_from)
47            .collect::<Result<_, _>>()?;
48
49        Ok(Self {
50            grantor_key,
51            ciphers,
52        })
53    }
54}
55
56#[bitwarden_ffi::wasm_export]
57impl EmergencyAccessClient {
58    /// Fetches and decrypts the grantor's vault items of an approved view-only emergency access.
59    pub async fn view_vault_items(
60        &self,
61        emergency_access_id: EmergencyAccessId,
62    ) -> Result<DecryptCipherResult, EmergencyAccessViewError> {
63        let response = self
64            .api_configurations
65            .api_client
66            .emergency_access_api()
67            .view_ciphers(emergency_access_id.into())
68            .await?;
69
70        let data = EmergencyAccessViewData::try_from(response)?;
71
72        // The server returns the grantor's user key encapsulated to the current user's public key;
73        // it only lives in the key store while the ciphers are decrypted. Ciphers that fail to
74        // decrypt are returned in `failures`.
75        let mut ctx = self.key_store.context();
76        let grantor_key_id = data
77            .grantor_key
78            .decapsulate(PrivateKeySlotId::UserPrivateKey, &mut ctx)?;
79
80        let mut successes = Vec::with_capacity(data.ciphers.len());
81        let mut failures = Vec::new();
82        for cipher in data.ciphers {
83            match cipher.decrypt(&mut ctx, grantor_key_id) {
84                Ok(view) => successes.push(view),
85                Err(_) => failures.push(cipher),
86            }
87        }
88
89        Ok(DecryptCipherResult {
90            successes,
91            failures,
92        })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel};
99    use bitwarden_core::{Client, client::test_accounts::test_bitwarden_com_account_v2};
100    use bitwarden_vault::VaultClientExt;
101
102    use super::*;
103    use crate::{
104        EmergencyAccessClientExt,
105        test_support::{
106            TEST_EMERGENCY_ACCESS_ID, TEST_VECTOR_GRANTOR_KEY, TEST_VECTOR_WRONG_GRANTOR_KEY,
107        },
108    };
109
110    // Recorded with the grantor `test_bitwarden_com_account` and the grantee
111    // `test_bitwarden_com_account_v2`. Never regenerate these to make a test pass: a vector that
112    // stops decrypting is a backward-compatibility break. Ciphers are in the server's
113    // `CipherResponseModel` shape.
114
115    /// Login "legacy" (password "pass"), legacy field-level encryption under the grantor user key.
116    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}"#;
117    /// Login "blob" (password "pass"), blob encryption under the grantor user key.
118    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=\"}"}"#;
119    /// Login "other", blob encryption under a key unrelated to the grantor.
120    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\"}"}"#;
121
122    fn cipher_response(json: &str) -> CipherResponseModel {
123        serde_json::from_str(json).unwrap()
124    }
125
126    /// Creates the grantee client whose API returns the given view response.
127    async fn grantee(key_encrypted: Option<&str>, ciphers: &[&str]) -> Client {
128        let response = EmergencyAccessViewResponseModel {
129            object: None,
130            key_encrypted: key_encrypted.map(str::to_owned),
131            ciphers: Some(ciphers.iter().map(|c| cipher_response(c)).collect()),
132        };
133
134        let api_client = ApiClient::new_mocked(move |mock| {
135            mock.emergency_access_api
136                .expect_view_ciphers()
137                .withf(|id| id.to_string() == TEST_EMERGENCY_ACCESS_ID)
138                .returning(move |_| Ok(response.clone()))
139                .once();
140        });
141
142        Client::init_test_account_with_api_client(test_bitwarden_com_account_v2(), api_client).await
143    }
144
145    async fn view(client: &Client) -> Result<DecryptCipherResult, EmergencyAccessViewError> {
146        client
147            .emergency_access()
148            .view_vault_items(TEST_EMERGENCY_ACCESS_ID.parse().unwrap())
149            .await
150    }
151
152    #[tokio::test]
153    async fn decrypts_legacy_and_blob_vault_items_with_grantor_key() {
154        let client = grantee(
155            Some(TEST_VECTOR_GRANTOR_KEY),
156            &[TEST_VECTOR_LEGACY_CIPHER, TEST_VECTOR_BLOB_CIPHER],
157        )
158        .await;
159
160        // The grantee cannot decrypt with their own user key.
161        let blob = Cipher::try_from(cipher_response(TEST_VECTOR_BLOB_CIPHER)).unwrap();
162        assert!(client.vault().ciphers().decrypt(blob).await.is_err());
163
164        let result = view(&client).await.unwrap();
165
166        let names: Vec<_> = result.successes.iter().map(|c| c.name.as_str()).collect();
167        assert_eq!(names, ["legacy", "blob"]);
168        assert!(result.failures.is_empty());
169        for view in &result.successes {
170            assert_eq!(
171                view.login.as_ref().unwrap().password.as_deref(),
172                Some("pass")
173            );
174        }
175    }
176
177    #[tokio::test]
178    async fn reports_vault_items_under_other_keys_as_failures() {
179        // Blob decryption fails hard, unlike lenient legacy decryption which nulls out
180        // undecryptable fields.
181        let client = grantee(
182            Some(TEST_VECTOR_GRANTOR_KEY),
183            &[TEST_VECTOR_OTHER_KEY_CIPHER, TEST_VECTOR_LEGACY_CIPHER],
184        )
185        .await;
186
187        let result = view(&client).await.unwrap();
188
189        assert_eq!(result.successes.len(), 1);
190        assert_eq!(result.successes[0].name, "legacy");
191        assert_eq!(result.failures.len(), 1);
192    }
193
194    #[tokio::test]
195    async fn fails_when_grantor_key_is_not_for_current_user() {
196        let client = grantee(
197            Some(TEST_VECTOR_WRONG_GRANTOR_KEY),
198            &[TEST_VECTOR_LEGACY_CIPHER],
199        )
200        .await;
201
202        let result = view(&client).await;
203
204        assert!(matches!(result, Err(EmergencyAccessViewError::Crypto(_))));
205    }
206
207    #[tokio::test]
208    async fn fails_when_grantor_key_is_malformed() {
209        let client = grantee(Some("not a key"), &[TEST_VECTOR_LEGACY_CIPHER]).await;
210
211        let result = view(&client).await;
212
213        assert!(matches!(result, Err(EmergencyAccessViewError::Crypto(_))));
214    }
215
216    #[tokio::test]
217    async fn fails_when_grantor_key_is_missing() {
218        let client = grantee(None, &[TEST_VECTOR_LEGACY_CIPHER]).await;
219
220        let result = view(&client).await;
221
222        assert!(matches!(
223            result,
224            Err(EmergencyAccessViewError::MissingField(_))
225        ));
226    }
227}