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
203 .client
204 .platform()
205 .state()
206 .get_client_managed::<Cipher>()?)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212
213 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
214 #[cfg(feature = "wasm")]
215 use bitwarden_crypto::CryptoError;
216
217 use super::*;
218 use crate::{Attachment, CipherRepromptType, CipherType, Login, LoginView, VaultClientExt};
219
220 fn test_cipher() -> Cipher {
221 Cipher {
222 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
223 organization_id: None,
224 folder_id: None,
225 collection_ids: vec![],
226 key: None,
227 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
228 notes: None,
229 r#type: CipherType::Login,
230 login: Some(Login{
231 username: None,
232 password: None,
233 password_revision_date: None,
234 uris:None,
235 totp: None,
236 autofill_on_page_load: None,
237 fido2_credentials: None,
238 }),
239 identity: None,
240 card: None,
241 secure_note: None,
242 ssh_key: None,
243 favorite: false,
244 reprompt: CipherRepromptType::None,
245 organization_use_totp: true,
246 edit: true,
247 permissions: None,
248 view_password: true,
249 local_data: None,
250 attachments: None,
251 fields: None,
252 password_history: None,
253 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
254 deleted_date: None,
255 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
256 archived_date: None,
257 data: None,
258 }
259 }
260
261 fn test_cipher_view() -> CipherView {
262 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
263 CipherView {
264 r#type: CipherType::Login,
265 login: Some(LoginView {
266 username: Some("test_username".to_string()),
267 password: Some("test_password".to_string()),
268 password_revision_date: None,
269 uris: None,
270 totp: None,
271 autofill_on_page_load: None,
272 fido2_credentials: None,
273 }),
274 id: Some(test_id),
275 organization_id: None,
276 folder_id: None,
277 collection_ids: vec![],
278 key: None,
279 name: "My test login".to_string(),
280 notes: None,
281 identity: None,
282 card: None,
283 secure_note: None,
284 ssh_key: None,
285 favorite: false,
286 reprompt: CipherRepromptType::None,
287 organization_use_totp: true,
288 edit: true,
289 permissions: None,
290 view_password: true,
291 local_data: None,
292 attachments: None,
293 fields: None,
294 password_history: None,
295 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
296 deleted_date: None,
297 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
298 archived_date: None,
299 }
300 }
301
302 fn test_attachment_legacy() -> Attachment {
303 Attachment {
304 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
305 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
306 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
307 key: None,
308 size: Some("65".to_string()),
309 size_name: Some("65 Bytes".to_string()),
310 }
311 }
312
313 fn test_attachment_v2() -> Attachment {
314 Attachment {
315 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
316 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
317 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
318 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
319 size: Some("65".to_string()),
320 size_name: Some("65 Bytes".to_string()),
321 }
322 }
323
324 #[tokio::test]
325 async fn test_decrypt_list() {
326 let client = Client::init_test_account(test_bitwarden_com_account()).await;
327
328 let dec = client
329 .vault()
330 .ciphers()
331 .decrypt_list(vec![Cipher {
332 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
333 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
334 folder_id: None,
335 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
336 key: None,
337 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
338 notes: None,
339 r#type: CipherType::Login,
340 login: Some(Login{
341 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
342 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
343 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
344 identity: None,
345 card: None,
346 secure_note: None,
347 ssh_key: None,
348 favorite: false,
349 reprompt: CipherRepromptType::None,
350 organization_use_totp: true,
351 edit: true,
352 permissions: None,
353 view_password: true,
354 local_data: None,
355 attachments: None,
356 fields: None,
357 password_history: None,
358 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
359 deleted_date: None,
360 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
361 archived_date: None,
362 data: None,
363 }])
364
365 .unwrap();
366
367 assert_eq!(dec[0].name, "Test item");
368 }
369
370 #[tokio::test]
371 async fn test_decrypt_list_with_failures_all_success() {
372 let client = Client::init_test_account(test_bitwarden_com_account()).await;
373
374 let valid_cipher = test_cipher();
375
376 let result = client
377 .vault()
378 .ciphers()
379 .decrypt_list_with_failures(vec![valid_cipher]);
380
381 assert_eq!(result.successes.len(), 1);
382 assert!(result.failures.is_empty());
383 assert_eq!(result.successes[0].name, "234234");
384 }
385
386 #[tokio::test]
387 async fn test_decrypt_list_with_failures_mixed_results() {
388 let client = Client::init_test_account(test_bitwarden_com_account()).await;
389 let valid_cipher = test_cipher();
390 let mut invalid_cipher = test_cipher();
391 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
393
394 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
395
396 let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
397
398 assert_eq!(result.successes.len(), 1);
399 assert_eq!(result.failures.len(), 1);
400
401 assert_eq!(result.successes[0].name, "234234");
402 }
403
404 #[tokio::test]
405 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
406 let client = Client::init_test_account(test_bitwarden_com_account()).await;
407
408 let mut cipher = test_cipher();
409 cipher.attachments = Some(vec![test_attachment_legacy()]);
410
411 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
412
413 let res = client.vault().ciphers().move_to_organization(
415 view,
416 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
417 );
418
419 assert!(res.is_err());
420 }
421
422 #[tokio::test]
423 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
424 let client = Client::init_test_account(test_bitwarden_com_account()).await;
425
426 let mut cipher = test_cipher();
427 let attachment = test_attachment_legacy();
428 cipher.attachments = Some(vec![attachment.clone()]);
429
430 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
431
432 assert!(cipher.key.is_none());
433
434 let EncryptionContext {
436 cipher: new_cipher,
437 encrypted_for: _,
438 } = client.vault().ciphers().encrypt(view).unwrap();
439 assert!(new_cipher.key.is_some());
440
441 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
442 let attachments = view.clone().attachments.unwrap();
443 let attachment_view = attachments.first().unwrap().clone();
444 assert!(attachment_view.key.is_none());
445
446 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
447
448 let buf = vec![
449 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
450 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
451 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
452 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
453 ];
454
455 let content = client
456 .vault()
457 .attachments()
458 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
459 .unwrap();
460
461 assert_eq!(content, b"Hello");
462 }
463
464 #[tokio::test]
465 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
466 let client = Client::init_test_account(test_bitwarden_com_account()).await;
467
468 let mut cipher = test_cipher();
469 let attachment = test_attachment_v2();
470 cipher.attachments = Some(vec![attachment.clone()]);
471
472 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
473
474 assert!(cipher.key.is_none());
475
476 let EncryptionContext {
478 cipher: new_cipher,
479 encrypted_for: _,
480 } = client.vault().ciphers().encrypt(view).unwrap();
481 assert!(new_cipher.key.is_some());
482
483 let view = client
484 .vault()
485 .ciphers()
486 .decrypt(new_cipher.clone())
487 .unwrap();
488 let attachments = view.clone().attachments.unwrap();
489 let attachment_view = attachments.first().unwrap().clone();
490 assert!(attachment_view.key.is_some());
491
492 assert_ne!(
494 attachment.clone().key.unwrap().to_string(),
495 attachment_view.clone().key.unwrap().to_string()
496 );
497
498 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
499
500 let buf = vec![
501 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
502 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
503 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
504 188, 196, 170, 127, 67, 44, 129, 90,
505 ];
506
507 let content = client
508 .vault()
509 .attachments()
510 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
511 .unwrap();
512
513 assert_eq!(content, b"Hello");
514
515 let new_view = client
517 .vault()
518 .ciphers()
519 .move_to_organization(
520 view,
521 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
522 )
523 .unwrap();
524 let EncryptionContext {
525 cipher: new_cipher,
526 encrypted_for: _,
527 } = client.vault().ciphers().encrypt(new_view).unwrap();
528
529 let attachment = new_cipher
530 .clone()
531 .attachments
532 .unwrap()
533 .first()
534 .unwrap()
535 .clone();
536
537 assert_eq!(
539 attachment.clone().key.as_ref().unwrap().to_string(),
540 attachment_view.key.as_ref().unwrap().to_string()
541 );
542
543 let content = client
544 .vault()
545 .attachments()
546 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
547 .unwrap();
548
549 assert_eq!(content, b"Hello");
550 }
551
552 #[tokio::test]
553 #[cfg(feature = "wasm")]
554 async fn test_encrypt_cipher_for_rotation() {
555 let client = Client::init_test_account(test_bitwarden_com_account()).await;
556
557 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
558
559 let cipher_view = test_cipher_view();
560 let new_key_b64 = new_key.to_base64();
561
562 let ctx = client
563 .vault()
564 .ciphers()
565 .encrypt_cipher_for_rotation(cipher_view, new_key_b64)
566 .unwrap();
567
568 assert!(ctx.cipher.key.is_some());
569
570 assert!(matches!(
572 client.vault().ciphers().decrypt(ctx.cipher).err(),
573 Some(DecryptError::Crypto(CryptoError::InvalidMac))
574 ));
575 }
576}