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