Skip to content

Commit 8423a4c

Browse files
authored
[PM-22515] Update cipher decrypt list to return successful decryptions and failure metadata (#332)
1 parent 7a1ae93 commit 8423a4c

File tree

5 files changed

+164
-48
lines changed

5 files changed

+164
-48
lines changed

crates/bitwarden-crypto/src/store/mod.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
2525
use std::sync::{Arc, RwLock};
2626

27-
use rayon::prelude::*;
27+
use rayon::{iter::Either, prelude::*};
2828

2929
use crate::{CompositeEncryptable, Decryptable, IdentifyKey, KeyId, KeyIds};
3030

@@ -266,6 +266,48 @@ impl<Ids: KeyIds> KeyStore<Ids> {
266266
res
267267
}
268268

269+
/// Decrypt a list of items using this key store, returning a tuple of successful and failed
270+
/// items.
271+
///
272+
/// # Arguments
273+
/// * `data` - The list of items to decrypt.
274+
///
275+
/// # Returns
276+
/// A tuple containing two vectors: the first vector contains the successfully decrypted items,
277+
/// and the second vector contains the original items that failed to decrypt.
278+
pub fn decrypt_list_with_failures<
279+
'a,
280+
Key: KeyId,
281+
Data: Decryptable<Ids, Key, Output> + IdentifyKey<Key> + Send + Sync + 'a,
282+
Output: Send + Sync,
283+
>(
284+
&self,
285+
data: &'a [Data],
286+
) -> (Vec<Output>, Vec<&'a Data>) {
287+
let results: (Vec<_>, Vec<_>) = data
288+
.par_chunks(batch_chunk_size(data.len()))
289+
.flat_map(|chunk| {
290+
let mut ctx = self.context();
291+
292+
chunk
293+
.iter()
294+
.map(|item| {
295+
let result = item
296+
.decrypt(&mut ctx, item.key_identifier())
297+
.map_err(|_| item);
298+
ctx.clear_local();
299+
result
300+
})
301+
.collect::<Vec<_>>()
302+
})
303+
.partition_map(|result| match result {
304+
Ok(output) => Either::Left(output),
305+
Err(original_item) => Either::Right(original_item),
306+
});
307+
308+
results
309+
}
310+
269311
/// Encrypt a list of items using this key store. The keys returned by
270312
/// `data[i].key_identifier()` must already be present in the store, otherwise this will
271313
/// return an error. This method will try to parallelize the encryption of the items, for

crates/bitwarden-uniffi/src/vault/ciphers.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use bitwarden_core::OrganizationId;
2-
use bitwarden_vault::{Cipher, CipherListView, CipherView, EncryptionContext, Fido2CredentialView};
2+
use bitwarden_vault::{
3+
Cipher, CipherListView, CipherView, DecryptCipherListResult, EncryptionContext,
4+
Fido2CredentialView,
5+
};
36
use uuid::Uuid;
47

58
use crate::{error::Error, Result};
@@ -25,6 +28,12 @@ impl CiphersClient {
2528
Ok(self.0.decrypt_list(ciphers).map_err(Error::Decrypt)?)
2629
}
2730

31+
/// Decrypt cipher list with failures
32+
/// Returns both successfully decrypted ciphers and any that failed to decrypt
33+
pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
34+
self.0.decrypt_list_with_failures(ciphers)
35+
}
36+
2837
pub fn decrypt_fido2_credentials(
2938
&self,
3039
cipher_view: CipherView,

crates/bitwarden-vault/src/cipher/cipher.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,22 @@ pub struct CipherListView {
252252
pub local_data: Option<LocalDataView>,
253253
}
254254

255+
/// Represents the result of decrypting a list of ciphers.
256+
///
257+
/// This struct contains two vectors: `successes` and `failures`.
258+
/// `successes` contains the decrypted `CipherListView` objects,
259+
/// while `failures` contains the original `Cipher` objects that failed to decrypt.
260+
#[derive(Serialize, Deserialize, Debug)]
261+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
262+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
263+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
264+
pub struct DecryptCipherListResult {
265+
/// The decrypted `CipherListView` objects.
266+
pub successes: Vec<CipherListView>,
267+
/// The original `Cipher` objects that failed to decrypt.
268+
pub failures: Vec<Cipher>,
269+
}
270+
255271
impl CipherListView {
256272
pub(crate) fn get_totp_key(
257273
self,

crates/bitwarden-vault/src/cipher/cipher_client.rs

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use bitwarden_crypto::IdentifyKey;
44
use wasm_bindgen::prelude::*;
55

66
use super::EncryptionContext;
7-
use crate::{Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError};
7+
use crate::{
8+
cipher::cipher::DecryptCipherListResult, Cipher, CipherError, CipherListView, CipherView,
9+
DecryptError, EncryptError,
10+
};
811

912
#[allow(missing_docs)]
1013
#[cfg_attr(feature = "wasm", wasm_bindgen)]
@@ -57,6 +60,18 @@ impl CiphersClient {
5760
Ok(cipher_views)
5861
}
5962

63+
/// Decrypt cipher list with failures
64+
/// Returns both successfully decrypted ciphers and any that failed to decrypt
65+
pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
66+
let key_store = self.client.internal.get_key_store();
67+
let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
68+
69+
DecryptCipherListResult {
70+
successes,
71+
failures: failures.into_iter().cloned().collect(),
72+
}
73+
}
74+
6075
#[allow(missing_docs)]
6176
pub fn decrypt_fido2_credentials(
6277
&self,
@@ -98,50 +113,6 @@ mod tests {
98113
use super::*;
99114
use crate::{Attachment, CipherRepromptType, CipherType, Login, VaultClientExt};
100115

101-
#[tokio::test]
102-
async fn test_decrypt_list() {
103-
let client = Client::init_test_account(test_bitwarden_com_account()).await;
104-
105-
let dec = client
106-
.vault()
107-
.ciphers()
108-
.decrypt_list(vec![Cipher {
109-
id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
110-
organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
111-
folder_id: None,
112-
collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
113-
key: None,
114-
name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
115-
notes: None,
116-
r#type: CipherType::Login,
117-
login: Some(Login{
118-
username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
119-
password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
120-
password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
121-
identity: None,
122-
card: None,
123-
secure_note: None,
124-
ssh_key: None,
125-
favorite: false,
126-
reprompt: CipherRepromptType::None,
127-
organization_use_totp: true,
128-
edit: true,
129-
permissions: None,
130-
view_password: true,
131-
local_data: None,
132-
attachments: None,
133-
fields: None,
134-
password_history: None,
135-
creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
136-
deleted_date: None,
137-
revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
138-
}])
139-
140-
.unwrap();
141-
142-
assert_eq!(dec[0].name, "Test item");
143-
}
144-
145116
fn test_cipher() -> Cipher {
146117
Cipher {
147118
id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
@@ -203,6 +174,84 @@ mod tests {
203174
}
204175
}
205176

177+
#[tokio::test]
178+
async fn test_decrypt_list() {
179+
let client = Client::init_test_account(test_bitwarden_com_account()).await;
180+
181+
let dec = client
182+
.vault()
183+
.ciphers()
184+
.decrypt_list(vec![Cipher {
185+
id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
186+
organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
187+
folder_id: None,
188+
collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
189+
key: None,
190+
name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
191+
notes: None,
192+
r#type: CipherType::Login,
193+
login: Some(Login{
194+
username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
195+
password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
196+
password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
197+
identity: None,
198+
card: None,
199+
secure_note: None,
200+
ssh_key: None,
201+
favorite: false,
202+
reprompt: CipherRepromptType::None,
203+
organization_use_totp: true,
204+
edit: true,
205+
permissions: None,
206+
view_password: true,
207+
local_data: None,
208+
attachments: None,
209+
fields: None,
210+
password_history: None,
211+
creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
212+
deleted_date: None,
213+
revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
214+
}])
215+
216+
.unwrap();
217+
218+
assert_eq!(dec[0].name, "Test item");
219+
}
220+
221+
#[tokio::test]
222+
async fn test_decrypt_list_with_failures_all_success() {
223+
let client = Client::init_test_account(test_bitwarden_com_account()).await;
224+
225+
let valid_cipher = test_cipher();
226+
227+
let result = client
228+
.vault()
229+
.ciphers()
230+
.decrypt_list_with_failures(vec![valid_cipher]);
231+
232+
assert_eq!(result.successes.len(), 1);
233+
assert!(result.failures.is_empty());
234+
assert_eq!(result.successes[0].name, "234234");
235+
}
236+
237+
#[tokio::test]
238+
async fn test_decrypt_list_with_failures_mixed_results() {
239+
let client = Client::init_test_account(test_bitwarden_com_account()).await;
240+
let valid_cipher = test_cipher();
241+
let mut invalid_cipher = test_cipher();
242+
// Set an invalid encryptedkey to cause decryption failure
243+
invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
244+
245+
let ciphers = vec![valid_cipher, invalid_cipher.clone()];
246+
247+
let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
248+
249+
assert_eq!(result.successes.len(), 1);
250+
assert_eq!(result.failures.len(), 1);
251+
252+
assert_eq!(result.successes[0].name, "234234");
253+
}
254+
206255
#[tokio::test]
207256
async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
208257
let client = Client::init_test_account(test_bitwarden_com_account()).await;

crates/bitwarden-vault/src/cipher/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use attachment_client::{AttachmentsClient, DecryptFileError, EncryptFileErro
2020
pub use card::{CardBrand, CardListView, CardView};
2121
pub use cipher::{
2222
Cipher, CipherError, CipherListView, CipherListViewType, CipherRepromptType, CipherType,
23-
CipherView, EncryptionContext,
23+
CipherView, DecryptCipherListResult, EncryptionContext,
2424
};
2525
pub use cipher_client::CiphersClient;
2626
pub use field::FieldView;

0 commit comments

Comments
 (0)