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