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