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