bitwarden_vault/cipher/cipher_client/
restore.rs

1use 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
33/// Restores a soft-deleted cipher on the server.
34pub 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
50/// Restores multiple soft-deleted ciphers on the server.
51pub 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    /// Restores a soft-deleted cipher on the server.
86    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    /// Restores multiple soft-deleted ciphers on the server.
99    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        // Set up test ciphers in the repository.
179        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        // Confirm repository was updated
321        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}