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
46        .set(cipher_id.to_string(), cipher.clone())
47        .await?;
48
49    Ok(key_store.decrypt(&cipher)?)
50}
51
52/// Restores multiple soft-deleted ciphers on the server.
53pub 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    /// Restores a soft-deleted cipher on the server.
89    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    /// Restores multiple soft-deleted ciphers on the server.
102    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        // Set up test ciphers in the repository.
182        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        // Confirm repository was updated
324        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}