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
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
33pub 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
54pub 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 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 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}