1use bitwarden_api_api::{
2 apis::ApiClient,
3 models::{self, SendEncryptionType},
4};
5use bitwarden_core::{ApiError, key_management::KeySlotIds};
6use bitwarden_crypto::{
7 CryptoError, EncString, KeyDecryptable as _, KeyStore, SymmetricCryptoKey, derive_shareable_key,
8};
9use bitwarden_encoding::{B64, B64Url};
10use bitwarden_error::bitwarden_error;
11use bitwarden_vault::{CipherId, CipherView};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17use zeroize::Zeroizing;
18
19use crate::{
20 SendParseError, SendType,
21 send::{SEND_ITERATIONS, SendItemMetadata},
22 send_client::SendClient,
23};
24
25pub(crate) const SEND_KEY_LEN: usize = 16;
29
30#[derive(Debug, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37#[bitwarden_ffi::wasm_record]
38pub struct SendAccessResponse {
39 pub id: Option<String>,
41 #[serde(rename = "type")]
43 pub type_: Option<SendType>,
44 pub name: Option<String>,
46 pub text: Option<SendAccessTextResponse>,
48 pub file: Option<SendAccessFileResponse>,
50 pub data: Option<SendAccessItemResponse>,
52 pub expiration_date: Option<DateTime<Utc>>,
54 pub creator_identifier: Option<String>,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61#[bitwarden_ffi::wasm_record]
62pub struct SendAccessTextResponse {
63 pub text: Option<String>,
65 pub hidden: bool,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72#[bitwarden_ffi::wasm_record]
73pub struct SendAccessFileResponse {
74 pub id: Option<String>,
76 pub file_name: Option<String>,
78 pub size: Option<String>,
80 pub size_name: Option<String>,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87#[bitwarden_ffi::wasm_record]
88pub struct SendAccessItemResponse {
89 pub encryption_version: Option<SendEncryptionType>,
91 pub data: Option<String>,
93 pub metadata: SendItemMetadata,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100#[bitwarden_ffi::wasm_record]
101pub struct SendFileDownloadData {
102 pub id: Option<String>,
104 pub url: Option<String>,
106}
107
108#[derive(Debug, Serialize, Deserialize, PartialEq)]
123#[serde(rename_all = "camelCase")]
124#[bitwarden_ffi::wasm_record]
125pub struct SendAccessView {
126 pub id: Option<String>,
128 #[serde(rename = "type")]
130 pub type_: Option<SendType>,
131 pub name: Option<String>,
134 pub text: Option<SendAccessTextView>,
136 pub file: Option<SendAccessFileView>,
138 pub data: Option<SendAccessItemView>,
140 pub expiration_date: Option<DateTime<Utc>>,
142 pub creator_identifier: Option<String>,
144}
145
146#[derive(Debug, Serialize, Deserialize, PartialEq)]
148#[serde(rename_all = "camelCase")]
149#[bitwarden_ffi::wasm_record]
150pub struct SendAccessTextView {
151 pub text: Option<String>,
153 pub hidden: bool,
155}
156
157#[derive(Debug, Serialize, Deserialize, PartialEq)]
159#[serde(rename_all = "camelCase")]
160#[bitwarden_ffi::wasm_record]
161pub struct SendAccessFileView {
162 pub id: Option<String>,
164 pub file_name: Option<String>,
166 pub size: Option<String>,
168 pub size_name: Option<String>,
170}
171
172#[derive(Debug, Serialize, Deserialize, PartialEq)]
174#[serde(rename_all = "camelCase")]
175#[bitwarden_ffi::wasm_record]
176pub struct SendAccessItemView {
177 pub data: Option<CipherView>,
179}
180
181#[bitwarden_error(flat)]
186#[derive(Debug, Error)]
187pub enum SendAccessKeyError {
188 #[error("The send key is not valid url-safe base64")]
190 InvalidEncoding,
191 #[error("The send key must be {SEND_KEY_LEN} bytes")]
193 InvalidLength,
194}
195
196#[bitwarden_error(flat)]
199#[derive(Debug, Error)]
200pub enum SendAccessDecryptError {
201 #[error(transparent)]
204 Crypto(#[from] CryptoError),
205 #[error(transparent)]
207 Key(#[from] SendAccessKeyError),
208}
209
210pub struct SendAccessKey {
220 secret: Zeroizing<[u8; SEND_KEY_LEN]>,
224 key: SymmetricCryptoKey,
227}
228
229impl SendAccessKey {
230 pub fn from_url_b64(key_b64: &str) -> Result<Self, SendAccessKeyError> {
239 let decoded = Zeroizing::new(
242 B64Url::try_from(key_b64)
243 .map_err(|_| SendAccessKeyError::InvalidEncoding)?
244 .into_bytes(),
245 );
246 if decoded.len() != SEND_KEY_LEN {
247 return Err(SendAccessKeyError::InvalidLength);
248 }
249 let mut secret = Zeroizing::new([0u8; SEND_KEY_LEN]);
250 secret.copy_from_slice(&decoded);
251
252 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_shareable_key(
253 secret.clone(),
254 "send",
255 Some("send"),
256 ));
257
258 Ok(Self { secret, key })
259 }
260
261 pub fn hash_password_b64(&self, password: &str) -> String {
268 let hashed =
269 bitwarden_crypto::pbkdf2(password.as_bytes(), self.secret.as_slice(), SEND_ITERATIONS);
270 B64::from(hashed.as_slice()).to_string()
271 }
272
273 pub fn decrypt_response(
276 &self,
277 response: SendAccessResponse,
278 ) -> Result<SendAccessView, SendAccessDecryptError> {
279 let text = match response.text {
280 Some(t) => Some(SendAccessTextView {
281 text: self.decrypt_optional(t.text)?,
282 hidden: t.hidden,
283 }),
284 None => None,
285 };
286 let file = match response.file {
287 Some(f) => Some(SendAccessFileView {
288 id: f.id,
289 file_name: self.decrypt_optional(f.file_name)?,
290 size: f.size,
291 size_name: f.size_name,
292 }),
293 None => None,
294 };
295 let data = match response.data {
296 Some(d) => {
297 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
298 let mut ctx = key_store.context_mut();
299 let key = ctx.add_local_symmetric_key(self.key.clone());
300 let Some(data) = d.data else {
301 return Err(SendAccessDecryptError::Crypto(CryptoError::MissingField(
302 "data",
303 )));
304 };
305 let mut cipher_view = CipherView::unseal_blob_for_item_sends(&data, &mut ctx, key)?;
306 cipher_view.id = Some(d.metadata.item_id);
308 Some(SendAccessItemView {
309 data: Some(cipher_view),
310 })
311 }
312 None => None,
313 };
314
315 Ok(SendAccessView {
316 id: response.id,
317 type_: response.type_,
318 name: self.decrypt_optional(response.name)?,
319 text,
320 file,
321 data,
322 expiration_date: response.expiration_date,
323 creator_identifier: response.creator_identifier,
324 })
325 }
326
327 pub fn decrypt_file_buffer(&self, buffer: &[u8]) -> Result<Vec<u8>, SendAccessDecryptError> {
331 Ok(EncString::from_buffer(buffer)?.decrypt_with_key(&self.key)?)
332 }
333
334 fn decrypt_optional(
337 &self,
338 value: Option<String>,
339 ) -> Result<Option<String>, SendAccessDecryptError> {
340 match value {
341 Some(s) => Ok(Some(s.parse::<EncString>()?.decrypt_with_key(&self.key)?)),
342 None => Ok(None),
343 }
344 }
345}
346
347#[bitwarden_error(flat)]
349#[derive(Debug, Error)]
350pub enum AccessSendError {
351 #[error("The send was not found")]
353 NotFound,
354 #[error(transparent)]
356 Api(ApiError),
357 #[error(transparent)]
361 Parse(#[from] SendParseError),
362}
363
364#[bitwarden_error(flat)]
366#[derive(Debug, Error)]
367pub enum GetFileDownloadDataError {
368 #[error("The send file was not found")]
371 NotFound,
372 #[error(transparent)]
374 Api(ApiError),
375}
376
377fn is_not_found(error: &ApiError) -> bool {
378 matches!(error, ApiError::Response(r) if r.status == reqwest::StatusCode::NOT_FOUND)
379}
380
381impl From<ApiError> for AccessSendError {
382 fn from(error: ApiError) -> Self {
383 if is_not_found(&error) {
384 Self::NotFound
385 } else {
386 Self::Api(error)
387 }
388 }
389}
390
391impl From<ApiError> for GetFileDownloadDataError {
392 fn from(error: ApiError) -> Self {
393 if is_not_found(&error) {
394 Self::NotFound
395 } else {
396 Self::Api(error)
397 }
398 }
399}
400
401pub(crate) async fn access_send(
404 api_client: &ApiClient,
405 access_token: &str,
406) -> Result<SendAccessResponse, AccessSendError> {
407 let resp = api_client
408 .sends_api()
409 .access_using_auth(access_token)
410 .await?;
411 Ok(resp.try_into()?)
412}
413
414pub(crate) async fn get_file_download_data(
415 api_client: &ApiClient,
416 file_id: &str,
417 access_token: &str,
418) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
419 let resp = api_client
420 .sends_api()
421 .get_send_file_download_data_using_auth(file_id, access_token)
422 .await?;
423 Ok(resp.into())
424}
425
426impl TryFrom<models::SendAccessResponseModel> for SendAccessResponse {
429 type Error = SendParseError;
430
431 fn try_from(r: models::SendAccessResponseModel) -> Result<Self, Self::Error> {
432 Ok(SendAccessResponse {
433 id: r.id,
434 type_: r.r#type.map(SendType::try_from).transpose()?,
435 name: r.name,
436 text: r.text.map(|t| SendAccessTextResponse {
437 text: t.text,
438 hidden: t.hidden.unwrap_or(false),
439 }),
440 file: r.file.map(|f| SendAccessFileResponse {
441 id: f.id,
442 file_name: f.file_name,
443 size: f.size,
444 size_name: f.size_name,
445 }),
446 data: r.data.map(|dat| SendAccessItemResponse {
447 encryption_version: dat.encryption_version,
448 data: dat.data,
449 metadata: SendItemMetadata {
450 item_id: CipherId::new(dat.metadata.item_id),
451 },
452 }),
453 expiration_date: r.expiration_date.map(|s| s.parse()).transpose()?,
454 creator_identifier: r.creator_identifier,
455 })
456 }
457}
458
459impl From<models::SendFileDownloadDataResponseModel> for SendFileDownloadData {
460 fn from(r: models::SendFileDownloadDataResponseModel) -> Self {
461 SendFileDownloadData {
462 id: r.id,
463 url: r.url,
464 }
465 }
466}
467
468#[allow(deprecated)]
473#[bitwarden_ffi::wasm_export]
474impl SendClient {
475 #[deprecated(
479 note = "use `SendReceiveClient::access_send` instead; a `SendClient` is tied to the signed-in instance"
480 )]
481 pub async fn access_send(
482 &self,
483 access_token: String,
484 ) -> Result<SendAccessResponse, AccessSendError> {
485 let config = self.client.internal.get_api_configurations();
486 access_send(&config.api_client, &access_token).await
487 }
488
489 #[deprecated(
491 note = "use `SendReceiveClient::get_file_download_data` instead; a `SendClient` is tied to the signed-in instance"
492 )]
493 pub async fn get_file_download_data(
494 &self,
495 access_token: String,
496 file_id: String,
497 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
498 let config = self.client.internal.get_api_configurations();
499 get_file_download_data(&config.api_client, &file_id, &access_token).await
500 }
501
502 #[deprecated(
509 note = "use `SendReceiveClient::decrypt_send_access` instead; a `SendClient` is tied to the signed-in instance"
510 )]
511 pub fn decrypt_send_access(
512 key_b64: String,
513 response: SendAccessResponse,
514 ) -> Result<SendAccessView, SendAccessDecryptError> {
515 let access_key = SendAccessKey::from_url_b64(key_b64.as_str())?;
516 access_key.decrypt_response(response)
517 }
518}
519
520#[cfg(test)]
521#[allow(deprecated)]
522mod tests {
523 use bitwarden_api_api::{
524 apis::ApiClient,
525 models::{
526 SendAccessResponseModel, SendFileDownloadDataResponseModel, SendFileModel,
527 SendTextModel, SendType,
528 },
529 };
530
531 use super::*;
532
533 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
534 const FILE_ID: &str = "file-id-abc";
535 const ACCESS_TOKEN: &str = "send-access-token";
536
537 #[tokio::test]
540 async fn test_access_send_text() {
541 let api_client = ApiClient::new_mocked(|mock| {
542 mock.sends_api
543 .expect_access_using_auth()
544 .returning(|token| {
545 assert_eq!(token, ACCESS_TOKEN);
546 Ok(SendAccessResponseModel {
547 object: Some("send-access".to_string()),
548 id: Some(SEND_ID.to_string()),
549 r#type: Some(SendType::Text),
550 auth_type: None,
551 name: Some("encrypted-name".to_string()),
552 file: None,
553 text: Some(Box::new(SendTextModel {
554 text: Some("encrypted_send_text".to_string()),
555 hidden: Some(true),
556 })),
557 data: None,
558 expiration_date: Some("2025-01-10T00:00:00Z".to_string()),
559 creator_identifier: Some("[email protected]".to_string()),
560 })
561 })
562 .once();
563 });
564
565 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
566
567 assert_eq!(result.id, Some(SEND_ID.to_string()));
568 assert_eq!(result.type_, Some(crate::SendType::Text));
569 assert_eq!(result.name, Some("encrypted-name".to_string()));
570 assert!(result.file.is_none());
571 let text = result.text.expect("text variant should be populated");
572 assert_eq!(text.text, Some("encrypted_send_text".to_string()));
573 assert!(text.hidden);
574 assert_eq!(
575 result.expiration_date,
576 Some("2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap())
577 );
578 assert_eq!(
579 result.creator_identifier,
580 Some("[email protected]".to_string())
581 );
582 }
583
584 #[tokio::test]
585 async fn test_access_send_file() {
586 let api_client = ApiClient::new_mocked(|mock| {
587 mock.sends_api
588 .expect_access_using_auth()
589 .returning(|token| {
590 assert_eq!(token, ACCESS_TOKEN);
591 Ok(SendAccessResponseModel {
592 object: Some("send-access".to_string()),
593 id: Some(SEND_ID.to_string()),
594 r#type: Some(SendType::File),
595 auth_type: None,
596 name: Some("encrypted-name".to_string()),
597 file: Some(Box::new(SendFileModel {
598 id: Some(FILE_ID.to_string()),
599 file_name: Some("encrypted-file-name".to_string()),
600 size: Some("4200".to_string()),
601 size_name: Some("4.2 KB".to_string()),
602 })),
603 text: None,
604 data: None,
605 expiration_date: None,
606 creator_identifier: None,
607 })
608 })
609 .once();
610 });
611
612 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
613
614 assert_eq!(result.id, Some(SEND_ID.to_string()));
615 assert_eq!(result.type_, Some(crate::SendType::File));
616 assert_eq!(result.name, Some("encrypted-name".to_string()));
617 assert!(result.text.is_none());
618 let file = result.file.expect("file variant should be populated");
619 assert_eq!(file.id, Some(FILE_ID.to_string()));
620 assert_eq!(file.file_name, Some("encrypted-file-name".to_string()));
621 assert_eq!(file.size, Some("4200".to_string()));
622 assert_eq!(file.size_name, Some("4.2 KB".to_string()));
623 assert_eq!(result.expiration_date, None);
624 assert_eq!(result.creator_identifier, None);
625 }
626
627 #[tokio::test]
628 async fn test_access_send_http_error() {
629 let api_client = ApiClient::new_mocked(|mock| {
630 mock.sends_api
631 .expect_access_using_auth()
632 .returning(|_token| {
633 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
634 "Simulated error",
635 )))
636 })
637 .once();
638 });
639
640 let result = access_send(&api_client, ACCESS_TOKEN).await;
641
642 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
643 }
644
645 #[tokio::test]
646 async fn test_access_send_404_is_not_found() {
647 let api_client = ApiClient::new_mocked(|mock| {
648 mock.sends_api
649 .expect_access_using_auth()
650 .returning(|_token| Err(not_found_response()))
651 .once();
652 });
653
654 let result = access_send(&api_client, ACCESS_TOKEN).await;
655
656 assert!(matches!(result.unwrap_err(), AccessSendError::NotFound));
657 }
658
659 #[tokio::test]
660 async fn test_access_send_other_status_stays_api_error() {
661 let api_client = ApiClient::new_mocked(|mock| {
662 mock.sends_api
663 .expect_access_using_auth()
664 .returning(|_token| {
665 Err(bitwarden_api_api::ApiError::Response(
666 bitwarden_api_api::ResponseContent {
667 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
668 message: "boom".to_string(),
669 },
670 ))
671 })
672 .once();
673 });
674
675 let result = access_send(&api_client, ACCESS_TOKEN).await;
676
677 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
678 }
679
680 fn not_found_response() -> bitwarden_api_api::ApiError {
681 bitwarden_api_api::ApiError::Response(bitwarden_api_api::ResponseContent {
682 status: reqwest::StatusCode::NOT_FOUND,
683 message: "Not Found".to_string(),
684 })
685 }
686
687 #[tokio::test]
690 async fn test_get_file_download_data() {
691 let api_client = ApiClient::new_mocked(|mock| {
692 mock.sends_api
693 .expect_get_send_file_download_data_using_auth()
694 .returning(|file_id, token| {
695 assert_eq!(file_id, FILE_ID);
696 assert_eq!(token, ACCESS_TOKEN);
697 Ok(SendFileDownloadDataResponseModel {
698 object: Some("send-fileDownload".to_string()),
699 id: Some(FILE_ID.to_string()),
700 url: Some("https://example.com/download".to_string()),
701 })
702 })
703 .once();
704 });
705
706 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN)
707 .await
708 .unwrap();
709
710 assert_eq!(result.id, Some(FILE_ID.to_string()));
711 assert_eq!(result.url, Some("https://example.com/download".to_string()));
712 }
713
714 #[tokio::test]
715 async fn test_get_file_download_data_http_error() {
716 let api_client = ApiClient::new_mocked(|mock| {
717 mock.sends_api
718 .expect_get_send_file_download_data_using_auth()
719 .returning(|_file_id, _token| {
720 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
721 "Simulated error",
722 )))
723 })
724 .once();
725 });
726
727 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
728
729 assert!(matches!(
730 result.unwrap_err(),
731 GetFileDownloadDataError::Api(_)
732 ));
733 }
734
735 #[tokio::test]
736 async fn test_get_file_download_data_404_is_not_found() {
737 let api_client = ApiClient::new_mocked(|mock| {
738 mock.sends_api
739 .expect_get_send_file_download_data_using_auth()
740 .returning(|_file_id, _token| Err(not_found_response()))
741 .once();
742 });
743
744 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
745
746 assert!(matches!(
747 result.unwrap_err(),
748 GetFileDownloadDataError::NotFound
749 ));
750 }
751
752 mod send_access_key {
755 use bitwarden_core::key_management::create_test_crypto_with_user_key;
759 use bitwarden_crypto::{OctetStreamBytes, PrimitiveEncryptable as _, SymmetricCryptoKey};
760 use bitwarden_vault::CipherId;
761
762 use crate::{
763 Send, SendAccessDecryptError, SendAccessFileResponse, SendAccessKey,
764 SendAccessKeyError, SendAccessResponse, SendAccessTextResponse, SendAuthType,
765 SendClient, SendFileView, SendTextView, SendType, SendView,
766 access::SendAccessItemResponse,
767 send::{
768 SendItemMetadata,
769 tests::{TEST_ITEM_ID, TEST_VECTOR_ITEM_SEND_DATA},
770 },
771 };
772
773 const URL_KEY: &str = "Pgui0FK85cNhBGWHAlBHBw";
776 const USER_KEY: &str = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
777
778 fn user_key() -> SymmetricCryptoKey {
779 USER_KEY
780 .to_string()
781 .try_into()
782 .expect("valid test user key")
783 }
784
785 fn encrypt_send(view: SendView) -> Send {
789 create_test_crypto_with_user_key(user_key())
790 .encrypt(view)
791 .expect("send encrypts")
792 }
793
794 fn text_send_view(text: &str, name: &str) -> SendView {
795 SendView {
796 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
797 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
798 name: name.to_owned(),
799 notes: None,
800 key: Some(URL_KEY.to_owned()),
801 new_password: None,
802 has_password: false,
803 r#type: SendType::Text,
804 file: None,
805 text: Some(SendTextView {
806 text: Some(text.to_owned()),
807 hidden: false,
808 }),
809 data: None,
810 max_access_count: None,
811 access_count: 0,
812 disabled: false,
813 hide_email: false,
814 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
815 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
816 expiration_date: None,
817 emails: Vec::new(),
818 auth_type: crate::AuthType::None,
819 }
820 }
821
822 fn text_send_response(send: &Send) -> SendAccessResponse {
825 SendAccessResponse {
826 id: Some("access-id".to_owned()),
827 type_: Some(SendType::Text),
828 name: Some(send.name.to_string()),
829 text: Some(SendAccessTextResponse {
830 text: send
831 .text
832 .as_ref()
833 .and_then(|t| t.text.as_ref())
834 .map(|t| t.to_string()),
835 hidden: false,
836 }),
837 file: None,
838 data: None,
839 expiration_date: None,
840 creator_identifier: None,
841 }
842 }
843
844 #[test]
851 fn decrypts_ciphertext_produced_by_the_authenticated_path() {
852 let send = encrypt_send(text_send_view("This is a test", "Test"));
853 let response = text_send_response(&send);
854
855 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
856 let view = access_key.decrypt_response(response).expect("decrypts");
857
858 assert_eq!(view.name.as_deref(), Some("Test"));
859 assert_eq!(
860 view.text.expect("text present").text.as_deref(),
861 Some("This is a test")
862 );
863 }
864
865 #[test]
866 fn decrypts_file_name_produced_by_the_authenticated_path() {
867 let mut view = text_send_view("unused", "File Send");
868 view.r#type = SendType::File;
869 view.text = None;
870 view.file = Some(SendFileView {
871 id: Some("file-id".to_owned()),
872 file_name: "secrets.txt".to_owned(),
873 size: Some("11".to_owned()),
874 size_name: Some("11 B".to_owned()),
875 });
876 let send = encrypt_send(view);
877 let file = send.file.expect("file present");
878
879 let response = SendAccessResponse {
880 id: Some("access-id".to_owned()),
881 type_: Some(SendType::File),
882 name: Some(send.name.to_string()),
883 text: None,
884 file: Some(SendAccessFileResponse {
885 id: file.id.clone(),
886 file_name: Some(file.file_name.to_string()),
887 size: file.size.clone(),
888 size_name: file.size_name.clone(),
889 }),
890 data: None,
891 expiration_date: None,
892 creator_identifier: None,
893 };
894
895 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
896 let view = access_key.decrypt_response(response).expect("decrypts");
897
898 let decrypted_file = view.file.expect("file present");
899 assert_eq!(decrypted_file.file_name.as_deref(), Some("secrets.txt"));
900 assert_eq!(decrypted_file.size.as_deref(), Some("11"));
901 assert_eq!(decrypted_file.id.as_deref(), Some("file-id"));
902 assert_eq!(view.name.as_deref(), Some("File Send"));
903 }
904
905 #[test]
910 fn decrypt_file_buffer_round_trips_with_the_authenticated_path() {
911 let plaintext = b"file send contents".to_vec();
912
913 let crypto = create_test_crypto_with_user_key(user_key());
914 let mut ctx = crypto.context();
915 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
916 .expect("url key decodes")
917 .into_bytes();
918 let send_key = Send::derive_shareable_key(&mut ctx, &raw_key).expect("key derives");
919 let encrypted = OctetStreamBytes::from(plaintext.clone())
920 .encrypt(&mut ctx, send_key)
921 .expect("buffer encrypts")
922 .to_buffer()
923 .expect("buffer serializes");
924
925 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
926 let decrypted = access_key
927 .decrypt_file_buffer(&encrypted)
928 .expect("buffer decrypts");
929
930 assert_eq!(decrypted, plaintext);
931 }
932
933 #[test]
938 fn hash_password_b64_matches_send_auth_type_auth_data() {
939 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
940 .expect("url key decodes")
941 .into_bytes();
942
943 let (created_hash, emails) = SendAuthType::Password {
944 password: "hunter2".to_owned(),
945 }
946 .auth_data(&raw_key);
947 assert_eq!(emails, None);
948
949 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
950 let receive_hash = access_key.hash_password_b64("hunter2");
951
952 assert_eq!(
953 created_hash,
954 Some(receive_hash),
955 "receive's password hash must match the one `bw send create` stored"
956 );
957 }
958
959 #[test]
960 fn hash_password_b64_is_salted_with_the_send_key() {
961 let a = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
964 let b = SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
965 assert_ne!(
966 a.hash_password_b64("hunter2"),
967 b.hash_password_b64("hunter2")
968 );
969 }
970
971 #[test]
972 fn from_url_b64_accepts_padded_and_unpadded() {
973 let unpadded = SendAccessKey::from_url_b64(URL_KEY).expect("unpadded parses");
976 let padded =
977 SendAccessKey::from_url_b64(&format!("{URL_KEY}==")).expect("padded parses");
978 assert_eq!(
979 unpadded.hash_password_b64("p"),
980 padded.hash_password_b64("p"),
981 "padded and unpadded forms must derive the same key"
982 );
983 }
984
985 #[test]
986 fn from_url_b64_rejects_invalid_base64() {
987 assert!(matches!(
988 SendAccessKey::from_url_b64("not valid base64!"),
989 Err(SendAccessKeyError::InvalidEncoding)
990 ));
991 }
992
993 #[test]
994 fn from_url_b64_rejects_wrong_length() {
995 for bad in ["AAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"] {
998 assert!(
999 matches!(
1000 SendAccessKey::from_url_b64(bad),
1001 Err(SendAccessKeyError::InvalidLength)
1002 ),
1003 "expected InvalidLength for {bad:?}"
1004 );
1005 }
1006 assert!(matches!(
1007 SendAccessKey::from_url_b64(""),
1008 Err(SendAccessKeyError::InvalidLength)
1009 ));
1010 }
1011
1012 #[test]
1016 fn decrypt_response_tolerates_absent_fields() {
1017 let response = SendAccessResponse {
1018 id: None,
1019 type_: None,
1020 name: None,
1021 text: Some(SendAccessTextResponse {
1022 text: None,
1023 hidden: true,
1024 }),
1025 file: None,
1026 data: None,
1027 expiration_date: None,
1028 creator_identifier: None,
1029 };
1030
1031 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
1032 let view = access_key.decrypt_response(response).expect("decrypts");
1033
1034 assert_eq!(view.name, None);
1035 assert_eq!(view.type_, None);
1036 let text = view.text.expect("text block present");
1037 assert_eq!(text.text, None);
1038 assert!(text.hidden);
1039 }
1040
1041 #[test]
1042 fn decrypt_response_restores_item_id_from_metadata() {
1043 let item_id: CipherId = TEST_ITEM_ID.parse().unwrap();
1044 let response = SendAccessResponse {
1045 id: None,
1046 type_: Some(SendType::Item),
1047 name: None,
1048 text: None,
1049 file: None,
1050 data: Some(SendAccessItemResponse {
1051 encryption_version: None,
1052 data: Some(TEST_VECTOR_ITEM_SEND_DATA.to_owned()),
1053 metadata: SendItemMetadata { item_id },
1054 }),
1055 expiration_date: None,
1056 creator_identifier: None,
1057 };
1058
1059 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
1060 let view = access_key.decrypt_response(response).expect("decrypts");
1061
1062 let cipher = view.data.and_then(|d| d.data).expect("item present");
1063 assert_eq!(cipher.id, Some(item_id));
1064 }
1065
1066 #[test]
1067 fn decrypt_response_errors_on_a_key_that_does_not_match() {
1068 let send = encrypt_send(text_send_view("This is a test", "Test"));
1069 let response = SendAccessResponse {
1070 id: None,
1071 type_: Some(SendType::Text),
1072 name: Some(send.name.to_string()),
1073 text: None,
1074 file: None,
1075 data: None,
1076 expiration_date: None,
1077 creator_identifier: None,
1078 };
1079
1080 let wrong_key =
1082 SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
1083 assert!(wrong_key.decrypt_response(response).is_err());
1084 }
1085
1086 #[test]
1087 fn decrypt_response_errors_on_a_malformed_enc_string() {
1088 let response = SendAccessResponse {
1089 id: None,
1090 type_: Some(SendType::Text),
1091 name: Some("this is not an EncString".to_owned()),
1092 text: None,
1093 file: None,
1094 data: None,
1095 expiration_date: None,
1096 creator_identifier: None,
1097 };
1098
1099 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
1100 assert!(access_key.decrypt_response(response).is_err());
1101 }
1102
1103 #[test]
1107 fn send_access_view_serializes_in_camel_case() {
1108 let view = crate::SendAccessView {
1109 id: Some("access-id".to_owned()),
1110 type_: Some(SendType::File),
1111 name: Some("name".to_owned()),
1112 text: None,
1113 file: Some(crate::SendAccessFileView {
1114 id: Some("file-id".to_owned()),
1115 file_name: Some("secrets.txt".to_owned()),
1116 size: Some("11".to_owned()),
1117 size_name: Some("11 B".to_owned()),
1118 }),
1119 data: None,
1120 expiration_date: None,
1121 creator_identifier: None,
1122 };
1123
1124 let json = serde_json::to_value(&view).expect("serializes");
1125 assert_eq!(json["type"], serde_json::json!(1));
1126 assert_eq!(json["file"]["fileName"], serde_json::json!("secrets.txt"));
1127 assert_eq!(json["file"]["sizeName"], serde_json::json!("11 B"));
1128 assert_eq!(json["creatorIdentifier"], serde_json::Value::Null);
1129 }
1130
1131 #[test]
1132 fn decrypt_send_access_success() {
1133 let send = encrypt_send(text_send_view("This is a test", "Test"));
1134 let view =
1135 SendClient::decrypt_send_access(URL_KEY.to_owned(), text_send_response(&send))
1136 .expect("decrypts");
1137
1138 assert_eq!(view.name.as_deref(), Some("Test"));
1139 assert_eq!(
1140 view.text.expect("text present").text.as_deref(),
1141 Some("This is a test")
1142 );
1143 }
1144
1145 #[test]
1146 fn decrypt_send_access_malformed_b64() {
1147 let response = SendAccessResponse {
1148 id: Some("access-id".to_owned()),
1149 type_: Some(SendType::Text),
1150 name: Some("Test".to_owned()),
1151 text: None,
1152 file: None,
1153 data: None,
1154 expiration_date: None,
1155 creator_identifier: None,
1156 };
1157
1158 let result = SendClient::decrypt_send_access("not valid base64!".to_owned(), response);
1159
1160 assert!(matches!(
1161 result.unwrap_err(),
1162 SendAccessDecryptError::Key(SendAccessKeyError::InvalidEncoding)
1163 ));
1164 }
1165 }
1166}