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
8use crate::{
9 Cipher, CipherId, CipherView, CiphersClient, DecryptCipherListResult, VaultParseError,
10 cipher::cipher::PartialCipher,
11};
12
13#[allow(missing_docs)]
14#[bitwarden_error(flat)]
15#[derive(Debug, Error)]
16pub enum RestoreCipherError {
17 #[error(transparent)]
18 Api(#[from] ApiError),
19 #[error(transparent)]
20 VaultParse(#[from] VaultParseError),
21 #[error(transparent)]
22 Repository(#[from] RepositoryError),
23 #[error(transparent)]
24 Crypto(#[from] CryptoError),
25}
26
27impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherError {
28 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
29 Self::Api(val.into())
30 }
31}
32
33pub async fn restore<R: Repository<Cipher> + ?Sized>(
35 cipher_id: CipherId,
36 api_client: &ApiClient,
37 repository: &R,
38 key_store: &KeyStore<KeyIds>,
39) -> Result<CipherView, RestoreCipherError> {
40 let api = api_client.ciphers_api();
41
42 let cipher: Cipher = api.put_restore(cipher_id.into()).await?.try_into()?;
43 repository
44 .set(cipher_id.to_string(), cipher.clone())
45 .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.to_string(), 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
84impl CiphersClient {
85 pub async fn restore(&self, cipher_id: CipherId) -> Result<CipherView, RestoreCipherError> {
87 let api_client = &self
88 .client
89 .internal
90 .get_api_configurations()
91 .await
92 .api_client;
93 let key_store = self.client.internal.get_key_store();
94
95 restore(cipher_id, api_client, &*self.get_repository()?, key_store).await
96 }
97
98 pub async fn restore_many(
100 &self,
101 cipher_ids: Vec<CipherId>,
102 ) -> Result<DecryptCipherListResult, RestoreCipherError> {
103 let api_client = &self
104 .client
105 .internal
106 .get_api_configurations()
107 .await
108 .api_client;
109 let key_store = self.client.internal.get_key_store();
110 let repository = &*self.get_repository()?;
111
112 restore_many(cipher_ids, api_client, repository, key_store).await
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use bitwarden_api_api::{
119 apis::ApiClient,
120 models::{
121 CipherMiniResponseModel, CipherMiniResponseModelListResponseModel, CipherResponseModel,
122 },
123 };
124 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
125 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
126 use bitwarden_state::repository::Repository;
127 use bitwarden_test::MemoryRepository;
128 use chrono::Utc;
129
130 use super::*;
131 use crate::{Cipher, CipherId, Login};
132
133 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
134 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
135
136 fn generate_test_cipher() -> Cipher {
137 Cipher {
138 id: TEST_CIPHER_ID.parse().ok(),
139 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
140 r#type: crate::CipherType::Login,
141 notes: Default::default(),
142 organization_id: Default::default(),
143 folder_id: Default::default(),
144 favorite: Default::default(),
145 reprompt: Default::default(),
146 fields: Default::default(),
147 collection_ids: Default::default(),
148 key: Default::default(),
149 login: Some(Login{
150 username: None,
151 password: None,
152 password_revision_date: None,
153 uris: None, totp: None,
154 autofill_on_page_load: None,
155 fido2_credentials: None,
156 }),
157 identity: Default::default(),
158 card: Default::default(),
159 secure_note: Default::default(),
160 ssh_key: Default::default(),
161 organization_use_totp: Default::default(),
162 edit: Default::default(),
163 permissions: Default::default(),
164 view_password: Default::default(),
165 local_data: Default::default(),
166 attachments: Default::default(),
167 password_history: Default::default(),
168 creation_date: Default::default(),
169 deleted_date: Default::default(),
170 revision_date: Default::default(),
171 archived_date: Default::default(),
172 data: Default::default(),
173 }
174 }
175
176 #[tokio::test]
177 async fn test_restore() {
178 let mut cipher_1 = generate_test_cipher();
180 cipher_1.deleted_date = Some(Utc::now());
181
182 let api_client = ApiClient::new_mocked(move |mock| {
183 mock.ciphers_api
184 .expect_put_restore()
185 .returning(move |_model| {
186 Ok(CipherResponseModel {
187 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
188 name: Some(cipher_1.name.to_string()),
189 r#type: Some(cipher_1.r#type.into()),
190 creation_date: Some(cipher_1.creation_date.to_string()),
191 revision_date: Some(Utc::now().to_string()),
192 ..Default::default()
193 })
194 });
195 });
196
197 let repository: MemoryRepository<Cipher> = Default::default();
198 let store: KeyStore<KeyIds> = KeyStore::default();
199 #[allow(deprecated)]
200 let _ = store.context_mut().set_symmetric_key(
201 SymmetricKeyId::User,
202 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
203 );
204
205 let mut cipher = generate_test_cipher();
206 cipher.deleted_date = Some(Utc::now());
207
208 repository
209 .set(TEST_CIPHER_ID.to_string(), 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.to_string())
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 cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
243 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
244 let mut cipher_1 = generate_test_cipher();
245 cipher_1.deleted_date = Some(Utc::now());
246 let mut cipher_2 = generate_test_cipher();
247 cipher_2.deleted_date = Some(Utc::now());
248 cipher_2.id = Some(cipher_id_2);
249
250 let api_client = {
251 let cipher_1 = cipher_1.clone();
252 let cipher_2 = cipher_2.clone();
253 ApiClient::new_mocked(move |mock| {
254 mock.ciphers_api.expect_put_restore_many().returning({
255 move |_model| {
256 Ok(CipherMiniResponseModelListResponseModel {
257 object: None,
258 data: Some(vec![
259 CipherMiniResponseModel {
260 id: cipher_1.id.map(|id| id.into()),
261 name: Some(cipher_1.name.to_string()),
262 r#type: Some(cipher_1.r#type.into()),
263 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
264 creation_date: cipher_1.creation_date.to_string().into(),
265 deleted_date: None,
266 revision_date: Some(Utc::now().to_string()),
267 ..Default::default()
268 },
269 CipherMiniResponseModel {
270 id: cipher_2.id.map(|id| id.into()),
271 name: Some(cipher_2.name.to_string()),
272 r#type: Some(cipher_2.r#type.into()),
273 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
274 creation_date: cipher_2.creation_date.to_string().into(),
275 deleted_date: None,
276 revision_date: Some(Utc::now().to_string()),
277 ..Default::default()
278 },
279 ]),
280 continuation_token: None,
281 })
282 }
283 });
284 })
285 };
286
287 let repository: MemoryRepository<Cipher> = Default::default();
288 let store: KeyStore<KeyIds> = KeyStore::default();
289 #[allow(deprecated)]
290 let _ = store.context_mut().set_symmetric_key(
291 SymmetricKeyId::User,
292 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
293 );
294
295 repository
296 .set(cipher_id.to_string(), cipher_1)
297 .await
298 .unwrap();
299 repository
300 .set(TEST_CIPHER_ID_2.to_string(), cipher_2)
301 .await
302 .unwrap();
303
304 let start_time = Utc::now();
305 let ciphers = restore_many(
306 vec![cipher_id, cipher_id_2],
307 &api_client,
308 &repository,
309 &store,
310 )
311 .await
312 .unwrap();
313 let end_time = Utc::now();
314
315 assert_eq!(ciphers.successes.len(), 2,);
316 assert_eq!(ciphers.failures.len(), 0,);
317 assert_eq!(ciphers.successes[0].deleted_date, None,);
318 assert_eq!(ciphers.successes[1].deleted_date, None,);
319
320 let cipher_1 = repository
322 .get(cipher_id.to_string())
323 .await
324 .unwrap()
325 .unwrap();
326 let cipher_2 = repository
327 .get(cipher_id_2.to_string())
328 .await
329 .unwrap()
330 .unwrap();
331 assert!(cipher_1.deleted_date.is_none());
332 assert!(cipher_2.deleted_date.is_none());
333 assert!(cipher_1.revision_date >= start_time && cipher_1.revision_date <= end_time);
334 assert!(cipher_2.revision_date >= start_time && cipher_2.revision_date <= end_time);
335 }
336}