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