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 tsify::Tsify;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::*;
19use zeroize::Zeroizing;
20
21use crate::{
22 SendParseError, SendType,
23 send::{SEND_ITERATIONS, SendItemMetadata},
24 send_client::SendClient,
25};
26
27pub(crate) const SEND_KEY_LEN: usize = 16;
31
32#[derive(Debug, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
40pub struct SendAccessResponse {
41 pub id: Option<String>,
43 #[serde(rename = "type")]
45 pub type_: Option<SendType>,
46 pub name: Option<String>,
48 pub text: Option<SendAccessTextResponse>,
50 pub file: Option<SendAccessFileResponse>,
52 pub data: Option<SendAccessItemResponse>,
54 pub expiration_date: Option<DateTime<Utc>>,
56 pub creator_identifier: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
64pub struct SendAccessTextResponse {
65 pub text: Option<String>,
67 pub hidden: bool,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
75pub struct SendAccessFileResponse {
76 pub id: Option<String>,
78 pub file_name: Option<String>,
80 pub size: Option<String>,
82 pub size_name: Option<String>,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
90pub struct SendAccessItemResponse {
91 pub encryption_version: Option<SendEncryptionType>,
93 pub data: Option<String>,
95 pub metadata: SendItemMetadata,
97}
98
99#[derive(Debug, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
103pub struct SendFileDownloadData {
104 pub id: Option<String>,
106 pub url: Option<String>,
108}
109
110#[derive(Debug, Serialize, Deserialize, PartialEq)]
125#[serde(rename_all = "camelCase")]
126#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
127pub struct SendAccessView {
128 pub id: Option<String>,
130 #[serde(rename = "type")]
132 pub type_: Option<SendType>,
133 pub name: Option<String>,
136 pub text: Option<SendAccessTextView>,
138 pub file: Option<SendAccessFileView>,
140 pub data: Option<SendAccessItemView>,
142 pub expiration_date: Option<DateTime<Utc>>,
144 pub creator_identifier: Option<String>,
146}
147
148#[derive(Debug, Serialize, Deserialize, PartialEq)]
150#[serde(rename_all = "camelCase")]
151#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
152pub struct SendAccessTextView {
153 pub text: Option<String>,
155 pub hidden: bool,
157}
158
159#[derive(Debug, Serialize, Deserialize, PartialEq)]
161#[serde(rename_all = "camelCase")]
162#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
163pub struct SendAccessFileView {
164 pub id: Option<String>,
166 pub file_name: Option<String>,
168 pub size: Option<String>,
170 pub size_name: Option<String>,
172}
173
174#[derive(Debug, Serialize, Deserialize, PartialEq)]
176#[serde(rename_all = "camelCase")]
177#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
178pub struct SendAccessItemView {
179 pub data: Option<CipherView>,
181}
182
183#[bitwarden_error(flat)]
188#[derive(Debug, Error)]
189pub enum SendAccessKeyError {
190 #[error("The send key is not valid url-safe base64")]
192 InvalidEncoding,
193 #[error("The send key must be {SEND_KEY_LEN} bytes")]
195 InvalidLength,
196}
197
198#[bitwarden_error(flat)]
201#[derive(Debug, Error)]
202pub enum SendAccessDecryptError {
203 #[error(transparent)]
206 Crypto(#[from] CryptoError),
207 #[error(transparent)]
209 Key(#[from] SendAccessKeyError),
210}
211
212pub struct SendAccessKey {
222 secret: Zeroizing<[u8; SEND_KEY_LEN]>,
226 key: SymmetricCryptoKey,
229}
230
231impl SendAccessKey {
232 pub fn from_url_b64(key_b64: &str) -> Result<Self, SendAccessKeyError> {
241 let decoded = Zeroizing::new(
244 B64Url::try_from(key_b64)
245 .map_err(|_| SendAccessKeyError::InvalidEncoding)?
246 .into_bytes(),
247 );
248 if decoded.len() != SEND_KEY_LEN {
249 return Err(SendAccessKeyError::InvalidLength);
250 }
251 let mut secret = Zeroizing::new([0u8; SEND_KEY_LEN]);
252 secret.copy_from_slice(&decoded);
253
254 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_shareable_key(
255 secret.clone(),
256 "send",
257 Some("send"),
258 ));
259
260 Ok(Self { secret, key })
261 }
262
263 pub fn hash_password_b64(&self, password: &str) -> String {
270 let hashed =
271 bitwarden_crypto::pbkdf2(password.as_bytes(), self.secret.as_slice(), SEND_ITERATIONS);
272 B64::from(hashed.as_slice()).to_string()
273 }
274
275 pub fn decrypt_response(
278 &self,
279 response: SendAccessResponse,
280 ) -> Result<SendAccessView, SendAccessDecryptError> {
281 let text = match response.text {
282 Some(t) => Some(SendAccessTextView {
283 text: self.decrypt_optional(t.text)?,
284 hidden: t.hidden,
285 }),
286 None => None,
287 };
288 let file = match response.file {
289 Some(f) => Some(SendAccessFileView {
290 id: f.id,
291 file_name: self.decrypt_optional(f.file_name)?,
292 size: f.size,
293 size_name: f.size_name,
294 }),
295 None => None,
296 };
297 let data = match response.data {
298 Some(d) => {
299 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
300 let mut ctx = key_store.context_mut();
301 let key = ctx.add_local_symmetric_key(self.key.clone());
302 let Some(data) = d.data else {
303 return Err(SendAccessDecryptError::Crypto(CryptoError::MissingField(
304 "data",
305 )));
306 };
307 let mut cipher_view = CipherView::unseal_blob_for_item_sends(&data, &mut ctx, key)?;
308 cipher_view.id = Some(d.metadata.item_id);
310 Some(SendAccessItemView {
311 data: Some(cipher_view),
312 })
313 }
314 None => None,
315 };
316
317 Ok(SendAccessView {
318 id: response.id,
319 type_: response.type_,
320 name: self.decrypt_optional(response.name)?,
321 text,
322 file,
323 data,
324 expiration_date: response.expiration_date,
325 creator_identifier: response.creator_identifier,
326 })
327 }
328
329 pub fn decrypt_file_buffer(&self, buffer: &[u8]) -> Result<Vec<u8>, SendAccessDecryptError> {
333 Ok(EncString::from_buffer(buffer)?.decrypt_with_key(&self.key)?)
334 }
335
336 fn decrypt_optional(
339 &self,
340 value: Option<String>,
341 ) -> Result<Option<String>, SendAccessDecryptError> {
342 match value {
343 Some(s) => Ok(Some(s.parse::<EncString>()?.decrypt_with_key(&self.key)?)),
344 None => Ok(None),
345 }
346 }
347}
348
349#[bitwarden_error(flat)]
351#[derive(Debug, Error)]
352pub enum AccessSendError {
353 #[error(transparent)]
355 Api(#[from] ApiError),
356 #[error(transparent)]
360 Parse(#[from] SendParseError),
361}
362
363#[bitwarden_error(flat)]
365#[derive(Debug, Error)]
366pub enum GetFileDownloadDataError {
367 #[error(transparent)]
369 Api(#[from] ApiError),
370}
371
372async fn access_send(
375 api_client: &ApiClient,
376 access_token: &str,
377) -> Result<SendAccessResponse, AccessSendError> {
378 let resp = api_client
379 .sends_api()
380 .access_using_auth(access_token)
381 .await?;
382 Ok(resp.try_into()?)
383}
384
385async fn get_file_download_data(
386 api_client: &ApiClient,
387 file_id: &str,
388 access_token: &str,
389) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
390 let resp = api_client
391 .sends_api()
392 .get_send_file_download_data_using_auth(file_id, access_token)
393 .await?;
394 Ok(resp.into())
395}
396
397impl TryFrom<models::SendAccessResponseModel> for SendAccessResponse {
400 type Error = SendParseError;
401
402 fn try_from(r: models::SendAccessResponseModel) -> Result<Self, Self::Error> {
403 Ok(SendAccessResponse {
404 id: r.id,
405 type_: r.r#type.map(SendType::try_from).transpose()?,
406 name: r.name,
407 text: r.text.map(|t| SendAccessTextResponse {
408 text: t.text,
409 hidden: t.hidden.unwrap_or(false),
410 }),
411 file: r.file.map(|f| SendAccessFileResponse {
412 id: f.id,
413 file_name: f.file_name,
414 size: f.size,
415 size_name: f.size_name,
416 }),
417 data: r.data.map(|dat| SendAccessItemResponse {
418 encryption_version: dat.encryption_version,
419 data: dat.data,
420 metadata: SendItemMetadata {
421 item_id: CipherId::new(dat.metadata.item_id),
422 },
423 }),
424 expiration_date: r.expiration_date.map(|s| s.parse()).transpose()?,
425 creator_identifier: r.creator_identifier,
426 })
427 }
428}
429
430impl From<models::SendFileDownloadDataResponseModel> for SendFileDownloadData {
431 fn from(r: models::SendFileDownloadDataResponseModel) -> Self {
432 SendFileDownloadData {
433 id: r.id,
434 url: r.url,
435 }
436 }
437}
438
439#[cfg_attr(feature = "wasm", wasm_bindgen)]
442impl SendClient {
443 pub async fn access_send(
447 &self,
448 access_token: String,
449 ) -> Result<SendAccessResponse, AccessSendError> {
450 let config = self.client.internal.get_api_configurations();
451 access_send(&config.api_client, &access_token).await
452 }
453
454 pub async fn get_file_download_data(
456 &self,
457 access_token: String,
458 file_id: String,
459 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
460 let config = self.client.internal.get_api_configurations();
461 get_file_download_data(&config.api_client, &file_id, &access_token).await
462 }
463
464 pub fn decrypt_send_access(
471 key_b64: String,
472 response: SendAccessResponse,
473 ) -> Result<SendAccessView, SendAccessDecryptError> {
474 let access_key = SendAccessKey::from_url_b64(key_b64.as_str())?;
475 access_key.decrypt_response(response)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use bitwarden_api_api::{
482 apis::ApiClient,
483 models::{
484 SendAccessResponseModel, SendFileDownloadDataResponseModel, SendFileModel,
485 SendTextModel, SendType,
486 },
487 };
488
489 use super::*;
490
491 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
492 const FILE_ID: &str = "file-id-abc";
493 const ACCESS_TOKEN: &str = "send-access-token";
494
495 #[tokio::test]
498 async fn test_access_send_text() {
499 let api_client = ApiClient::new_mocked(|mock| {
500 mock.sends_api
501 .expect_access_using_auth()
502 .returning(|token| {
503 assert_eq!(token, ACCESS_TOKEN);
504 Ok(SendAccessResponseModel {
505 object: Some("send-access".to_string()),
506 id: Some(SEND_ID.to_string()),
507 r#type: Some(SendType::Text),
508 auth_type: None,
509 name: Some("encrypted-name".to_string()),
510 file: None,
511 text: Some(Box::new(SendTextModel {
512 text: Some("encrypted_send_text".to_string()),
513 hidden: Some(true),
514 })),
515 data: None,
516 expiration_date: Some("2025-01-10T00:00:00Z".to_string()),
517 creator_identifier: Some("[email protected]".to_string()),
518 })
519 })
520 .once();
521 });
522
523 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
524
525 assert_eq!(result.id, Some(SEND_ID.to_string()));
526 assert_eq!(result.type_, Some(crate::SendType::Text));
527 assert_eq!(result.name, Some("encrypted-name".to_string()));
528 assert!(result.file.is_none());
529 let text = result.text.expect("text variant should be populated");
530 assert_eq!(text.text, Some("encrypted_send_text".to_string()));
531 assert!(text.hidden);
532 assert_eq!(
533 result.expiration_date,
534 Some("2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap())
535 );
536 assert_eq!(
537 result.creator_identifier,
538 Some("[email protected]".to_string())
539 );
540 }
541
542 #[tokio::test]
543 async fn test_access_send_file() {
544 let api_client = ApiClient::new_mocked(|mock| {
545 mock.sends_api
546 .expect_access_using_auth()
547 .returning(|token| {
548 assert_eq!(token, ACCESS_TOKEN);
549 Ok(SendAccessResponseModel {
550 object: Some("send-access".to_string()),
551 id: Some(SEND_ID.to_string()),
552 r#type: Some(SendType::File),
553 auth_type: None,
554 name: Some("encrypted-name".to_string()),
555 file: Some(Box::new(SendFileModel {
556 id: Some(FILE_ID.to_string()),
557 file_name: Some("encrypted-file-name".to_string()),
558 size: Some("4200".to_string()),
559 size_name: Some("4.2 KB".to_string()),
560 })),
561 text: None,
562 data: None,
563 expiration_date: None,
564 creator_identifier: None,
565 })
566 })
567 .once();
568 });
569
570 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
571
572 assert_eq!(result.id, Some(SEND_ID.to_string()));
573 assert_eq!(result.type_, Some(crate::SendType::File));
574 assert_eq!(result.name, Some("encrypted-name".to_string()));
575 assert!(result.text.is_none());
576 let file = result.file.expect("file variant should be populated");
577 assert_eq!(file.id, Some(FILE_ID.to_string()));
578 assert_eq!(file.file_name, Some("encrypted-file-name".to_string()));
579 assert_eq!(file.size, Some("4200".to_string()));
580 assert_eq!(file.size_name, Some("4.2 KB".to_string()));
581 assert_eq!(result.expiration_date, None);
582 assert_eq!(result.creator_identifier, None);
583 }
584
585 #[tokio::test]
586 async fn test_access_send_http_error() {
587 let api_client = ApiClient::new_mocked(|mock| {
588 mock.sends_api
589 .expect_access_using_auth()
590 .returning(|_token| {
591 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
592 "Simulated error",
593 )))
594 })
595 .once();
596 });
597
598 let result = access_send(&api_client, ACCESS_TOKEN).await;
599
600 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
601 }
602
603 #[tokio::test]
606 async fn test_get_file_download_data() {
607 let api_client = ApiClient::new_mocked(|mock| {
608 mock.sends_api
609 .expect_get_send_file_download_data_using_auth()
610 .returning(|file_id, token| {
611 assert_eq!(file_id, FILE_ID);
612 assert_eq!(token, ACCESS_TOKEN);
613 Ok(SendFileDownloadDataResponseModel {
614 object: Some("send-fileDownload".to_string()),
615 id: Some(FILE_ID.to_string()),
616 url: Some("https://example.com/download".to_string()),
617 })
618 })
619 .once();
620 });
621
622 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN)
623 .await
624 .unwrap();
625
626 assert_eq!(result.id, Some(FILE_ID.to_string()));
627 assert_eq!(result.url, Some("https://example.com/download".to_string()));
628 }
629
630 #[tokio::test]
631 async fn test_get_file_download_data_http_error() {
632 let api_client = ApiClient::new_mocked(|mock| {
633 mock.sends_api
634 .expect_get_send_file_download_data_using_auth()
635 .returning(|_file_id, _token| {
636 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
637 "Simulated error",
638 )))
639 })
640 .once();
641 });
642
643 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
644
645 assert!(matches!(
646 result.unwrap_err(),
647 GetFileDownloadDataError::Api(_)
648 ));
649 }
650
651 mod send_access_key {
654 use bitwarden_core::key_management::create_test_crypto_with_user_key;
658 use bitwarden_crypto::{OctetStreamBytes, PrimitiveEncryptable as _, SymmetricCryptoKey};
659 use bitwarden_vault::CipherId;
660
661 use crate::{
662 Send, SendAccessDecryptError, SendAccessFileResponse, SendAccessKey,
663 SendAccessKeyError, SendAccessResponse, SendAccessTextResponse, SendAuthType,
664 SendClient, SendFileView, SendTextView, SendType, SendView,
665 access::SendAccessItemResponse,
666 send::{
667 SendItemMetadata,
668 tests::{TEST_ITEM_ID, TEST_VECTOR_ITEM_SEND_DATA},
669 },
670 };
671
672 const URL_KEY: &str = "Pgui0FK85cNhBGWHAlBHBw";
675 const USER_KEY: &str = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
676
677 fn user_key() -> SymmetricCryptoKey {
678 USER_KEY
679 .to_string()
680 .try_into()
681 .expect("valid test user key")
682 }
683
684 fn encrypt_send(view: SendView) -> Send {
688 create_test_crypto_with_user_key(user_key())
689 .encrypt(view)
690 .expect("send encrypts")
691 }
692
693 fn text_send_view(text: &str, name: &str) -> SendView {
694 SendView {
695 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
696 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
697 name: name.to_owned(),
698 notes: None,
699 key: Some(URL_KEY.to_owned()),
700 new_password: None,
701 has_password: false,
702 r#type: SendType::Text,
703 file: None,
704 text: Some(SendTextView {
705 text: Some(text.to_owned()),
706 hidden: false,
707 }),
708 data: None,
709 max_access_count: None,
710 access_count: 0,
711 disabled: false,
712 hide_email: false,
713 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
714 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
715 expiration_date: None,
716 emails: Vec::new(),
717 auth_type: crate::AuthType::None,
718 }
719 }
720
721 fn text_send_response(send: &Send) -> SendAccessResponse {
724 SendAccessResponse {
725 id: Some("access-id".to_owned()),
726 type_: Some(SendType::Text),
727 name: Some(send.name.to_string()),
728 text: Some(SendAccessTextResponse {
729 text: send
730 .text
731 .as_ref()
732 .and_then(|t| t.text.as_ref())
733 .map(|t| t.to_string()),
734 hidden: false,
735 }),
736 file: None,
737 data: None,
738 expiration_date: None,
739 creator_identifier: None,
740 }
741 }
742
743 #[test]
750 fn decrypts_ciphertext_produced_by_the_authenticated_path() {
751 let send = encrypt_send(text_send_view("This is a test", "Test"));
752 let response = text_send_response(&send);
753
754 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
755 let view = access_key.decrypt_response(response).expect("decrypts");
756
757 assert_eq!(view.name.as_deref(), Some("Test"));
758 assert_eq!(
759 view.text.expect("text present").text.as_deref(),
760 Some("This is a test")
761 );
762 }
763
764 #[test]
765 fn decrypts_file_name_produced_by_the_authenticated_path() {
766 let mut view = text_send_view("unused", "File Send");
767 view.r#type = SendType::File;
768 view.text = None;
769 view.file = Some(SendFileView {
770 id: Some("file-id".to_owned()),
771 file_name: "secrets.txt".to_owned(),
772 size: Some("11".to_owned()),
773 size_name: Some("11 B".to_owned()),
774 });
775 let send = encrypt_send(view);
776 let file = send.file.expect("file present");
777
778 let response = SendAccessResponse {
779 id: Some("access-id".to_owned()),
780 type_: Some(SendType::File),
781 name: Some(send.name.to_string()),
782 text: None,
783 file: Some(SendAccessFileResponse {
784 id: file.id.clone(),
785 file_name: Some(file.file_name.to_string()),
786 size: file.size.clone(),
787 size_name: file.size_name.clone(),
788 }),
789 data: None,
790 expiration_date: None,
791 creator_identifier: None,
792 };
793
794 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
795 let view = access_key.decrypt_response(response).expect("decrypts");
796
797 let decrypted_file = view.file.expect("file present");
798 assert_eq!(decrypted_file.file_name.as_deref(), Some("secrets.txt"));
799 assert_eq!(decrypted_file.size.as_deref(), Some("11"));
800 assert_eq!(decrypted_file.id.as_deref(), Some("file-id"));
801 assert_eq!(view.name.as_deref(), Some("File Send"));
802 }
803
804 #[test]
809 fn decrypt_file_buffer_round_trips_with_the_authenticated_path() {
810 let plaintext = b"file send contents".to_vec();
811
812 let crypto = create_test_crypto_with_user_key(user_key());
813 let mut ctx = crypto.context();
814 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
815 .expect("url key decodes")
816 .into_bytes();
817 let send_key = Send::derive_shareable_key(&mut ctx, &raw_key).expect("key derives");
818 let encrypted = OctetStreamBytes::from(plaintext.clone())
819 .encrypt(&mut ctx, send_key)
820 .expect("buffer encrypts")
821 .to_buffer()
822 .expect("buffer serializes");
823
824 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
825 let decrypted = access_key
826 .decrypt_file_buffer(&encrypted)
827 .expect("buffer decrypts");
828
829 assert_eq!(decrypted, plaintext);
830 }
831
832 #[test]
837 fn hash_password_b64_matches_send_auth_type_auth_data() {
838 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
839 .expect("url key decodes")
840 .into_bytes();
841
842 let (created_hash, emails) = SendAuthType::Password {
843 password: "hunter2".to_owned(),
844 }
845 .auth_data(&raw_key);
846 assert_eq!(emails, None);
847
848 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
849 let receive_hash = access_key.hash_password_b64("hunter2");
850
851 assert_eq!(
852 created_hash,
853 Some(receive_hash),
854 "receive's password hash must match the one `bw send create` stored"
855 );
856 }
857
858 #[test]
859 fn hash_password_b64_is_salted_with_the_send_key() {
860 let a = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
863 let b = SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
864 assert_ne!(
865 a.hash_password_b64("hunter2"),
866 b.hash_password_b64("hunter2")
867 );
868 }
869
870 #[test]
871 fn from_url_b64_accepts_padded_and_unpadded() {
872 let unpadded = SendAccessKey::from_url_b64(URL_KEY).expect("unpadded parses");
875 let padded =
876 SendAccessKey::from_url_b64(&format!("{URL_KEY}==")).expect("padded parses");
877 assert_eq!(
878 unpadded.hash_password_b64("p"),
879 padded.hash_password_b64("p"),
880 "padded and unpadded forms must derive the same key"
881 );
882 }
883
884 #[test]
885 fn from_url_b64_rejects_invalid_base64() {
886 assert!(matches!(
887 SendAccessKey::from_url_b64("not valid base64!"),
888 Err(SendAccessKeyError::InvalidEncoding)
889 ));
890 }
891
892 #[test]
893 fn from_url_b64_rejects_wrong_length() {
894 for bad in ["AAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"] {
897 assert!(
898 matches!(
899 SendAccessKey::from_url_b64(bad),
900 Err(SendAccessKeyError::InvalidLength)
901 ),
902 "expected InvalidLength for {bad:?}"
903 );
904 }
905 assert!(matches!(
906 SendAccessKey::from_url_b64(""),
907 Err(SendAccessKeyError::InvalidLength)
908 ));
909 }
910
911 #[test]
915 fn decrypt_response_tolerates_absent_fields() {
916 let response = SendAccessResponse {
917 id: None,
918 type_: None,
919 name: None,
920 text: Some(SendAccessTextResponse {
921 text: None,
922 hidden: true,
923 }),
924 file: None,
925 data: None,
926 expiration_date: None,
927 creator_identifier: None,
928 };
929
930 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
931 let view = access_key.decrypt_response(response).expect("decrypts");
932
933 assert_eq!(view.name, None);
934 assert_eq!(view.type_, None);
935 let text = view.text.expect("text block present");
936 assert_eq!(text.text, None);
937 assert!(text.hidden);
938 }
939
940 #[test]
941 fn decrypt_response_restores_item_id_from_metadata() {
942 let item_id: CipherId = TEST_ITEM_ID.parse().unwrap();
943 let response = SendAccessResponse {
944 id: None,
945 type_: Some(SendType::Item),
946 name: None,
947 text: None,
948 file: None,
949 data: Some(SendAccessItemResponse {
950 encryption_version: None,
951 data: Some(TEST_VECTOR_ITEM_SEND_DATA.to_owned()),
952 metadata: SendItemMetadata { item_id },
953 }),
954 expiration_date: None,
955 creator_identifier: None,
956 };
957
958 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
959 let view = access_key.decrypt_response(response).expect("decrypts");
960
961 let cipher = view.data.and_then(|d| d.data).expect("item present");
962 assert_eq!(cipher.id, Some(item_id));
963 }
964
965 #[test]
966 fn decrypt_response_errors_on_a_key_that_does_not_match() {
967 let send = encrypt_send(text_send_view("This is a test", "Test"));
968 let response = SendAccessResponse {
969 id: None,
970 type_: Some(SendType::Text),
971 name: Some(send.name.to_string()),
972 text: None,
973 file: None,
974 data: None,
975 expiration_date: None,
976 creator_identifier: None,
977 };
978
979 let wrong_key =
981 SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
982 assert!(wrong_key.decrypt_response(response).is_err());
983 }
984
985 #[test]
986 fn decrypt_response_errors_on_a_malformed_enc_string() {
987 let response = SendAccessResponse {
988 id: None,
989 type_: Some(SendType::Text),
990 name: Some("this is not an EncString".to_owned()),
991 text: None,
992 file: None,
993 data: None,
994 expiration_date: None,
995 creator_identifier: None,
996 };
997
998 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
999 assert!(access_key.decrypt_response(response).is_err());
1000 }
1001
1002 #[test]
1006 fn send_access_view_serializes_in_camel_case() {
1007 let view = crate::SendAccessView {
1008 id: Some("access-id".to_owned()),
1009 type_: Some(SendType::File),
1010 name: Some("name".to_owned()),
1011 text: None,
1012 file: Some(crate::SendAccessFileView {
1013 id: Some("file-id".to_owned()),
1014 file_name: Some("secrets.txt".to_owned()),
1015 size: Some("11".to_owned()),
1016 size_name: Some("11 B".to_owned()),
1017 }),
1018 data: None,
1019 expiration_date: None,
1020 creator_identifier: None,
1021 };
1022
1023 let json = serde_json::to_value(&view).expect("serializes");
1024 assert_eq!(json["type"], serde_json::json!(1));
1025 assert_eq!(json["file"]["fileName"], serde_json::json!("secrets.txt"));
1026 assert_eq!(json["file"]["sizeName"], serde_json::json!("11 B"));
1027 assert_eq!(json["creatorIdentifier"], serde_json::Value::Null);
1028 }
1029
1030 #[test]
1031 fn decrypt_send_access_success() {
1032 let send = encrypt_send(text_send_view("This is a test", "Test"));
1033 let view =
1034 SendClient::decrypt_send_access(URL_KEY.to_owned(), text_send_response(&send))
1035 .expect("decrypts");
1036
1037 assert_eq!(view.name.as_deref(), Some("Test"));
1038 assert_eq!(
1039 view.text.expect("text present").text.as_deref(),
1040 Some("This is a test")
1041 );
1042 }
1043
1044 #[test]
1045 fn decrypt_send_access_malformed_b64() {
1046 let response = SendAccessResponse {
1047 id: Some("access-id".to_owned()),
1048 type_: Some(SendType::Text),
1049 name: Some("Test".to_owned()),
1050 text: None,
1051 file: None,
1052 data: None,
1053 expiration_date: None,
1054 creator_identifier: None,
1055 };
1056
1057 let result = SendClient::decrypt_send_access("not valid base64!".to_owned(), response);
1058
1059 assert!(matches!(
1060 result.unwrap_err(),
1061 SendAccessDecryptError::Key(SendAccessKeyError::InvalidEncoding)
1062 ));
1063 }
1064 }
1065}