Skip to main content

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#[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
35/// Restores a soft-deleted cipher on the server.
36pub 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
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, 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    /// Restores a soft-deleted cipher on the server.
87    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    /// Restores multiple soft-deleted ciphers on the server.
95    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        // Set up test ciphers in the repository.
170        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        // Confirm repository was updated
306        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}