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