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, PrimitiveEncryptable, 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 setup_key_store() -> KeyStore<KeyIds> {
128 let store: KeyStore<KeyIds> = KeyStore::default();
129 #[allow(deprecated)]
130 let _ = store.context_mut().set_symmetric_key(
131 SymmetricKeyId::User,
132 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
133 );
134 store
135 }
136
137 fn generate_test_cipher(store: &KeyStore<KeyIds>) -> Cipher {
138 let mut ctx = store.context();
139 Cipher {
140 id: TEST_CIPHER_ID.parse().ok(),
141 name: "Test cipher"
142 .encrypt(&mut ctx, SymmetricKeyId::User)
143 .unwrap(),
144 r#type: crate::CipherType::Login,
145 notes: Default::default(),
146 organization_id: Default::default(),
147 folder_id: Default::default(),
148 favorite: Default::default(),
149 reprompt: Default::default(),
150 fields: Default::default(),
151 collection_ids: Default::default(),
152 key: Default::default(),
153 login: Some(Login {
154 username: None,
155 password: None,
156 password_revision_date: None,
157 uris: None,
158 totp: None,
159 autofill_on_page_load: None,
160 fido2_credentials: None,
161 }),
162 identity: Default::default(),
163 card: Default::default(),
164 secure_note: Default::default(),
165 ssh_key: Default::default(),
166 organization_use_totp: Default::default(),
167 edit: Default::default(),
168 permissions: Default::default(),
169 view_password: Default::default(),
170 local_data: Default::default(),
171 attachments: Default::default(),
172 password_history: Default::default(),
173 creation_date: Default::default(),
174 deleted_date: Default::default(),
175 revision_date: Default::default(),
176 archived_date: Default::default(),
177 data: Default::default(),
178 }
179 }
180
181 #[tokio::test]
182 async fn test_restore() {
183 let store = setup_key_store();
184 let mut cipher_1 = generate_test_cipher(&store);
186 cipher_1.deleted_date = Some(Utc::now());
187
188 let api_client = ApiClient::new_mocked(move |mock| {
189 mock.ciphers_api
190 .expect_put_restore()
191 .returning(move |_model| {
192 Ok(CipherResponseModel {
193 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
194 name: Some(cipher_1.name.to_string()),
195 r#type: Some(cipher_1.r#type.into()),
196 creation_date: Some(cipher_1.creation_date.to_string()),
197 revision_date: Some(Utc::now().to_string()),
198 ..Default::default()
199 })
200 });
201 });
202
203 let repository: MemoryRepository<Cipher> = Default::default();
204
205 let mut cipher = generate_test_cipher(&store);
206 cipher.deleted_date = Some(Utc::now());
207
208 repository
209 .set(TEST_CIPHER_ID.parse().unwrap(), cipher)
210 .await
211 .unwrap();
212
213 let start_time = Utc::now();
214 let updated_cipher = restore(
215 TEST_CIPHER_ID.parse().unwrap(),
216 &api_client,
217 &repository,
218 &store,
219 )
220 .await
221 .unwrap();
222
223 let end_time = Utc::now();
224 assert!(updated_cipher.deleted_date.is_none());
225 assert!(
226 updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
227 );
228
229 let repo_cipher = repository
230 .get(TEST_CIPHER_ID.parse().unwrap())
231 .await
232 .unwrap()
233 .unwrap();
234 assert!(repo_cipher.deleted_date.is_none());
235 assert!(
236 repo_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
237 );
238 }
239
240 #[tokio::test]
241 async fn test_restore_many() {
242 let store = setup_key_store();
243 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
244 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
245 let mut cipher_1 = generate_test_cipher(&store);
246 cipher_1.deleted_date = Some(Utc::now());
247 let mut cipher_2 = generate_test_cipher(&store);
248 cipher_2.deleted_date = Some(Utc::now());
249 cipher_2.id = Some(cipher_id_2);
250
251 let api_client = {
252 let cipher_1 = cipher_1.clone();
253 let cipher_2 = cipher_2.clone();
254 ApiClient::new_mocked(move |mock| {
255 mock.ciphers_api.expect_put_restore_many().returning({
256 move |_model| {
257 Ok(CipherMiniResponseModelListResponseModel {
258 object: None,
259 data: Some(vec![
260 CipherMiniResponseModel {
261 id: cipher_1.id.map(|id| id.into()),
262 name: Some(cipher_1.name.to_string()),
263 r#type: Some(cipher_1.r#type.into()),
264 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
265 creation_date: cipher_1.creation_date.to_string().into(),
266 deleted_date: None,
267 revision_date: Some(Utc::now().to_string()),
268 ..Default::default()
269 },
270 CipherMiniResponseModel {
271 id: cipher_2.id.map(|id| id.into()),
272 name: Some(cipher_2.name.to_string()),
273 r#type: Some(cipher_2.r#type.into()),
274 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
275 creation_date: cipher_2.creation_date.to_string().into(),
276 deleted_date: None,
277 revision_date: Some(Utc::now().to_string()),
278 ..Default::default()
279 },
280 ]),
281 continuation_token: None,
282 })
283 }
284 });
285 })
286 };
287
288 let repository: MemoryRepository<Cipher> = Default::default();
289
290 repository.set(cipher_id, cipher_1).await.unwrap();
291 repository.set(cipher_id_2, cipher_2).await.unwrap();
292
293 let start_time = Utc::now();
294 let ciphers = restore_many(
295 vec![cipher_id, cipher_id_2],
296 &api_client,
297 &repository,
298 &store,
299 )
300 .await
301 .unwrap();
302 let end_time = Utc::now();
303
304 assert_eq!(ciphers.successes.len(), 2,);
305 assert_eq!(ciphers.failures.len(), 0,);
306 assert_eq!(ciphers.successes[0].deleted_date, None,);
307 assert_eq!(ciphers.successes[1].deleted_date, None,);
308
309 let cipher_1 = repository.get(cipher_id).await.unwrap().unwrap();
311 let cipher_2 = repository.get(cipher_id_2).await.unwrap().unwrap();
312 assert!(cipher_1.deleted_date.is_none());
313 assert!(cipher_2.deleted_date.is_none());
314 assert!(cipher_1.revision_date >= start_time && cipher_1.revision_date <= end_time);
315 assert!(cipher_2.revision_date >= start_time && cipher_2.revision_date <= end_time);
316 }
317}