Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
restore.rs

1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeySlotIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10    Cipher, CipherId, CipherView, DecryptCipherListResult, VaultParseError,
11    cipher::cipher::{PartialCipher, StrictDecrypt},
12    cipher_client::admin::CipherAdminClient,
13};
14
15#[allow(missing_docs)]
16#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum RestoreCipherAdminError {
19    #[error(transparent)]
20    Api(#[from] ApiError),
21    #[error(transparent)]
22    VaultParse(#[from] VaultParseError),
23    #[error(transparent)]
24    Crypto(#[from] CryptoError),
25}
26
27impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherAdminError {
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, using the admin endpoint.
34pub async fn restore_as_admin(
35    cipher_id: CipherId,
36    api_client: &ApiClient,
37    key_store: &KeyStore<KeySlotIds>,
38    use_strict_decryption: bool,
39) -> Result<CipherView, RestoreCipherAdminError> {
40    let api = api_client.ciphers_api();
41
42    let cipher: Cipher = api
43        .put_restore_admin(cipher_id.into())
44        .await?
45        .merge_with_cipher(None)?;
46
47    Ok(if use_strict_decryption {
48        key_store.decrypt(&StrictDecrypt(cipher))?
49    } else {
50        key_store.decrypt(&cipher)?
51    })
52}
53
54/// Restores multiple soft-deleted ciphers on the server.
55pub async fn restore_many_as_admin(
56    cipher_ids: Vec<CipherId>,
57    org_id: OrganizationId,
58    api_client: &ApiClient,
59    key_store: &KeyStore<KeySlotIds>,
60    use_strict_decryption: bool,
61) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
62    let api = api_client.ciphers_api();
63
64    let ciphers: Vec<Cipher> = api
65        .put_restore_many_admin(Some(CipherBulkRestoreRequestModel {
66            ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
67            organization_id: Some(org_id.into()),
68        }))
69        .await?
70        .data
71        .into_iter()
72        .flatten()
73        .map(|c| c.merge_with_cipher(None))
74        .collect::<Result<Vec<_>, _>>()?;
75
76    Ok(if use_strict_decryption {
77        let wrapped: Vec<StrictDecrypt<Cipher>> = ciphers.into_iter().map(StrictDecrypt).collect();
78        let (successes, failures) = key_store.decrypt_list_with_failures(&wrapped);
79        DecryptCipherListResult {
80            successes,
81            failures: failures.into_iter().map(|f| f.0.clone()).collect(),
82        }
83    } else {
84        let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
85        DecryptCipherListResult {
86            successes,
87            failures: failures.into_iter().cloned().collect(),
88        }
89    })
90}
91
92#[cfg_attr(feature = "wasm", wasm_bindgen)]
93impl CipherAdminClient {
94    /// Restores a soft-deleted cipher on the server, using the admin endpoint.
95    pub async fn restore(
96        &self,
97        cipher_id: CipherId,
98    ) -> Result<CipherView, RestoreCipherAdminError> {
99        let api_client = &self.api_configurations.api_client;
100        let key_store = &self.key_store;
101
102        restore_as_admin(
103            cipher_id,
104            api_client,
105            key_store,
106            self.is_strict_decrypt().await,
107        )
108        .await
109    }
110    /// Restores multiple soft-deleted ciphers on the server.
111    pub async fn restore_many(
112        &self,
113        cipher_ids: Vec<CipherId>,
114        org_id: OrganizationId,
115    ) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
116        let api_client = &self.api_configurations.api_client;
117        let key_store = &self.key_store;
118
119        restore_many_as_admin(
120            cipher_ids,
121            org_id,
122            api_client,
123            key_store,
124            self.is_strict_decrypt().await,
125        )
126        .await
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use bitwarden_api_api::{
133        apis::ApiClient,
134        models::{CipherMiniResponseModel, CipherMiniResponseModelListResponseModel},
135    };
136    use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
137    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, SymmetricKeyAlgorithm};
138    use chrono::Utc;
139
140    use super::*;
141    use crate::{Cipher, CipherId, Login};
142
143    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
144    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
145    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
146
147    fn generate_test_cipher() -> Cipher {
148        Cipher {
149            id: TEST_CIPHER_ID.parse().ok(),
150            name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap()),
151            r#type: crate::CipherType::Login,
152            notes: Default::default(),
153            organization_id: Default::default(),
154            folder_id: Default::default(),
155            favorite: Default::default(),
156            reprompt: Default::default(),
157            fields: Default::default(),
158            collection_ids: Default::default(),
159            key: Default::default(),
160            login: Some(Login{
161                username: None,
162                password: None,
163                password_revision_date: None,
164                uris: None, totp: None,
165                autofill_on_page_load: None,
166                fido2_credentials: None,
167            }),
168            identity: Default::default(),
169            card: Default::default(),
170            secure_note: Default::default(),
171            ssh_key: Default::default(),
172            bank_account: Default::default(),
173            drivers_license: Default::default(),
174            passport: Default::default(),
175            organization_use_totp: Default::default(),
176            edit: Default::default(),
177            permissions: Default::default(),
178            view_password: Default::default(),
179            local_data: Default::default(),
180            attachments: Default::default(),
181            password_history: Default::default(),
182            creation_date: Default::default(),
183            deleted_date: Default::default(),
184            revision_date: Default::default(),
185            archived_date: Default::default(),
186            data: Default::default(),
187        }
188    }
189
190    #[tokio::test]
191    async fn test_restore_as_admin() {
192        let mut cipher = generate_test_cipher();
193        cipher.deleted_date = Some(Utc::now());
194
195        let api_client = {
196            let cipher = cipher.clone();
197            ApiClient::new_mocked(move |mock| {
198                mock.ciphers_api
199                    .expect_put_restore_admin()
200                    .returning(move |_model| {
201                        Ok(CipherMiniResponseModel {
202                            id: Some(TEST_CIPHER_ID.try_into().unwrap()),
203                            name: cipher.name.as_ref().map(ToString::to_string),
204                            r#type: Some(cipher.r#type.into()),
205                            creation_date: Some(cipher.creation_date.to_string()),
206                            revision_date: Some(Utc::now().to_rfc3339()),
207                            login: cipher.login.clone().map(|l| Box::new(l.into())),
208                            ..Default::default()
209                        })
210                    });
211            })
212        };
213
214        let store: KeyStore<KeySlotIds> = KeyStore::default();
215        #[allow(deprecated)]
216        let _ = store.context_mut().set_symmetric_key(
217            SymmetricKeySlotId::User,
218            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
219        );
220        let start_time = Utc::now();
221        let updated_cipher =
222            restore_as_admin(TEST_CIPHER_ID.parse().unwrap(), &api_client, &store, false)
223                .await
224                .unwrap();
225        let end_time = Utc::now();
226
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
233    #[tokio::test]
234    async fn test_restore_many_as_admin() {
235        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
236        let mut cipher_1 = generate_test_cipher();
237        cipher_1.deleted_date = Some(Utc::now());
238        let mut cipher_2 = generate_test_cipher();
239        cipher_2.deleted_date = Some(Utc::now());
240        cipher_2.id = Some(cipher_id_2);
241
242        let api_client = ApiClient::new_mocked(move |mock| {
243            mock.ciphers_api
244                .expect_put_restore_many_admin()
245                .returning(move |_model| {
246                    Ok(CipherMiniResponseModelListResponseModel {
247                        object: None,
248                        data: Some(vec![
249                            CipherMiniResponseModel {
250                                id: cipher_1.id.map(|id| id.into()),
251                                name: cipher_1.name.as_ref().map(ToString::to_string),
252                                r#type: Some(cipher_1.r#type.into()),
253                                login: cipher_1.login.clone().map(|l| Box::new(l.into())),
254                                creation_date: cipher_1.creation_date.to_string().into(),
255                                deleted_date: None,
256                                revision_date: Some(Utc::now().to_rfc3339()),
257                                ..Default::default()
258                            },
259                            CipherMiniResponseModel {
260                                id: cipher_2.id.map(|id| id.into()),
261                                name: cipher_2.name.as_ref().map(ToString::to_string),
262                                r#type: Some(cipher_2.r#type.into()),
263                                login: cipher_2.login.clone().map(|l| Box::new(l.into())),
264                                creation_date: cipher_2.creation_date.to_string().into(),
265                                deleted_date: None,
266                                revision_date: Some(Utc::now().to_rfc3339()),
267                                ..Default::default()
268                            },
269                        ]),
270                        continuation_token: None,
271                    })
272                });
273        });
274        let store: KeyStore<KeySlotIds> = KeyStore::default();
275        #[allow(deprecated)]
276        let _ = store.context_mut().set_symmetric_key(
277            SymmetricKeySlotId::User,
278            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
279        );
280
281        let start_time = Utc::now();
282        let ciphers = restore_many_as_admin(
283            vec![
284                TEST_CIPHER_ID.parse().unwrap(),
285                TEST_CIPHER_ID_2.parse().unwrap(),
286            ],
287            TEST_ORG_ID.parse().unwrap(),
288            &api_client,
289            &store,
290            false,
291        )
292        .await
293        .unwrap();
294        let end_time = Utc::now();
295
296        assert_eq!(ciphers.successes.len(), 2,);
297        assert_eq!(ciphers.failures.len(), 0,);
298        assert_eq!(
299            ciphers.successes[0].id,
300            Some(TEST_CIPHER_ID.parse().unwrap()),
301        );
302        assert_eq!(
303            ciphers.successes[1].id,
304            Some(TEST_CIPHER_ID_2.parse().unwrap()),
305        );
306        assert_eq!(ciphers.successes[0].deleted_date, None,);
307        assert_eq!(ciphers.successes[1].deleted_date, None,);
308
309        assert!(
310            ciphers.successes[0].revision_date >= start_time
311                && ciphers.successes[0].revision_date <= end_time
312        );
313        assert!(
314            ciphers.successes[1].revision_date >= start_time
315                && ciphers.successes[1].revision_date <= end_time
316        );
317    }
318}