bitwarden_emergency_access/
view_vault_items.rs1use 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#[bitwarden_error(flat)]
14#[derive(Debug, Error)]
15pub enum EmergencyAccessViewError {
16 #[error(transparent)]
18 Api(#[from] ApiError),
19 #[error(transparent)]
21 MissingField(#[from] MissingFieldError),
22 #[error(transparent)]
25 Crypto(#[from] CryptoError),
26 #[error(transparent)]
28 VaultParse(#[from] VaultParseError),
29}
30
31struct EmergencyAccessViewData {
33 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 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 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 const TEST_VECTOR_GRANTOR_KEY: &str = "4.bbiAjKYUjktIP1PggRJ+ha+O8M0LxWCNkv2bir5QHIYQnAwtx9X3Cta4j0JFnDS2Zy/UFAzRMCpdLR1DgupZ31lZvAhThC86hDvMMa0h84d7o41Rx9tCvqW1FcziuAA0UEr+Gfkc0+qX5yPFEn2eJ2U+ft9J7rSTTDeha1QIMhYnTf2sD2O0nSWSSPoFNLLg2WKoe9uXC5w+MemcK6x6ogG2T77fDr5trXnK7SoWamAWjhK65bYxYrSIpJOtdCR0O9ZIqiITg0lS0I6ACbiFBpHyo+wb+ZUCieUWrNaBjNHHES2XqEWSVVgz1XL2Q0Vh9XNwwPZLbIWZDxbz8FWqeQ==";
114 const TEST_VECTOR_WRONG_GRANTOR_KEY: &str = "4.PBhgsvKoOu8weZWhRAjPoSrucZneEh0Nc+R3xATSuJQk0GyS+xAagJX3Kh7EuPM2pU2OjGbGJeutDpNbp1EKWyYnPzbgXkMi8VU7dnbT8FmidRr/BcrNdcaNzyzJ06GPnL+wPH047iEhzP6DK7prjd3ufEtRp+x9ZCHPnN/vqzoWJD8AZfbC1c8jQFJSaTV/DfkKrF5rfTH3qoQYUNdfL5HOsbTHM8HTMsR18qtj4w638le1ejsjFFgAihcCdlLRsu6okGWCV6LZK3LyjfzXr/WliJUPV3/lySCHEyZoefK52v3TJ8Xy22Tqo+Y86IA/N/UA58c6tV/Iu1Gv3KQsPA==";
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}"#;
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=\"}"}"#;
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\"}"}"#;
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 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 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 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}