1use std::sync::Arc;
2
3use bitwarden_core::{Client, OrganizationId};
4use bitwarden_crypto::IdentifyKey;
5#[cfg(feature = "wasm")]
6use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey};
7#[cfg(feature = "wasm")]
8use bitwarden_encoding::B64;
9use bitwarden_state::repository::{Repository, RepositoryError};
10#[cfg(feature = "wasm")]
11use wasm_bindgen::prelude::*;
12
13use super::EncryptionContext;
14#[cfg(feature = "wasm")]
15use crate::Fido2CredentialFullView;
16use crate::{
17 Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError,
18 cipher::cipher::DecryptCipherListResult, cipher_client::admin::CipherAdminClient,
19};
20
21mod admin;
22mod create;
23mod delete;
24mod edit;
25mod get;
26mod restore;
27mod share_cipher;
28
29#[allow(missing_docs)]
30#[cfg_attr(feature = "wasm", wasm_bindgen)]
31pub struct CiphersClient {
32 pub(crate) client: Client,
33}
34
35#[cfg_attr(feature = "wasm", wasm_bindgen)]
36impl CiphersClient {
37 #[allow(missing_docs)]
38 pub fn encrypt(&self, mut cipher_view: CipherView) -> Result<EncryptionContext, EncryptError> {
39 let user_id = self
40 .client
41 .internal
42 .get_user_id()
43 .ok_or(EncryptError::MissingUserId)?;
44 let key_store = self.client.internal.get_key_store();
45
46 if cipher_view.key.is_none()
49 && self
50 .client
51 .internal
52 .get_flags()
53 .enable_cipher_key_encryption
54 {
55 let key = cipher_view.key_identifier();
56 cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
57 }
58
59 let cipher = key_store.encrypt(cipher_view)?;
60 Ok(EncryptionContext {
61 cipher,
62 encrypted_for: user_id,
63 })
64 }
65
66 #[cfg(feature = "wasm")]
77 pub fn encrypt_cipher_for_rotation(
78 &self,
79 mut cipher_view: CipherView,
80 new_key: B64,
81 ) -> Result<EncryptionContext, CipherError> {
82 let new_key = SymmetricCryptoKey::try_from(new_key)?;
83
84 let user_id = self
85 .client
86 .internal
87 .get_user_id()
88 .ok_or(EncryptError::MissingUserId)?;
89 let key_store = self.client.internal.get_key_store();
90 let mut ctx = key_store.context();
91
92 let new_key_id = ctx.add_local_symmetric_key(new_key);
94
95 if cipher_view.key.is_none()
96 && self
97 .client
98 .internal
99 .get_flags()
100 .enable_cipher_key_encryption
101 {
102 cipher_view.generate_cipher_key(&mut ctx, new_key_id)?;
103 } else {
104 cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?;
105 }
106
107 let cipher = cipher_view.encrypt_composite(&mut ctx, new_key_id)?;
108
109 Ok(EncryptionContext {
110 cipher,
111 encrypted_for: user_id,
112 })
113 }
114
115 #[allow(missing_docs)]
116 pub fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
117 let key_store = self.client.internal.get_key_store();
118 let cipher_view = key_store.decrypt(&cipher)?;
119 Ok(cipher_view)
120 }
121
122 #[allow(missing_docs)]
123 pub fn decrypt_list(&self, ciphers: Vec<Cipher>) -> Result<Vec<CipherListView>, DecryptError> {
124 let key_store = self.client.internal.get_key_store();
125 let cipher_views = key_store.decrypt_list(&ciphers)?;
126 Ok(cipher_views)
127 }
128
129 pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
132 let key_store = self.client.internal.get_key_store();
133 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
134
135 DecryptCipherListResult {
136 successes,
137 failures: failures.into_iter().cloned().collect(),
138 }
139 }
140
141 #[allow(missing_docs)]
142 pub fn decrypt_fido2_credentials(
143 &self,
144 cipher_view: CipherView,
145 ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
146 let key_store = self.client.internal.get_key_store();
147 let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
148 Ok(credentials)
149 }
150
151 #[cfg(feature = "wasm")]
157 pub fn set_fido2_credentials(
158 &self,
159 mut cipher_view: CipherView,
160 fido2_credentials: Vec<Fido2CredentialFullView>,
161 ) -> Result<CipherView, CipherError> {
162 let key_store = self.client.internal.get_key_store();
163
164 cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?;
165
166 Ok(cipher_view)
167 }
168
169 #[allow(missing_docs)]
170 pub fn move_to_organization(
171 &self,
172 mut cipher_view: CipherView,
173 organization_id: OrganizationId,
174 ) -> Result<CipherView, CipherError> {
175 let key_store = self.client.internal.get_key_store();
176 cipher_view.move_to_organization(&mut key_store.context(), organization_id)?;
177 Ok(cipher_view)
178 }
179
180 #[cfg(feature = "wasm")]
181 #[allow(missing_docs)]
182 pub fn decrypt_fido2_private_key(
183 &self,
184 cipher_view: CipherView,
185 ) -> Result<String, CipherError> {
186 let key_store = self.client.internal.get_key_store();
187 let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
188 Ok(decrypted_key)
189 }
190
191 pub fn admin(&self) -> CipherAdminClient {
194 CipherAdminClient {
195 client: self.client.clone(),
196 }
197 }
198}
199
200impl CiphersClient {
201 fn get_repository(&self) -> Result<Arc<dyn Repository<Cipher>>, RepositoryError> {
202 Ok(self.client.platform().state().get::<Cipher>()?)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208
209 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
210 #[cfg(feature = "wasm")]
211 use bitwarden_crypto::CryptoError;
212
213 use super::*;
214 use crate::{Attachment, CipherRepromptType, CipherType, Login, LoginView, VaultClientExt};
215
216 fn test_cipher() -> Cipher {
217 Cipher {
218 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
219 organization_id: None,
220 folder_id: None,
221 collection_ids: vec![],
222 key: None,
223 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
224 notes: None,
225 r#type: CipherType::Login,
226 login: Some(Login{
227 username: None,
228 password: None,
229 password_revision_date: None,
230 uris:None,
231 totp: None,
232 autofill_on_page_load: None,
233 fido2_credentials: None,
234 }),
235 identity: None,
236 card: None,
237 secure_note: None,
238 ssh_key: None,
239 favorite: false,
240 reprompt: CipherRepromptType::None,
241 organization_use_totp: true,
242 edit: true,
243 permissions: None,
244 view_password: true,
245 local_data: None,
246 attachments: None,
247 fields: None,
248 password_history: None,
249 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
250 deleted_date: None,
251 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
252 archived_date: None,
253 data: None,
254 }
255 }
256
257 fn test_cipher_view() -> CipherView {
258 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
259 CipherView {
260 r#type: CipherType::Login,
261 login: Some(LoginView {
262 username: Some("test_username".to_string()),
263 password: Some("test_password".to_string()),
264 password_revision_date: None,
265 uris: None,
266 totp: None,
267 autofill_on_page_load: None,
268 fido2_credentials: None,
269 }),
270 id: Some(test_id),
271 organization_id: None,
272 folder_id: None,
273 collection_ids: vec![],
274 key: None,
275 name: "My test login".to_string(),
276 notes: None,
277 identity: None,
278 card: None,
279 secure_note: None,
280 ssh_key: None,
281 favorite: false,
282 reprompt: CipherRepromptType::None,
283 organization_use_totp: true,
284 edit: true,
285 permissions: None,
286 view_password: true,
287 local_data: None,
288 attachments: None,
289 attachment_decryption_failures: None,
290 fields: None,
291 password_history: None,
292 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
293 deleted_date: None,
294 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
295 archived_date: None,
296 }
297 }
298
299 fn test_attachment_legacy() -> Attachment {
300 Attachment {
301 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
302 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
303 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
304 key: None,
305 size: Some("65".to_string()),
306 size_name: Some("65 Bytes".to_string()),
307 }
308 }
309
310 fn test_attachment_v2() -> Attachment {
311 Attachment {
312 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
313 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
314 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
315 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
316 size: Some("65".to_string()),
317 size_name: Some("65 Bytes".to_string()),
318 }
319 }
320
321 #[tokio::test]
322 async fn test_decrypt_list() {
323 let client = Client::init_test_account(test_bitwarden_com_account()).await;
324
325 let dec = client
326 .vault()
327 .ciphers()
328 .decrypt_list(vec![Cipher {
329 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
330 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
331 folder_id: None,
332 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
333 key: None,
334 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
335 notes: None,
336 r#type: CipherType::Login,
337 login: Some(Login{
338 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
339 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
340 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
341 identity: None,
342 card: None,
343 secure_note: None,
344 ssh_key: None,
345 favorite: false,
346 reprompt: CipherRepromptType::None,
347 organization_use_totp: true,
348 edit: true,
349 permissions: None,
350 view_password: true,
351 local_data: None,
352 attachments: None,
353 fields: None,
354 password_history: None,
355 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
356 deleted_date: None,
357 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
358 archived_date: None,
359 data: None,
360 }])
361
362 .unwrap();
363
364 assert_eq!(dec[0].name, "Test item");
365 }
366
367 #[tokio::test]
368 async fn test_decrypt_list_with_failures_all_success() {
369 let client = Client::init_test_account(test_bitwarden_com_account()).await;
370
371 let valid_cipher = test_cipher();
372
373 let result = client
374 .vault()
375 .ciphers()
376 .decrypt_list_with_failures(vec![valid_cipher]);
377
378 assert_eq!(result.successes.len(), 1);
379 assert!(result.failures.is_empty());
380 assert_eq!(result.successes[0].name, "234234");
381 }
382
383 #[tokio::test]
384 async fn test_decrypt_list_with_failures_mixed_results() {
385 let client = Client::init_test_account(test_bitwarden_com_account()).await;
386 let valid_cipher = test_cipher();
387 let mut invalid_cipher = test_cipher();
388 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
390
391 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
392
393 let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
394
395 assert_eq!(result.successes.len(), 1);
396 assert_eq!(result.failures.len(), 1);
397
398 assert_eq!(result.successes[0].name, "234234");
399 }
400
401 #[tokio::test]
402 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
403 let client = Client::init_test_account(test_bitwarden_com_account()).await;
404
405 let mut cipher = test_cipher();
406 cipher.attachments = Some(vec![test_attachment_legacy()]);
407
408 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
409
410 let res = client.vault().ciphers().move_to_organization(
412 view,
413 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
414 );
415
416 assert!(res.is_err());
417 }
418
419 #[tokio::test]
420 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
421 let client = Client::init_test_account(test_bitwarden_com_account()).await;
422
423 let mut cipher = test_cipher();
424 let attachment = test_attachment_legacy();
425 cipher.attachments = Some(vec![attachment.clone()]);
426
427 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
428
429 assert!(cipher.key.is_none());
430
431 let EncryptionContext {
433 cipher: new_cipher,
434 encrypted_for: _,
435 } = client.vault().ciphers().encrypt(view).unwrap();
436 assert!(new_cipher.key.is_some());
437
438 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
439 let attachments = view.clone().attachments.unwrap();
440 let attachment_view = attachments.first().unwrap().clone();
441 assert!(attachment_view.key.is_none());
442
443 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
444
445 let buf = vec![
446 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
447 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
448 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
449 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
450 ];
451
452 let content = client
453 .vault()
454 .attachments()
455 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
456 .unwrap();
457
458 assert_eq!(content, b"Hello");
459 }
460
461 #[tokio::test]
462 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
463 let client = Client::init_test_account(test_bitwarden_com_account()).await;
464
465 let mut cipher = test_cipher();
466 let attachment = test_attachment_v2();
467 cipher.attachments = Some(vec![attachment.clone()]);
468
469 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
470
471 assert!(cipher.key.is_none());
472
473 let EncryptionContext {
475 cipher: new_cipher,
476 encrypted_for: _,
477 } = client.vault().ciphers().encrypt(view).unwrap();
478 assert!(new_cipher.key.is_some());
479
480 let view = client
481 .vault()
482 .ciphers()
483 .decrypt(new_cipher.clone())
484 .unwrap();
485 let attachments = view.clone().attachments.unwrap();
486 let attachment_view = attachments.first().unwrap().clone();
487 assert!(attachment_view.key.is_some());
488
489 assert_ne!(
491 attachment.clone().key.unwrap().to_string(),
492 attachment_view.clone().key.unwrap().to_string()
493 );
494
495 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
496
497 let buf = vec![
498 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
499 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
500 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
501 188, 196, 170, 127, 67, 44, 129, 90,
502 ];
503
504 let content = client
505 .vault()
506 .attachments()
507 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
508 .unwrap();
509
510 assert_eq!(content, b"Hello");
511
512 let new_view = client
514 .vault()
515 .ciphers()
516 .move_to_organization(
517 view,
518 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
519 )
520 .unwrap();
521 let EncryptionContext {
522 cipher: new_cipher,
523 encrypted_for: _,
524 } = client.vault().ciphers().encrypt(new_view).unwrap();
525
526 let attachment = new_cipher
527 .clone()
528 .attachments
529 .unwrap()
530 .first()
531 .unwrap()
532 .clone();
533
534 assert_eq!(
536 attachment.clone().key.as_ref().unwrap().to_string(),
537 attachment_view.key.as_ref().unwrap().to_string()
538 );
539
540 let content = client
541 .vault()
542 .attachments()
543 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
544 .unwrap();
545
546 assert_eq!(content, b"Hello");
547 }
548
549 #[tokio::test]
550 #[cfg(feature = "wasm")]
551 async fn test_encrypt_cipher_for_rotation() {
552 let client = Client::init_test_account(test_bitwarden_com_account()).await;
553
554 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
555
556 let cipher_view = test_cipher_view();
557 let new_key_b64 = new_key.to_base64();
558
559 let ctx = client
560 .vault()
561 .ciphers()
562 .encrypt_cipher_for_rotation(cipher_view, new_key_b64)
563 .unwrap();
564
565 assert!(ctx.cipher.key.is_some());
566
567 assert!(matches!(
569 client.vault().ciphers().decrypt(ctx.cipher).err(),
570 Some(DecryptError::Crypto(CryptoError::Decrypt))
571 ));
572 }
573}