bitwarden_vault/cipher/cipher_client/
restore.rs1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, key_management::KeyIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use bitwarden_state::repository::{Repository, RepositoryError};
6use thiserror::Error;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::prelude::wasm_bindgen;
9
10use crate::{
11 Cipher, CipherId, CipherView, CiphersClient, DecryptCipherListResult, VaultParseError,
12 cipher::cipher::PartialCipher,
13};
14
15#[allow(missing_docs)]
16#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum RestoreCipherError {
19 #[error(transparent)]
20 Api(#[from] ApiError),
21 #[error(transparent)]
22 VaultParse(#[from] VaultParseError),
23 #[error(transparent)]
24 Repository(#[from] RepositoryError),
25 #[error(transparent)]
26 Crypto(#[from] CryptoError),
27}
28
29impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherError {
30 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
31 Self::Api(val.into())
32 }
33}
34
35pub async fn restore<R: Repository<Cipher> + ?Sized>(
37 cipher_id: CipherId,
38 api_client: &ApiClient,
39 repository: &R,
40 key_store: &KeyStore<KeyIds>,
41) -> Result<CipherView, RestoreCipherError> {
42 let api = api_client.ciphers_api();
43
44 let cipher: Cipher = api.put_restore(cipher_id.into()).await?.try_into()?;
45 repository.set(cipher_id, cipher.clone()).await?;
46
47 Ok(key_store.decrypt(&cipher)?)
48}
49
50pub async fn restore_many<R: Repository<Cipher> + ?Sized>(
52 cipher_ids: Vec<CipherId>,
53 api_client: &ApiClient,
54 repository: &R,
55 key_store: &KeyStore<KeyIds>,
56) -> Result<DecryptCipherListResult, RestoreCipherError> {
57 let api = api_client.ciphers_api();
58
59 let ciphers: Vec<Cipher> = api
60 .put_restore_many(Some(CipherBulkRestoreRequestModel {
61 ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
62 organization_id: None,
63 }))
64 .await?
65 .data
66 .into_iter()
67 .flatten()
68 .map(|c| c.merge_with_cipher(None))
69 .collect::<Result<Vec<Cipher>, _>>()?;
70
71 for cipher in &ciphers {
72 if let Some(id) = cipher.id {
73 repository.set(id, cipher.clone()).await?;
74 }
75 }
76
77 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
78 Ok(DecryptCipherListResult {
79 successes,
80 failures: failures.into_iter().cloned().collect(),
81 })
82}
83
84#[cfg_attr(feature = "wasm", wasm_bindgen)]
85impl CiphersClient {
86 pub async fn restore(&self, cipher_id: CipherId) -> Result<CipherView, RestoreCipherError> {
88 let api_client = &self.client.internal.get_api_configurations().api_client;
89 let key_store = self.client.internal.get_key_store();
90
91 restore(cipher_id, api_client, &*self.get_repository()?, key_store).await
92 }
93
94 pub async fn restore_many(
96 &self,
97 cipher_ids: Vec<CipherId>,
98 ) -> Result<DecryptCipherListResult, RestoreCipherError> {
99 let api_client = &self.client.internal.get_api_configurations().api_client;
100 let key_store = self.client.internal.get_key_store();
101 let repository = &*self.get_repository()?;
102
103 restore_many(cipher_ids, api_client, repository, key_store).await
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use bitwarden_api_api::{
110 apis::ApiClient,
111 models::{
112 CipherMiniResponseModel, CipherMiniResponseModelListResponseModel, CipherResponseModel,
113 },
114 };
115 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
116 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
117 use bitwarden_state::repository::Repository;
118 use bitwarden_test::MemoryRepository;
119 use chrono::Utc;
120
121 use super::*;
122 use crate::{Cipher, CipherId, Login};
123
124 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
125 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
126
127 fn generate_test_cipher() -> Cipher {
128 Cipher {
129 id: TEST_CIPHER_ID.parse().ok(),
130 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
131 r#type: crate::CipherType::Login,
132 notes: Default::default(),
133 organization_id: Default::default(),
134 folder_id: Default::default(),
135 favorite: Default::default(),
136 reprompt: Default::default(),
137 fields: Default::default(),
138 collection_ids: Default::default(),
139 key: Default::default(),
140 login: Some(Login{
141 username: None,
142 password: None,
143 password_revision_date: None,
144 uris: None, totp: None,
145 autofill_on_page_load: None,
146 fido2_credentials: None,
147 }),
148 identity: Default::default(),
149 card: Default::default(),
150 secure_note: Default::default(),
151 ssh_key: Default::default(),
152 organization_use_totp: Default::default(),
153 edit: Default::default(),
154 permissions: Default::default(),
155 view_password: Default::default(),
156 local_data: Default::default(),
157 attachments: Default::default(),
158 password_history: Default::default(),
159 creation_date: Default::default(),
160 deleted_date: Default::default(),
161 revision_date: Default::default(),
162 archived_date: Default::default(),
163 data: Default::default(),
164 }
165 }
166
167 #[tokio::test]
168 async fn test_restore() {
169 let mut cipher_1 = generate_test_cipher();
171 cipher_1.deleted_date = Some(Utc::now());
172
173 let api_client = ApiClient::new_mocked(move |mock| {
174 mock.ciphers_api
175 .expect_put_restore()
176 .returning(move |_model| {
177 Ok(CipherResponseModel {
178 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
179 name: Some(cipher_1.name.to_string()),
180 r#type: Some(cipher_1.r#type.into()),
181 creation_date: Some(cipher_1.creation_date.to_string()),
182 revision_date: Some(Utc::now().to_string()),
183 ..Default::default()
184 })
185 });
186 });
187
188 let repository: MemoryRepository<Cipher> = Default::default();
189 let store: KeyStore<KeyIds> = KeyStore::default();
190 #[allow(deprecated)]
191 let _ = store.context_mut().set_symmetric_key(
192 SymmetricKeyId::User,
193 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
194 );
195
196 let mut cipher = generate_test_cipher();
197 cipher.deleted_date = Some(Utc::now());
198
199 repository
200 .set(TEST_CIPHER_ID.parse().unwrap(), cipher)
201 .await
202 .unwrap();
203
204 let start_time = Utc::now();
205 let updated_cipher = restore(
206 TEST_CIPHER_ID.parse().unwrap(),
207 &api_client,
208 &repository,
209 &store,
210 )
211 .await
212 .unwrap();
213
214 let end_time = Utc::now();
215 assert!(updated_cipher.deleted_date.is_none());
216 assert!(
217 updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
218 );
219
220 let repo_cipher = repository
221 .get(TEST_CIPHER_ID.parse().unwrap())
222 .await
223 .unwrap()
224 .unwrap();
225 assert!(repo_cipher.deleted_date.is_none());
226 assert!(
227 repo_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
228 );
229 }
230
231 #[tokio::test]
232 async fn test_restore_many() {
233 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
234 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
235 let mut cipher_1 = generate_test_cipher();
236 cipher_1.deleted_date = Some(Utc::now());
237 let mut cipher_2 = generate_test_cipher();
238 cipher_2.deleted_date = Some(Utc::now());
239 cipher_2.id = Some(cipher_id_2);
240
241 let api_client = {
242 let cipher_1 = cipher_1.clone();
243 let cipher_2 = cipher_2.clone();
244 ApiClient::new_mocked(move |mock| {
245 mock.ciphers_api.expect_put_restore_many().returning({
246 move |_model| {
247 Ok(CipherMiniResponseModelListResponseModel {
248 object: None,
249 data: Some(vec![
250 CipherMiniResponseModel {
251 id: cipher_1.id.map(|id| id.into()),
252 name: Some(cipher_1.name.to_string()),
253 r#type: Some(cipher_1.r#type.into()),
254 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
255 creation_date: cipher_1.creation_date.to_string().into(),
256 deleted_date: None,
257 revision_date: Some(Utc::now().to_string()),
258 ..Default::default()
259 },
260 CipherMiniResponseModel {
261 id: cipher_2.id.map(|id| id.into()),
262 name: Some(cipher_2.name.to_string()),
263 r#type: Some(cipher_2.r#type.into()),
264 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
265 creation_date: cipher_2.creation_date.to_string().into(),
266 deleted_date: None,
267 revision_date: Some(Utc::now().to_string()),
268 ..Default::default()
269 },
270 ]),
271 continuation_token: None,
272 })
273 }
274 });
275 })
276 };
277
278 let repository: MemoryRepository<Cipher> = Default::default();
279 let store: KeyStore<KeyIds> = KeyStore::default();
280 #[allow(deprecated)]
281 let _ = store.context_mut().set_symmetric_key(
282 SymmetricKeyId::User,
283 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
284 );
285
286 repository.set(cipher_id, cipher_1).await.unwrap();
287 repository.set(cipher_id_2, cipher_2).await.unwrap();
288
289 let start_time = Utc::now();
290 let ciphers = restore_many(
291 vec![cipher_id, cipher_id_2],
292 &api_client,
293 &repository,
294 &store,
295 )
296 .await
297 .unwrap();
298 let end_time = Utc::now();
299
300 assert_eq!(ciphers.successes.len(), 2,);
301 assert_eq!(ciphers.failures.len(), 0,);
302 assert_eq!(ciphers.successes[0].deleted_date, None,);
303 assert_eq!(ciphers.successes[1].deleted_date, None,);
304
305 let cipher_1 = repository.get(cipher_id).await.unwrap().unwrap();
307 let cipher_2 = repository.get(cipher_id_2).await.unwrap().unwrap();
308 assert!(cipher_1.deleted_date.is_none());
309 assert!(cipher_2.deleted_date.is_none());
310 assert!(cipher_1.revision_date >= start_time && cipher_1.revision_date <= end_time);
311 assert!(cipher_2.revision_date >= start_time && cipher_2.revision_date <= end_time);
312 }
313}