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