1use bitwarden_api_api::{
2 apis::ApiClient,
3 models::{self, SendEncryptionType},
4};
5use bitwarden_core::{ApiError, key_management::KeySlotIds};
6use bitwarden_crypto::{
7 CryptoError, Decryptable, EncString, KeyDecryptable as _, KeyStore, SymmetricCryptoKey,
8 derive_shareable_key,
9};
10use bitwarden_encoding::{B64, B64Url};
11use bitwarden_error::bitwarden_error;
12use bitwarden_vault::{Cipher, CipherView};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16#[cfg(feature = "wasm")]
17use tsify::Tsify;
18#[cfg(feature = "wasm")]
19use wasm_bindgen::prelude::*;
20use zeroize::Zeroizing;
21
22use crate::{SendParseError, SendType, send::SEND_ITERATIONS, send_client::SendClient};
23
24pub(crate) const SEND_KEY_LEN: usize = 16;
28
29#[derive(Debug, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
37pub struct SendAccessResponse {
38 pub id: Option<String>,
40 #[serde(rename = "type")]
42 pub type_: Option<SendType>,
43 pub name: Option<String>,
45 pub text: Option<SendAccessTextResponse>,
47 pub file: Option<SendAccessFileResponse>,
49 pub data: Option<SendAccessItemResponse>,
51 pub expiration_date: Option<DateTime<Utc>>,
53 pub creator_identifier: Option<String>,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
61pub struct SendAccessTextResponse {
62 pub text: Option<String>,
64 pub hidden: bool,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
72pub struct SendAccessFileResponse {
73 pub id: Option<String>,
75 pub file_name: Option<String>,
77 pub size: Option<String>,
79 pub size_name: Option<String>,
81}
82
83#[derive(Debug, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
87pub struct SendAccessItemResponse {
88 pub encryption_version: Option<SendEncryptionType>,
90 pub data: Option<String>,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
98pub struct SendFileDownloadData {
99 pub id: Option<String>,
101 pub url: Option<String>,
103}
104
105#[derive(Debug, Serialize, Deserialize, PartialEq)]
120#[serde(rename_all = "camelCase")]
121#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
122pub struct SendAccessView {
123 pub id: Option<String>,
125 #[serde(rename = "type")]
127 pub type_: Option<SendType>,
128 pub name: Option<String>,
131 pub text: Option<SendAccessTextView>,
133 pub file: Option<SendAccessFileView>,
135 pub data: Option<SendAccessItemView>,
137 pub expiration_date: Option<DateTime<Utc>>,
139 pub creator_identifier: Option<String>,
141}
142
143#[derive(Debug, Serialize, Deserialize, PartialEq)]
145#[serde(rename_all = "camelCase")]
146#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
147pub struct SendAccessTextView {
148 pub text: Option<String>,
150 pub hidden: bool,
152}
153
154#[derive(Debug, Serialize, Deserialize, PartialEq)]
156#[serde(rename_all = "camelCase")]
157#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
158pub struct SendAccessFileView {
159 pub id: Option<String>,
161 pub file_name: Option<String>,
163 pub size: Option<String>,
165 pub size_name: Option<String>,
167}
168
169#[derive(Debug, Serialize, Deserialize, PartialEq)]
171#[serde(rename_all = "camelCase")]
172#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
173pub struct SendAccessItemView {
174 pub data: Option<CipherView>,
176}
177
178#[bitwarden_error(flat)]
183#[derive(Debug, Error)]
184pub enum SendAccessKeyError {
185 #[error("The send key is not valid url-safe base64")]
187 InvalidEncoding,
188 #[error("The send key must be {SEND_KEY_LEN} bytes")]
190 InvalidLength,
191}
192
193#[bitwarden_error(flat)]
196#[derive(Debug, Error)]
197pub enum SendAccessDecryptError {
198 #[error(transparent)]
201 Crypto(#[from] CryptoError),
202}
203
204pub struct SendAccessKey {
214 secret: Zeroizing<[u8; SEND_KEY_LEN]>,
218 key: SymmetricCryptoKey,
221}
222
223impl SendAccessKey {
224 pub fn from_url_b64(key_b64: &str) -> Result<Self, SendAccessKeyError> {
233 let decoded = Zeroizing::new(
236 B64Url::try_from(key_b64)
237 .map_err(|_| SendAccessKeyError::InvalidEncoding)?
238 .into_bytes(),
239 );
240 if decoded.len() != SEND_KEY_LEN {
241 return Err(SendAccessKeyError::InvalidLength);
242 }
243 let mut secret = Zeroizing::new([0u8; SEND_KEY_LEN]);
244 secret.copy_from_slice(&decoded);
245
246 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_shareable_key(
247 secret.clone(),
248 "send",
249 Some("send"),
250 ));
251
252 Ok(Self { secret, key })
253 }
254
255 pub fn hash_password_b64(&self, password: &str) -> String {
262 let hashed =
263 bitwarden_crypto::pbkdf2(password.as_bytes(), self.secret.as_slice(), SEND_ITERATIONS);
264 B64::from(hashed.as_slice()).to_string()
265 }
266
267 pub fn decrypt_response(
270 &self,
271 response: SendAccessResponse,
272 ) -> Result<SendAccessView, SendAccessDecryptError> {
273 let text = match response.text {
274 Some(t) => Some(SendAccessTextView {
275 text: self.decrypt_optional(t.text)?,
276 hidden: t.hidden,
277 }),
278 None => None,
279 };
280 let file = match response.file {
281 Some(f) => Some(SendAccessFileView {
282 id: f.id,
283 file_name: self.decrypt_optional(f.file_name)?,
284 size: f.size,
285 size_name: f.size_name,
286 }),
287 None => None,
288 };
289 let data = match response.data {
290 Some(d) => {
291 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
292 let mut ctx = key_store.context_mut();
293 let key = ctx.add_local_symmetric_key(self.key.clone());
294 let cipher = serde_json::from_str::<Cipher>(
295 d.data.expect("Item type Send requires data field").as_str(),
296 );
297 match cipher {
298 Ok(c) => {
299 let cipher_view: CipherView = c.decrypt(&mut ctx, key)?;
300 Some(SendAccessItemView {
301 data: Some(cipher_view),
302 })
303 }
304 Err(_) => None,
305 }
306 }
307 None => None,
308 };
309
310 Ok(SendAccessView {
311 id: response.id,
312 type_: response.type_,
313 name: self.decrypt_optional(response.name)?,
314 text,
315 file,
316 data,
317 expiration_date: response.expiration_date,
318 creator_identifier: response.creator_identifier,
319 })
320 }
321
322 pub fn decrypt_file_buffer(&self, buffer: &[u8]) -> Result<Vec<u8>, SendAccessDecryptError> {
326 Ok(EncString::from_buffer(buffer)?.decrypt_with_key(&self.key)?)
327 }
328
329 fn decrypt_optional(
332 &self,
333 value: Option<String>,
334 ) -> Result<Option<String>, SendAccessDecryptError> {
335 match value {
336 Some(s) => Ok(Some(s.parse::<EncString>()?.decrypt_with_key(&self.key)?)),
337 None => Ok(None),
338 }
339 }
340}
341
342#[bitwarden_error(flat)]
344#[derive(Debug, Error)]
345pub enum AccessSendError {
346 #[error(transparent)]
348 Api(#[from] ApiError),
349 #[error(transparent)]
353 Parse(#[from] SendParseError),
354}
355
356#[bitwarden_error(flat)]
358#[derive(Debug, Error)]
359pub enum GetFileDownloadDataError {
360 #[error(transparent)]
362 Api(#[from] ApiError),
363}
364
365async fn access_send(
368 api_client: &ApiClient,
369 access_token: &str,
370) -> Result<SendAccessResponse, AccessSendError> {
371 let resp = api_client
372 .sends_api()
373 .access_using_auth(access_token)
374 .await?;
375 Ok(resp.try_into()?)
376}
377
378async fn get_file_download_data(
379 api_client: &ApiClient,
380 file_id: &str,
381 access_token: &str,
382) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
383 let resp = api_client
384 .sends_api()
385 .get_send_file_download_data_using_auth(file_id, access_token)
386 .await?;
387 Ok(resp.into())
388}
389
390impl TryFrom<models::SendAccessResponseModel> for SendAccessResponse {
393 type Error = SendParseError;
394
395 fn try_from(r: models::SendAccessResponseModel) -> Result<Self, Self::Error> {
396 Ok(SendAccessResponse {
397 id: r.id,
398 type_: r.r#type.map(SendType::try_from).transpose()?,
399 name: r.name,
400 text: r.text.map(|t| SendAccessTextResponse {
401 text: t.text,
402 hidden: t.hidden.unwrap_or(false),
403 }),
404 file: r.file.map(|f| SendAccessFileResponse {
405 id: f.id,
406 file_name: f.file_name,
407 size: f.size,
408 size_name: f.size_name,
409 }),
410 data: r.data.map(|dat| SendAccessItemResponse {
411 encryption_version: dat.encryption_version,
412 data: dat.data,
413 }),
414 expiration_date: r.expiration_date.map(|s| s.parse()).transpose()?,
415 creator_identifier: r.creator_identifier,
416 })
417 }
418}
419
420impl From<models::SendFileDownloadDataResponseModel> for SendFileDownloadData {
421 fn from(r: models::SendFileDownloadDataResponseModel) -> Self {
422 SendFileDownloadData {
423 id: r.id,
424 url: r.url,
425 }
426 }
427}
428
429#[cfg_attr(feature = "wasm", wasm_bindgen)]
432impl SendClient {
433 pub async fn access_send(
437 &self,
438 access_token: String,
439 ) -> Result<SendAccessResponse, AccessSendError> {
440 let config = self.client.internal.get_api_configurations();
441 access_send(&config.api_client, &access_token).await
442 }
443
444 pub async fn get_file_download_data(
446 &self,
447 access_token: String,
448 file_id: String,
449 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
450 let config = self.client.internal.get_api_configurations();
451 get_file_download_data(&config.api_client, &file_id, &access_token).await
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use bitwarden_api_api::{
458 apis::ApiClient,
459 models::{
460 SendAccessResponseModel, SendFileDownloadDataResponseModel, SendFileModel,
461 SendTextModel, SendType,
462 },
463 };
464
465 use super::*;
466
467 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
468 const FILE_ID: &str = "file-id-abc";
469 const ACCESS_TOKEN: &str = "send-access-token";
470
471 #[tokio::test]
474 async fn test_access_send_text() {
475 let api_client = ApiClient::new_mocked(|mock| {
476 mock.sends_api
477 .expect_access_using_auth()
478 .returning(|token| {
479 assert_eq!(token, ACCESS_TOKEN);
480 Ok(SendAccessResponseModel {
481 object: Some("send-access".to_string()),
482 id: Some(SEND_ID.to_string()),
483 r#type: Some(SendType::Text),
484 auth_type: None,
485 name: Some("encrypted-name".to_string()),
486 file: None,
487 text: Some(Box::new(SendTextModel {
488 text: Some("encrypted_send_text".to_string()),
489 hidden: Some(true),
490 })),
491 data: None,
492 expiration_date: Some("2025-01-10T00:00:00Z".to_string()),
493 creator_identifier: Some("[email protected]".to_string()),
494 })
495 })
496 .once();
497 });
498
499 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
500
501 assert_eq!(result.id, Some(SEND_ID.to_string()));
502 assert_eq!(result.type_, Some(crate::SendType::Text));
503 assert_eq!(result.name, Some("encrypted-name".to_string()));
504 assert!(result.file.is_none());
505 let text = result.text.expect("text variant should be populated");
506 assert_eq!(text.text, Some("encrypted_send_text".to_string()));
507 assert!(text.hidden);
508 assert_eq!(
509 result.expiration_date,
510 Some("2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap())
511 );
512 assert_eq!(
513 result.creator_identifier,
514 Some("[email protected]".to_string())
515 );
516 }
517
518 #[tokio::test]
519 async fn test_access_send_file() {
520 let api_client = ApiClient::new_mocked(|mock| {
521 mock.sends_api
522 .expect_access_using_auth()
523 .returning(|token| {
524 assert_eq!(token, ACCESS_TOKEN);
525 Ok(SendAccessResponseModel {
526 object: Some("send-access".to_string()),
527 id: Some(SEND_ID.to_string()),
528 r#type: Some(SendType::File),
529 auth_type: None,
530 name: Some("encrypted-name".to_string()),
531 file: Some(Box::new(SendFileModel {
532 id: Some(FILE_ID.to_string()),
533 file_name: Some("encrypted-file-name".to_string()),
534 size: Some("4200".to_string()),
535 size_name: Some("4.2 KB".to_string()),
536 })),
537 text: None,
538 data: None,
539 expiration_date: None,
540 creator_identifier: None,
541 })
542 })
543 .once();
544 });
545
546 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
547
548 assert_eq!(result.id, Some(SEND_ID.to_string()));
549 assert_eq!(result.type_, Some(crate::SendType::File));
550 assert_eq!(result.name, Some("encrypted-name".to_string()));
551 assert!(result.text.is_none());
552 let file = result.file.expect("file variant should be populated");
553 assert_eq!(file.id, Some(FILE_ID.to_string()));
554 assert_eq!(file.file_name, Some("encrypted-file-name".to_string()));
555 assert_eq!(file.size, Some("4200".to_string()));
556 assert_eq!(file.size_name, Some("4.2 KB".to_string()));
557 assert_eq!(result.expiration_date, None);
558 assert_eq!(result.creator_identifier, None);
559 }
560
561 #[tokio::test]
562 async fn test_access_send_http_error() {
563 let api_client = ApiClient::new_mocked(|mock| {
564 mock.sends_api
565 .expect_access_using_auth()
566 .returning(|_token| {
567 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
568 "Simulated error",
569 )))
570 })
571 .once();
572 });
573
574 let result = access_send(&api_client, ACCESS_TOKEN).await;
575
576 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
577 }
578
579 #[tokio::test]
582 async fn test_get_file_download_data() {
583 let api_client = ApiClient::new_mocked(|mock| {
584 mock.sends_api
585 .expect_get_send_file_download_data_using_auth()
586 .returning(|file_id, token| {
587 assert_eq!(file_id, FILE_ID);
588 assert_eq!(token, ACCESS_TOKEN);
589 Ok(SendFileDownloadDataResponseModel {
590 object: Some("send-fileDownload".to_string()),
591 id: Some(FILE_ID.to_string()),
592 url: Some("https://example.com/download".to_string()),
593 })
594 })
595 .once();
596 });
597
598 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN)
599 .await
600 .unwrap();
601
602 assert_eq!(result.id, Some(FILE_ID.to_string()));
603 assert_eq!(result.url, Some("https://example.com/download".to_string()));
604 }
605
606 #[tokio::test]
607 async fn test_get_file_download_data_http_error() {
608 let api_client = ApiClient::new_mocked(|mock| {
609 mock.sends_api
610 .expect_get_send_file_download_data_using_auth()
611 .returning(|_file_id, _token| {
612 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
613 "Simulated error",
614 )))
615 })
616 .once();
617 });
618
619 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
620
621 assert!(matches!(
622 result.unwrap_err(),
623 GetFileDownloadDataError::Api(_)
624 ));
625 }
626
627 mod send_access_key {
630 use bitwarden_core::key_management::create_test_crypto_with_user_key;
634 use bitwarden_crypto::{OctetStreamBytes, PrimitiveEncryptable as _, SymmetricCryptoKey};
635
636 use crate::{
637 Send, SendAccessFileResponse, SendAccessKey, SendAccessKeyError, SendAccessResponse,
638 SendAccessTextResponse, SendAuthType, SendFileView, SendTextView, SendType, SendView,
639 };
640
641 const URL_KEY: &str = "Pgui0FK85cNhBGWHAlBHBw";
644 const USER_KEY: &str = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
645
646 fn user_key() -> SymmetricCryptoKey {
647 USER_KEY
648 .to_string()
649 .try_into()
650 .expect("valid test user key")
651 }
652
653 fn encrypt_send(view: SendView) -> Send {
657 create_test_crypto_with_user_key(user_key())
658 .encrypt(view)
659 .expect("send encrypts")
660 }
661
662 fn text_send_view(text: &str, name: &str) -> SendView {
663 SendView {
664 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
665 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
666 name: name.to_owned(),
667 notes: None,
668 key: Some(URL_KEY.to_owned()),
669 new_password: None,
670 has_password: false,
671 r#type: SendType::Text,
672 file: None,
673 text: Some(SendTextView {
674 text: Some(text.to_owned()),
675 hidden: false,
676 }),
677 data: None,
678 max_access_count: None,
679 access_count: 0,
680 disabled: false,
681 hide_email: false,
682 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
683 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
684 expiration_date: None,
685 emails: Vec::new(),
686 auth_type: crate::AuthType::None,
687 }
688 }
689
690 #[test]
697 fn decrypts_ciphertext_produced_by_the_authenticated_path() {
698 let send = encrypt_send(text_send_view("This is a test", "Test"));
699
700 let response = SendAccessResponse {
701 id: Some("access-id".to_owned()),
702 type_: Some(SendType::Text),
703 name: Some(send.name.to_string()),
704 text: Some(SendAccessTextResponse {
705 text: send
706 .text
707 .as_ref()
708 .and_then(|t| t.text.as_ref())
709 .map(|t| t.to_string()),
710 hidden: false,
711 }),
712 file: None,
713 data: None,
714 expiration_date: None,
715 creator_identifier: None,
716 };
717
718 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
719 let view = access_key.decrypt_response(response).expect("decrypts");
720
721 assert_eq!(view.name.as_deref(), Some("Test"));
722 assert_eq!(
723 view.text.expect("text present").text.as_deref(),
724 Some("This is a test")
725 );
726 }
727
728 #[test]
729 fn decrypts_file_name_produced_by_the_authenticated_path() {
730 let mut view = text_send_view("unused", "File Send");
731 view.r#type = SendType::File;
732 view.text = None;
733 view.file = Some(SendFileView {
734 id: Some("file-id".to_owned()),
735 file_name: "secrets.txt".to_owned(),
736 size: Some("11".to_owned()),
737 size_name: Some("11 B".to_owned()),
738 });
739 let send = encrypt_send(view);
740 let file = send.file.expect("file present");
741
742 let response = SendAccessResponse {
743 id: Some("access-id".to_owned()),
744 type_: Some(SendType::File),
745 name: Some(send.name.to_string()),
746 text: None,
747 file: Some(SendAccessFileResponse {
748 id: file.id.clone(),
749 file_name: Some(file.file_name.to_string()),
750 size: file.size.clone(),
751 size_name: file.size_name.clone(),
752 }),
753 data: None,
754 expiration_date: None,
755 creator_identifier: None,
756 };
757
758 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
759 let view = access_key.decrypt_response(response).expect("decrypts");
760
761 let decrypted_file = view.file.expect("file present");
762 assert_eq!(decrypted_file.file_name.as_deref(), Some("secrets.txt"));
763 assert_eq!(decrypted_file.size.as_deref(), Some("11"));
764 assert_eq!(decrypted_file.id.as_deref(), Some("file-id"));
765 assert_eq!(view.name.as_deref(), Some("File Send"));
766 }
767
768 #[test]
773 fn decrypt_file_buffer_round_trips_with_the_authenticated_path() {
774 let plaintext = b"file send contents".to_vec();
775
776 let crypto = create_test_crypto_with_user_key(user_key());
777 let mut ctx = crypto.context();
778 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
779 .expect("url key decodes")
780 .into_bytes();
781 let send_key = Send::derive_shareable_key(&mut ctx, &raw_key).expect("key derives");
782 let encrypted = OctetStreamBytes::from(plaintext.clone())
783 .encrypt(&mut ctx, send_key)
784 .expect("buffer encrypts")
785 .to_buffer()
786 .expect("buffer serializes");
787
788 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
789 let decrypted = access_key
790 .decrypt_file_buffer(&encrypted)
791 .expect("buffer decrypts");
792
793 assert_eq!(decrypted, plaintext);
794 }
795
796 #[test]
801 fn hash_password_b64_matches_send_auth_type_auth_data() {
802 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
803 .expect("url key decodes")
804 .into_bytes();
805
806 let (created_hash, emails) = SendAuthType::Password {
807 password: "hunter2".to_owned(),
808 }
809 .auth_data(&raw_key);
810 assert_eq!(emails, None);
811
812 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
813 let receive_hash = access_key.hash_password_b64("hunter2");
814
815 assert_eq!(
816 created_hash,
817 Some(receive_hash),
818 "receive's password hash must match the one `bw send create` stored"
819 );
820 }
821
822 #[test]
823 fn hash_password_b64_is_salted_with_the_send_key() {
824 let a = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
827 let b = SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
828 assert_ne!(
829 a.hash_password_b64("hunter2"),
830 b.hash_password_b64("hunter2")
831 );
832 }
833
834 #[test]
835 fn from_url_b64_accepts_padded_and_unpadded() {
836 let unpadded = SendAccessKey::from_url_b64(URL_KEY).expect("unpadded parses");
839 let padded =
840 SendAccessKey::from_url_b64(&format!("{URL_KEY}==")).expect("padded parses");
841 assert_eq!(
842 unpadded.hash_password_b64("p"),
843 padded.hash_password_b64("p"),
844 "padded and unpadded forms must derive the same key"
845 );
846 }
847
848 #[test]
849 fn from_url_b64_rejects_invalid_base64() {
850 assert!(matches!(
851 SendAccessKey::from_url_b64("not valid base64!"),
852 Err(SendAccessKeyError::InvalidEncoding)
853 ));
854 }
855
856 #[test]
857 fn from_url_b64_rejects_wrong_length() {
858 for bad in ["AAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"] {
861 assert!(
862 matches!(
863 SendAccessKey::from_url_b64(bad),
864 Err(SendAccessKeyError::InvalidLength)
865 ),
866 "expected InvalidLength for {bad:?}"
867 );
868 }
869 assert!(matches!(
870 SendAccessKey::from_url_b64(""),
871 Err(SendAccessKeyError::InvalidLength)
872 ));
873 }
874
875 #[test]
879 fn decrypt_response_tolerates_absent_fields() {
880 let response = SendAccessResponse {
881 id: None,
882 type_: None,
883 name: None,
884 text: Some(SendAccessTextResponse {
885 text: None,
886 hidden: true,
887 }),
888 file: None,
889 data: None,
890 expiration_date: None,
891 creator_identifier: None,
892 };
893
894 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
895 let view = access_key.decrypt_response(response).expect("decrypts");
896
897 assert_eq!(view.name, None);
898 assert_eq!(view.type_, None);
899 let text = view.text.expect("text block present");
900 assert_eq!(text.text, None);
901 assert!(text.hidden);
902 }
903
904 #[test]
905 fn decrypt_response_errors_on_a_key_that_does_not_match() {
906 let send = encrypt_send(text_send_view("This is a test", "Test"));
907 let response = SendAccessResponse {
908 id: None,
909 type_: Some(SendType::Text),
910 name: Some(send.name.to_string()),
911 text: None,
912 file: None,
913 data: None,
914 expiration_date: None,
915 creator_identifier: None,
916 };
917
918 let wrong_key =
920 SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
921 assert!(wrong_key.decrypt_response(response).is_err());
922 }
923
924 #[test]
925 fn decrypt_response_errors_on_a_malformed_enc_string() {
926 let response = SendAccessResponse {
927 id: None,
928 type_: Some(SendType::Text),
929 name: Some("this is not an EncString".to_owned()),
930 text: None,
931 file: None,
932 data: None,
933 expiration_date: None,
934 creator_identifier: None,
935 };
936
937 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
938 assert!(access_key.decrypt_response(response).is_err());
939 }
940
941 #[test]
945 fn send_access_view_serializes_in_camel_case() {
946 let view = crate::SendAccessView {
947 id: Some("access-id".to_owned()),
948 type_: Some(SendType::File),
949 name: Some("name".to_owned()),
950 text: None,
951 file: Some(crate::SendAccessFileView {
952 id: Some("file-id".to_owned()),
953 file_name: Some("secrets.txt".to_owned()),
954 size: Some("11".to_owned()),
955 size_name: Some("11 B".to_owned()),
956 }),
957 data: None,
958 expiration_date: None,
959 creator_identifier: None,
960 };
961
962 let json = serde_json::to_value(&view).expect("serializes");
963 assert_eq!(json["type"], serde_json::json!(1));
964 assert_eq!(json["file"]["fileName"], serde_json::json!("secrets.txt"));
965 assert_eq!(json["file"]["sizeName"], serde_json::json!("11 B"));
966 assert_eq!(json["creatorIdentifier"], serde_json::Value::Null);
967 }
968 }
969}