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