1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, key_management::KeySlotIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use bitwarden_state::repository::{Repository, RepositoryError};
6use futures::future::OptionFuture;
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::wasm_bindgen;
10
11use crate::{
12 Cipher, CipherId, CipherView, CiphersClient, DecryptCipherListResult, VaultParseError,
13 cipher::cipher::{PartialCipher, StrictDecrypt},
14};
15
16#[allow(missing_docs)]
17#[bitwarden_error(flat)]
18#[derive(Debug, Error)]
19pub enum RestoreCipherError {
20 #[error(transparent)]
21 Api(#[from] ApiError),
22 #[error(transparent)]
23 VaultParse(#[from] VaultParseError),
24 #[error(transparent)]
25 Repository(#[from] RepositoryError),
26 #[error(transparent)]
27 Crypto(#[from] CryptoError),
28}
29
30impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherError {
31 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
32 Self::Api(val.into())
33 }
34}
35
36pub async fn restore<R: Repository<Cipher> + ?Sized>(
38 cipher_id: CipherId,
39 api_client: &ApiClient,
40 repository: &R,
41 key_store: &KeyStore<KeySlotIds>,
42 use_strict_decryption: bool,
43) -> Result<CipherView, RestoreCipherError> {
44 let api = api_client.ciphers_api();
45
46 let existing_cipher = repository.get(cipher_id).await?;
47 let cipher: Cipher = api
48 .put_restore(cipher_id.into())
49 .await?
50 .merge_with_cipher(existing_cipher)?;
51 repository.set(cipher_id, cipher.clone()).await?;
52
53 if use_strict_decryption {
54 Ok(key_store.decrypt(&StrictDecrypt(cipher))?)
55 } else {
56 Ok(key_store.decrypt(&cipher)?)
57 }
58}
59
60pub async fn restore_many<R: Repository<Cipher> + ?Sized>(
62 cipher_ids: Vec<CipherId>,
63 api_client: &ApiClient,
64 repository: &R,
65 key_store: &KeyStore<KeySlotIds>,
66) -> Result<DecryptCipherListResult, RestoreCipherError> {
67 let api = api_client.ciphers_api();
68
69 let response_models: Vec<_> = api
70 .put_restore_many(Some(CipherBulkRestoreRequestModel {
71 ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
72 organization_id: None,
73 }))
74 .await?
75 .data
76 .into_iter()
77 .flatten()
78 .collect();
79
80 let mut ciphers = Vec::with_capacity(response_models.len());
81 for model in response_models {
82 let existing = OptionFuture::from(model.id.map(|id| repository.get(CipherId::new(id))))
83 .await
84 .transpose()?
85 .flatten();
86 ciphers.push(model.merge_with_cipher(existing)?);
87 }
88
89 for cipher in &ciphers {
90 if let Some(id) = cipher.id {
91 repository.set(id, cipher.clone()).await?;
92 }
93 }
94
95 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
96 Ok(DecryptCipherListResult {
97 successes,
98 failures: failures.into_iter().cloned().collect(),
99 })
100}
101
102#[allow(deprecated)]
103#[cfg_attr(feature = "wasm", wasm_bindgen)]
104impl CiphersClient {
105 pub async fn restore(&self, cipher_id: CipherId) -> Result<CipherView, RestoreCipherError> {
107 let api_client = &self.client.internal.get_api_configurations().api_client;
108 let key_store = self.client.internal.get_key_store();
109
110 restore(
111 cipher_id,
112 api_client,
113 &*self.get_repository()?,
114 key_store,
115 self.is_strict_decrypt().await,
116 )
117 .await
118 }
119
120 pub async fn restore_many(
122 &self,
123 cipher_ids: Vec<CipherId>,
124 ) -> Result<DecryptCipherListResult, RestoreCipherError> {
125 let api_client = &self.client.internal.get_api_configurations().api_client;
126 let key_store = self.client.internal.get_key_store();
127 let repository = &*self.get_repository()?;
128
129 restore_many(cipher_ids, api_client, repository, key_store).await
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use bitwarden_api_api::{
136 apis::ApiClient,
137 models::{
138 CipherMiniResponseModel, CipherMiniResponseModelListResponseModel, CipherResponseModel,
139 },
140 };
141 use bitwarden_collections::collection::CollectionId;
142 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
143 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
144 use bitwarden_state::repository::Repository;
145 use bitwarden_test::MemoryRepository;
146 use chrono::Utc;
147
148 use super::*;
149 use crate::{Cipher, CipherId, Login};
150
151 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
152 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
153
154 fn setup_key_store() -> KeyStore<KeySlotIds> {
155 let store: KeyStore<KeySlotIds> = KeyStore::default();
156 #[allow(deprecated)]
157 let _ = store.context_mut().set_symmetric_key(
158 SymmetricKeySlotId::User,
159 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
160 );
161 store
162 }
163
164 fn generate_test_cipher() -> Cipher {
165 Cipher {
166 id: TEST_CIPHER_ID.parse().ok(),
167 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
168 r#type: crate::CipherType::Login,
169 notes: Default::default(),
170 organization_id: Default::default(),
171 folder_id: Default::default(),
172 favorite: Default::default(),
173 reprompt: Default::default(),
174 fields: Default::default(),
175 collection_ids: Default::default(),
176 key: Default::default(),
177 login: Some(Login{
178 username: None,
179 password: None,
180 password_revision_date: None,
181 uris: None, totp: None,
182 autofill_on_page_load: None,
183 fido2_credentials: None,
184 }),
185 identity: Default::default(),
186 card: Default::default(),
187 secure_note: Default::default(),
188 ssh_key: Default::default(),
189 bank_account: Default::default(),
190 drivers_license: Default::default(),
191 passport: Default::default(),
192 organization_use_totp: Default::default(),
193 edit: Default::default(),
194 permissions: Default::default(),
195 view_password: Default::default(),
196 local_data: Default::default(),
197 attachments: Default::default(),
198 password_history: Default::default(),
199 creation_date: Default::default(),
200 deleted_date: Default::default(),
201 revision_date: Default::default(),
202 archived_date: Default::default(),
203 data: Default::default(),
204 }
205 }
206
207 #[tokio::test]
208 async fn test_restore() {
209 let mut cipher_1 = generate_test_cipher();
211 cipher_1.deleted_date = Some(Utc::now());
212
213 let api_client = ApiClient::new_mocked(move |mock| {
214 mock.ciphers_api
215 .expect_put_restore()
216 .returning(move |_model| {
217 Ok(CipherResponseModel {
218 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
219 name: Some(cipher_1.name.to_string()),
220 r#type: Some(cipher_1.r#type.into()),
221 creation_date: Some(cipher_1.creation_date.to_string()),
222 revision_date: Some(Utc::now().to_string()),
223 ..Default::default()
224 })
225 });
226 });
227
228 let repository: MemoryRepository<Cipher> = Default::default();
229 let store: KeyStore<KeySlotIds> = KeyStore::default();
230 #[allow(deprecated)]
231 let _ = store.context_mut().set_symmetric_key(
232 SymmetricKeySlotId::User,
233 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
234 );
235
236 let collection_id: CollectionId = "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
237 let mut cipher = generate_test_cipher();
238 cipher.deleted_date = Some(Utc::now());
239 cipher.collection_ids = vec![collection_id];
240
241 repository
242 .set(TEST_CIPHER_ID.parse().unwrap(), cipher)
243 .await
244 .unwrap();
245
246 let start_time = Utc::now();
247 let updated_cipher = restore(
248 TEST_CIPHER_ID.parse().unwrap(),
249 &api_client,
250 &repository,
251 &store,
252 false,
253 )
254 .await
255 .unwrap();
256
257 let end_time = Utc::now();
258 assert!(updated_cipher.deleted_date.is_none());
259 assert!(
260 updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
261 );
262 assert_eq!(updated_cipher.collection_ids, vec![collection_id]);
265
266 let repo_cipher = repository
267 .get(TEST_CIPHER_ID.parse().unwrap())
268 .await
269 .unwrap()
270 .unwrap();
271 assert!(repo_cipher.deleted_date.is_none());
272 assert!(
273 repo_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
274 );
275 }
276
277 #[tokio::test]
278 async fn test_restore_many() {
279 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
280 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
281 let collection_id: CollectionId = "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
282 let collection_id_2: CollectionId = "b5e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
283 let mut cipher_1 = generate_test_cipher();
284 cipher_1.deleted_date = Some(Utc::now());
285 cipher_1.collection_ids = vec![collection_id];
286 let mut cipher_2 = generate_test_cipher();
287 cipher_2.deleted_date = Some(Utc::now());
288 cipher_2.id = Some(cipher_id_2);
289 cipher_2.collection_ids = vec![collection_id_2];
290
291 let api_client = {
292 let cipher_1 = cipher_1.clone();
293 let cipher_2 = cipher_2.clone();
294 ApiClient::new_mocked(move |mock| {
295 mock.ciphers_api.expect_put_restore_many().returning({
296 move |_model| {
297 Ok(CipherMiniResponseModelListResponseModel {
298 object: None,
299 data: Some(vec![
300 CipherMiniResponseModel {
301 id: cipher_1.id.map(|id| id.into()),
302 name: Some(cipher_1.name.to_string()),
303 r#type: Some(cipher_1.r#type.into()),
304 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
305 creation_date: cipher_1.creation_date.to_string().into(),
306 deleted_date: None,
307 revision_date: Some(Utc::now().to_string()),
308 ..Default::default()
309 },
310 CipherMiniResponseModel {
311 id: cipher_2.id.map(|id| id.into()),
312 name: Some(cipher_2.name.to_string()),
313 r#type: Some(cipher_2.r#type.into()),
314 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
315 creation_date: cipher_2.creation_date.to_string().into(),
316 deleted_date: None,
317 revision_date: Some(Utc::now().to_string()),
318 ..Default::default()
319 },
320 ]),
321 continuation_token: None,
322 })
323 }
324 });
325 })
326 };
327
328 let repository: MemoryRepository<Cipher> = Default::default();
329 let store: KeyStore<KeySlotIds> = KeyStore::default();
330 #[allow(deprecated)]
331 let _ = store.context_mut().set_symmetric_key(
332 SymmetricKeySlotId::User,
333 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
334 );
335
336 repository.set(cipher_id, cipher_1).await.unwrap();
337 repository.set(cipher_id_2, cipher_2).await.unwrap();
338
339 let start_time = Utc::now();
340 let ciphers = restore_many(
341 vec![cipher_id, cipher_id_2],
342 &api_client,
343 &repository,
344 &store,
345 )
346 .await
347 .unwrap();
348 let end_time = Utc::now();
349
350 assert_eq!(ciphers.successes.len(), 2,);
351 assert_eq!(ciphers.failures.len(), 0,);
352 assert_eq!(ciphers.successes[0].deleted_date, None,);
353 assert_eq!(ciphers.successes[1].deleted_date, None,);
354
355 let cipher_1 = repository.get(cipher_id).await.unwrap().unwrap();
357 let cipher_2 = repository.get(cipher_id_2).await.unwrap().unwrap();
358 assert!(cipher_1.deleted_date.is_none());
359 assert!(cipher_2.deleted_date.is_none());
360 assert!(cipher_1.revision_date >= start_time && cipher_1.revision_date <= end_time);
361 assert!(cipher_2.revision_date >= start_time && cipher_2.revision_date <= end_time);
362 }
363
364 #[tokio::test]
365 async fn test_restore_preserves_collection_ids() {
366 let store = setup_key_store();
367 let collection_id: CollectionId = "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
368
369 let mut cipher = generate_test_cipher();
370 cipher.deleted_date = Some(Utc::now());
371 cipher.collection_ids = vec![collection_id];
372
373 let cipher_name = cipher.name.to_string();
374 let cipher_type = cipher.r#type;
375
376 let api_client = ApiClient::new_mocked(move |mock| {
377 mock.ciphers_api.expect_put_restore().returning(move |_| {
378 Ok(CipherResponseModel {
379 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
380 name: Some(cipher_name.clone()),
381 r#type: Some(cipher_type.into()),
382 creation_date: Some("2025-01-01T00:00:00Z".to_string()),
383 revision_date: Some(Utc::now().to_string()),
384 ..Default::default()
385 })
386 });
387 });
388
389 let repository: MemoryRepository<Cipher> = Default::default();
390 repository
391 .set(TEST_CIPHER_ID.parse().unwrap(), cipher)
392 .await
393 .unwrap();
394
395 let result = restore(
396 TEST_CIPHER_ID.parse().unwrap(),
397 &api_client,
398 &repository,
399 &store,
400 false,
401 )
402 .await
403 .unwrap();
404
405 assert_eq!(result.collection_ids, vec![collection_id]);
408 }
409
410 #[tokio::test]
411 async fn test_restore_many_preserves_collection_ids() {
412 let store = setup_key_store();
413 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
414 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
415 let collection_id: CollectionId = "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
416 let collection_id_2: CollectionId = "b5e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
417
418 let mut cipher_1 = generate_test_cipher();
419 cipher_1.deleted_date = Some(Utc::now());
420 cipher_1.collection_ids = vec![collection_id];
421
422 let mut cipher_2 = generate_test_cipher();
423 cipher_2.id = Some(cipher_id_2);
424 cipher_2.deleted_date = Some(Utc::now());
425 cipher_2.collection_ids = vec![collection_id_2];
426
427 let api_client = {
428 let cipher_1 = cipher_1.clone();
429 let cipher_2 = cipher_2.clone();
430 ApiClient::new_mocked(move |mock| {
431 mock.ciphers_api.expect_put_restore_many().returning({
432 move |_| {
433 Ok(CipherMiniResponseModelListResponseModel {
434 object: None,
435 data: Some(vec![
436 CipherMiniResponseModel {
437 id: cipher_1.id.map(|id| id.into()),
438 name: Some(cipher_1.name.to_string()),
439 r#type: Some(cipher_1.r#type.into()),
440 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
441 creation_date: cipher_1.creation_date.to_string().into(),
442 deleted_date: None,
443 revision_date: Some(Utc::now().to_string()),
444 ..Default::default()
445 },
446 CipherMiniResponseModel {
447 id: cipher_2.id.map(|id| id.into()),
448 name: Some(cipher_2.name.to_string()),
449 r#type: Some(cipher_2.r#type.into()),
450 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
451 creation_date: cipher_2.creation_date.to_string().into(),
452 deleted_date: None,
453 revision_date: Some(Utc::now().to_string()),
454 ..Default::default()
455 },
456 ]),
457 continuation_token: None,
458 })
459 }
460 });
461 })
462 };
463
464 let repository: MemoryRepository<Cipher> = Default::default();
465 repository.set(cipher_id, cipher_1).await.unwrap();
466 repository.set(cipher_id_2, cipher_2).await.unwrap();
467
468 let ciphers = restore_many(
469 vec![cipher_id, cipher_id_2],
470 &api_client,
471 &repository,
472 &store,
473 )
474 .await
475 .unwrap();
476
477 assert_eq!(ciphers.successes.len(), 2);
478
479 let result_1 = ciphers
482 .successes
483 .iter()
484 .find(|c| c.id == Some(cipher_id))
485 .unwrap();
486 let result_2 = ciphers
487 .successes
488 .iter()
489 .find(|c| c.id == Some(cipher_id_2))
490 .unwrap();
491 assert_eq!(result_1.collection_ids, vec![collection_id]);
492 assert_eq!(result_2.collection_ids, vec![collection_id_2]);
493 }
494}