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
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
25pub 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
46pub 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 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 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}