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
8use crate::{EmergencyAccessClient, EmergencyAccessId};
9
10#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum EmergencyAccessViewError {
14 #[error(transparent)]
16 Api(#[from] ApiError),
17 #[error(transparent)]
19 MissingField(#[from] MissingFieldError),
20 #[error(transparent)]
23 Crypto(#[from] CryptoError),
24 #[error(transparent)]
26 VaultParse(#[from] VaultParseError),
27}
28
29struct EmergencyAccessViewData {
31 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 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 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 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 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 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 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 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 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}