1use bitwarden_api_api::{apis::ApiClient, models};
2use bitwarden_core::ApiError;
3use bitwarden_crypto::{
4 CryptoError, EncString, KeyDecryptable as _, SymmetricCryptoKey, derive_shareable_key,
5};
6use bitwarden_encoding::{B64, B64Url};
7use bitwarden_error::bitwarden_error;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11#[cfg(feature = "wasm")]
12use tsify::Tsify;
13#[cfg(feature = "wasm")]
14use wasm_bindgen::prelude::*;
15use zeroize::Zeroizing;
16
17use crate::{SendParseError, SendType, send::SEND_ITERATIONS, send_client::SendClient};
18
19const SEND_KEY_LEN: usize = 16;
22
23#[derive(Debug, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
31pub struct SendAccessResponse {
32 pub id: Option<String>,
34 #[serde(rename = "type")]
36 pub type_: Option<SendType>,
37 pub name: Option<String>,
39 pub text: Option<SendAccessTextResponse>,
41 pub file: Option<SendAccessFileResponse>,
43 pub expiration_date: Option<DateTime<Utc>>,
45 pub creator_identifier: Option<String>,
47}
48
49#[derive(Debug, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
53pub struct SendAccessTextResponse {
54 pub text: Option<String>,
56 pub hidden: bool,
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 SendAccessFileResponse {
65 pub id: Option<String>,
67 pub file_name: Option<String>,
69 pub size: Option<String>,
71 pub size_name: Option<String>,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
79pub struct SendFileDownloadData {
80 pub id: Option<String>,
82 pub url: Option<String>,
84}
85
86#[derive(Debug, Serialize, Deserialize, PartialEq)]
100#[serde(rename_all = "camelCase")]
101#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
102pub struct SendAccessView {
103 pub id: Option<String>,
105 #[serde(rename = "type")]
107 pub type_: Option<SendType>,
108 pub name: Option<String>,
111 pub text: Option<SendAccessTextView>,
113 pub file: Option<SendAccessFileView>,
115 pub expiration_date: Option<DateTime<Utc>>,
117 pub creator_identifier: Option<String>,
119}
120
121#[derive(Debug, Serialize, Deserialize, PartialEq)]
123#[serde(rename_all = "camelCase")]
124#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
125pub struct SendAccessTextView {
126 pub text: Option<String>,
128 pub hidden: bool,
130}
131
132#[derive(Debug, Serialize, Deserialize, PartialEq)]
134#[serde(rename_all = "camelCase")]
135#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
136pub struct SendAccessFileView {
137 pub id: Option<String>,
139 pub file_name: Option<String>,
141 pub size: Option<String>,
143 pub size_name: Option<String>,
145}
146
147#[bitwarden_error(flat)]
152#[derive(Debug, Error)]
153pub enum SendAccessKeyError {
154 #[error("The send key is not valid url-safe base64")]
156 InvalidEncoding,
157 #[error("The send key must be {SEND_KEY_LEN} bytes")]
159 InvalidLength,
160}
161
162#[bitwarden_error(flat)]
165#[derive(Debug, Error)]
166pub enum SendAccessDecryptError {
167 #[error(transparent)]
170 Crypto(#[from] CryptoError),
171}
172
173pub struct SendAccessKey {
183 secret: Zeroizing<[u8; SEND_KEY_LEN]>,
186 key: SymmetricCryptoKey,
188}
189
190impl SendAccessKey {
191 pub fn from_url_b64(key_b64: &str) -> Result<Self, SendAccessKeyError> {
200 let decoded = Zeroizing::new(
203 B64Url::try_from(key_b64)
204 .map_err(|_| SendAccessKeyError::InvalidEncoding)?
205 .into_bytes(),
206 );
207 if decoded.len() != SEND_KEY_LEN {
208 return Err(SendAccessKeyError::InvalidLength);
209 }
210 let mut secret = Zeroizing::new([0u8; SEND_KEY_LEN]);
211 secret.copy_from_slice(&decoded);
212
213 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_shareable_key(
214 secret.clone(),
215 "send",
216 Some("send"),
217 ));
218
219 Ok(Self { secret, key })
220 }
221
222 pub fn hash_password_b64(&self, password: &str) -> String {
229 let hashed =
230 bitwarden_crypto::pbkdf2(password.as_bytes(), self.secret.as_slice(), SEND_ITERATIONS);
231 B64::from(hashed.as_slice()).to_string()
232 }
233
234 pub fn decrypt_response(
237 &self,
238 response: SendAccessResponse,
239 ) -> Result<SendAccessView, SendAccessDecryptError> {
240 let text = match response.text {
241 Some(t) => Some(SendAccessTextView {
242 text: self.decrypt_optional(t.text)?,
243 hidden: t.hidden,
244 }),
245 None => None,
246 };
247 let file = match response.file {
248 Some(f) => Some(SendAccessFileView {
249 id: f.id,
250 file_name: self.decrypt_optional(f.file_name)?,
251 size: f.size,
252 size_name: f.size_name,
253 }),
254 None => None,
255 };
256
257 Ok(SendAccessView {
258 id: response.id,
259 type_: response.type_,
260 name: self.decrypt_optional(response.name)?,
261 text,
262 file,
263 expiration_date: response.expiration_date,
264 creator_identifier: response.creator_identifier,
265 })
266 }
267
268 pub fn decrypt_file_buffer(&self, buffer: &[u8]) -> Result<Vec<u8>, SendAccessDecryptError> {
272 Ok(EncString::from_buffer(buffer)?.decrypt_with_key(&self.key)?)
273 }
274
275 fn decrypt_optional(
278 &self,
279 value: Option<String>,
280 ) -> Result<Option<String>, SendAccessDecryptError> {
281 match value {
282 Some(s) => Ok(Some(s.parse::<EncString>()?.decrypt_with_key(&self.key)?)),
283 None => Ok(None),
284 }
285 }
286}
287
288#[bitwarden_error(flat)]
290#[derive(Debug, Error)]
291pub enum AccessSendError {
292 #[error(transparent)]
294 Api(#[from] ApiError),
295 #[error(transparent)]
299 Parse(#[from] SendParseError),
300}
301
302#[bitwarden_error(flat)]
304#[derive(Debug, Error)]
305pub enum GetFileDownloadDataError {
306 #[error(transparent)]
308 Api(#[from] ApiError),
309}
310
311async fn access_send(
314 api_client: &ApiClient,
315 access_token: &str,
316) -> Result<SendAccessResponse, AccessSendError> {
317 let resp = api_client
318 .sends_api()
319 .access_using_auth(access_token)
320 .await?;
321 Ok(resp.try_into()?)
322}
323
324async fn get_file_download_data(
325 api_client: &ApiClient,
326 file_id: &str,
327 access_token: &str,
328) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
329 let resp = api_client
330 .sends_api()
331 .get_send_file_download_data_using_auth(file_id, access_token)
332 .await?;
333 Ok(resp.into())
334}
335
336impl TryFrom<models::SendAccessResponseModel> for SendAccessResponse {
339 type Error = SendParseError;
340
341 fn try_from(r: models::SendAccessResponseModel) -> Result<Self, Self::Error> {
342 Ok(SendAccessResponse {
343 id: r.id,
344 type_: r.r#type.map(SendType::try_from).transpose()?,
345 name: r.name,
346 text: r.text.map(|t| SendAccessTextResponse {
347 text: t.text,
348 hidden: t.hidden.unwrap_or(false),
349 }),
350 file: r.file.map(|f| SendAccessFileResponse {
351 id: f.id,
352 file_name: f.file_name,
353 size: f.size,
354 size_name: f.size_name,
355 }),
356 expiration_date: r.expiration_date.map(|s| s.parse()).transpose()?,
357 creator_identifier: r.creator_identifier,
358 })
359 }
360}
361
362impl From<models::SendFileDownloadDataResponseModel> for SendFileDownloadData {
363 fn from(r: models::SendFileDownloadDataResponseModel) -> Self {
364 SendFileDownloadData {
365 id: r.id,
366 url: r.url,
367 }
368 }
369}
370
371#[cfg_attr(feature = "wasm", wasm_bindgen)]
374impl SendClient {
375 pub async fn access_send(
379 &self,
380 access_token: String,
381 ) -> Result<SendAccessResponse, AccessSendError> {
382 let config = self.client.internal.get_api_configurations();
383 access_send(&config.api_client, &access_token).await
384 }
385
386 pub async fn get_file_download_data(
388 &self,
389 access_token: String,
390 file_id: String,
391 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
392 let config = self.client.internal.get_api_configurations();
393 get_file_download_data(&config.api_client, &file_id, &access_token).await
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use bitwarden_api_api::{
400 apis::ApiClient,
401 models::{
402 SendAccessResponseModel, SendFileDownloadDataResponseModel, SendFileModel,
403 SendTextModel, SendType,
404 },
405 };
406
407 use super::*;
408
409 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
410 const FILE_ID: &str = "file-id-abc";
411 const ACCESS_TOKEN: &str = "send-access-token";
412
413 #[tokio::test]
416 async fn test_access_send_text() {
417 let api_client = ApiClient::new_mocked(|mock| {
418 mock.sends_api
419 .expect_access_using_auth()
420 .returning(|token| {
421 assert_eq!(token, ACCESS_TOKEN);
422 Ok(SendAccessResponseModel {
423 object: Some("send-access".to_string()),
424 id: Some(SEND_ID.to_string()),
425 r#type: Some(SendType::Text),
426 auth_type: None,
427 name: Some("encrypted-name".to_string()),
428 file: None,
429 text: Some(Box::new(SendTextModel {
430 text: Some("encrypted_send_text".to_string()),
431 hidden: Some(true),
432 })),
433 data: None,
434 expiration_date: Some("2025-01-10T00:00:00Z".to_string()),
435 creator_identifier: Some("[email protected]".to_string()),
436 })
437 })
438 .once();
439 });
440
441 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
442
443 assert_eq!(result.id, Some(SEND_ID.to_string()));
444 assert_eq!(result.type_, Some(crate::SendType::Text));
445 assert_eq!(result.name, Some("encrypted-name".to_string()));
446 assert!(result.file.is_none());
447 let text = result.text.expect("text variant should be populated");
448 assert_eq!(text.text, Some("encrypted_send_text".to_string()));
449 assert!(text.hidden);
450 assert_eq!(
451 result.expiration_date,
452 Some("2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap())
453 );
454 assert_eq!(
455 result.creator_identifier,
456 Some("[email protected]".to_string())
457 );
458 }
459
460 #[tokio::test]
461 async fn test_access_send_file() {
462 let api_client = ApiClient::new_mocked(|mock| {
463 mock.sends_api
464 .expect_access_using_auth()
465 .returning(|token| {
466 assert_eq!(token, ACCESS_TOKEN);
467 Ok(SendAccessResponseModel {
468 object: Some("send-access".to_string()),
469 id: Some(SEND_ID.to_string()),
470 r#type: Some(SendType::File),
471 auth_type: None,
472 name: Some("encrypted-name".to_string()),
473 file: Some(Box::new(SendFileModel {
474 id: Some(FILE_ID.to_string()),
475 file_name: Some("encrypted-file-name".to_string()),
476 size: Some("4200".to_string()),
477 size_name: Some("4.2 KB".to_string()),
478 })),
479 text: None,
480 data: None,
481 expiration_date: None,
482 creator_identifier: None,
483 })
484 })
485 .once();
486 });
487
488 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
489
490 assert_eq!(result.id, Some(SEND_ID.to_string()));
491 assert_eq!(result.type_, Some(crate::SendType::File));
492 assert_eq!(result.name, Some("encrypted-name".to_string()));
493 assert!(result.text.is_none());
494 let file = result.file.expect("file variant should be populated");
495 assert_eq!(file.id, Some(FILE_ID.to_string()));
496 assert_eq!(file.file_name, Some("encrypted-file-name".to_string()));
497 assert_eq!(file.size, Some("4200".to_string()));
498 assert_eq!(file.size_name, Some("4.2 KB".to_string()));
499 assert_eq!(result.expiration_date, None);
500 assert_eq!(result.creator_identifier, None);
501 }
502
503 #[tokio::test]
504 async fn test_access_send_http_error() {
505 let api_client = ApiClient::new_mocked(|mock| {
506 mock.sends_api
507 .expect_access_using_auth()
508 .returning(|_token| {
509 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
510 "Simulated error",
511 )))
512 })
513 .once();
514 });
515
516 let result = access_send(&api_client, ACCESS_TOKEN).await;
517
518 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
519 }
520
521 #[tokio::test]
524 async fn test_get_file_download_data() {
525 let api_client = ApiClient::new_mocked(|mock| {
526 mock.sends_api
527 .expect_get_send_file_download_data_using_auth()
528 .returning(|file_id, token| {
529 assert_eq!(file_id, FILE_ID);
530 assert_eq!(token, ACCESS_TOKEN);
531 Ok(SendFileDownloadDataResponseModel {
532 object: Some("send-fileDownload".to_string()),
533 id: Some(FILE_ID.to_string()),
534 url: Some("https://example.com/download".to_string()),
535 })
536 })
537 .once();
538 });
539
540 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN)
541 .await
542 .unwrap();
543
544 assert_eq!(result.id, Some(FILE_ID.to_string()));
545 assert_eq!(result.url, Some("https://example.com/download".to_string()));
546 }
547
548 #[tokio::test]
549 async fn test_get_file_download_data_http_error() {
550 let api_client = ApiClient::new_mocked(|mock| {
551 mock.sends_api
552 .expect_get_send_file_download_data_using_auth()
553 .returning(|_file_id, _token| {
554 Err(bitwarden_api_api::ApiError::Io(std::io::Error::other(
555 "Simulated error",
556 )))
557 })
558 .once();
559 });
560
561 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
562
563 assert!(matches!(
564 result.unwrap_err(),
565 GetFileDownloadDataError::Api(_)
566 ));
567 }
568
569 mod send_access_key {
572 use bitwarden_core::key_management::create_test_crypto_with_user_key;
573 use bitwarden_crypto::{OctetStreamBytes, PrimitiveEncryptable as _, SymmetricCryptoKey};
574
575 use crate::{
576 Send, SendAccessFileResponse, SendAccessKey, SendAccessKeyError, SendAccessResponse,
577 SendAccessTextResponse, SendAuthType, SendFileView, SendTextView, SendType, SendView,
578 access::SEND_KEY_LEN,
579 };
580
581 const URL_KEY: &str = "Pgui0FK85cNhBGWHAlBHBw";
584 const USER_KEY: &str = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
585
586 fn user_key() -> SymmetricCryptoKey {
587 USER_KEY
588 .to_string()
589 .try_into()
590 .expect("valid test user key")
591 }
592
593 fn encrypt_send(view: SendView) -> Send {
597 create_test_crypto_with_user_key(user_key())
598 .encrypt(view)
599 .expect("send encrypts")
600 }
601
602 fn text_send_view(text: &str, name: &str) -> SendView {
603 SendView {
604 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
605 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
606 name: name.to_owned(),
607 notes: None,
608 key: Some(URL_KEY.to_owned()),
609 new_password: None,
610 has_password: false,
611 r#type: SendType::Text,
612 file: None,
613 text: Some(SendTextView {
614 text: Some(text.to_owned()),
615 hidden: false,
616 }),
617 max_access_count: None,
618 access_count: 0,
619 disabled: false,
620 hide_email: false,
621 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
622 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
623 expiration_date: None,
624 emails: Vec::new(),
625 auth_type: crate::AuthType::None,
626 }
627 }
628
629 #[test]
636 fn decrypts_ciphertext_produced_by_the_authenticated_path() {
637 let send = encrypt_send(text_send_view("This is a test", "Test"));
638
639 let response = SendAccessResponse {
640 id: Some("access-id".to_owned()),
641 type_: Some(SendType::Text),
642 name: Some(send.name.to_string()),
643 text: Some(SendAccessTextResponse {
644 text: send
645 .text
646 .as_ref()
647 .and_then(|t| t.text.as_ref())
648 .map(|t| t.to_string()),
649 hidden: false,
650 }),
651 file: None,
652 expiration_date: None,
653 creator_identifier: None,
654 };
655
656 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
657 let view = access_key.decrypt_response(response).expect("decrypts");
658
659 assert_eq!(view.name.as_deref(), Some("Test"));
660 assert_eq!(
661 view.text.expect("text present").text.as_deref(),
662 Some("This is a test")
663 );
664 }
665
666 #[test]
667 fn decrypts_file_name_produced_by_the_authenticated_path() {
668 let mut view = text_send_view("unused", "File Send");
669 view.r#type = SendType::File;
670 view.text = None;
671 view.file = Some(SendFileView {
672 id: Some("file-id".to_owned()),
673 file_name: "secrets.txt".to_owned(),
674 size: Some("11".to_owned()),
675 size_name: Some("11 B".to_owned()),
676 });
677 let send = encrypt_send(view);
678 let file = send.file.expect("file present");
679
680 let response = SendAccessResponse {
681 id: Some("access-id".to_owned()),
682 type_: Some(SendType::File),
683 name: Some(send.name.to_string()),
684 text: None,
685 file: Some(SendAccessFileResponse {
686 id: file.id.clone(),
687 file_name: Some(file.file_name.to_string()),
688 size: file.size.clone(),
689 size_name: file.size_name.clone(),
690 }),
691 expiration_date: None,
692 creator_identifier: None,
693 };
694
695 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
696 let view = access_key.decrypt_response(response).expect("decrypts");
697
698 let decrypted_file = view.file.expect("file present");
699 assert_eq!(decrypted_file.file_name.as_deref(), Some("secrets.txt"));
700 assert_eq!(decrypted_file.size.as_deref(), Some("11"));
701 assert_eq!(decrypted_file.id.as_deref(), Some("file-id"));
702 assert_eq!(view.name.as_deref(), Some("File Send"));
703 }
704
705 #[test]
710 fn decrypt_file_buffer_round_trips_with_the_authenticated_path() {
711 let plaintext = b"file send contents".to_vec();
712
713 let crypto = create_test_crypto_with_user_key(user_key());
714 let mut ctx = crypto.context();
715 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
716 .expect("url key decodes")
717 .into_bytes();
718 let send_key = Send::derive_shareable_key(&mut ctx, &raw_key).expect("key derives");
719 let encrypted = OctetStreamBytes::from(plaintext.clone())
720 .encrypt(&mut ctx, send_key)
721 .expect("buffer encrypts")
722 .to_buffer()
723 .expect("buffer serializes");
724
725 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
726 let decrypted = access_key
727 .decrypt_file_buffer(&encrypted)
728 .expect("buffer decrypts");
729
730 assert_eq!(decrypted, plaintext);
731 }
732
733 #[test]
738 fn hash_password_b64_matches_send_auth_type_auth_data() {
739 let raw_key = bitwarden_encoding::B64Url::try_from(URL_KEY)
740 .expect("url key decodes")
741 .into_bytes();
742
743 let (created_hash, emails) = SendAuthType::Password {
744 password: "hunter2".to_owned(),
745 }
746 .auth_data(&raw_key);
747 assert_eq!(emails, None);
748
749 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
750 let receive_hash = access_key.hash_password_b64("hunter2");
751
752 assert_eq!(
753 created_hash,
754 Some(receive_hash),
755 "receive's password hash must match the one `bw send create` stored"
756 );
757 }
758
759 #[test]
760 fn hash_password_b64_is_salted_with_the_send_key() {
761 let a = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
764 let b = SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
765 assert_ne!(
766 a.hash_password_b64("hunter2"),
767 b.hash_password_b64("hunter2")
768 );
769 }
770
771 #[test]
772 fn from_url_b64_accepts_padded_and_unpadded() {
773 let unpadded = SendAccessKey::from_url_b64(URL_KEY).expect("unpadded parses");
776 let padded =
777 SendAccessKey::from_url_b64(&format!("{URL_KEY}==")).expect("padded parses");
778 assert_eq!(
779 unpadded.hash_password_b64("p"),
780 padded.hash_password_b64("p"),
781 "padded and unpadded forms must derive the same key"
782 );
783 }
784
785 #[test]
786 fn from_url_b64_rejects_invalid_base64() {
787 assert!(matches!(
788 SendAccessKey::from_url_b64("not valid base64!"),
789 Err(SendAccessKeyError::InvalidEncoding)
790 ));
791 }
792
793 #[test]
794 fn from_url_b64_rejects_wrong_length() {
795 for bad in ["AAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"] {
798 assert!(
799 matches!(
800 SendAccessKey::from_url_b64(bad),
801 Err(SendAccessKeyError::InvalidLength)
802 ),
803 "expected InvalidLength for {bad:?}"
804 );
805 }
806 assert!(matches!(
807 SendAccessKey::from_url_b64(""),
808 Err(SendAccessKeyError::InvalidLength)
809 ));
810 }
811
812 #[test]
816 fn decrypt_response_tolerates_absent_fields() {
817 let response = SendAccessResponse {
818 id: None,
819 type_: None,
820 name: None,
821 text: Some(SendAccessTextResponse {
822 text: None,
823 hidden: true,
824 }),
825 file: None,
826 expiration_date: None,
827 creator_identifier: None,
828 };
829
830 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
831 let view = access_key.decrypt_response(response).expect("decrypts");
832
833 assert_eq!(view.name, None);
834 assert_eq!(view.type_, None);
835 let text = view.text.expect("text block present");
836 assert_eq!(text.text, None);
837 assert!(text.hidden);
838 }
839
840 #[test]
841 fn decrypt_response_errors_on_a_key_that_does_not_match() {
842 let send = encrypt_send(text_send_view("This is a test", "Test"));
843 let response = SendAccessResponse {
844 id: None,
845 type_: Some(SendType::Text),
846 name: Some(send.name.to_string()),
847 text: None,
848 file: None,
849 expiration_date: None,
850 creator_identifier: None,
851 };
852
853 let wrong_key =
855 SendAccessKey::from_url_b64("AAAAAAAAAAAAAAAAAAAAAA").expect("key parses");
856 assert!(wrong_key.decrypt_response(response).is_err());
857 }
858
859 #[test]
860 fn decrypt_response_errors_on_a_malformed_enc_string() {
861 let response = SendAccessResponse {
862 id: None,
863 type_: Some(SendType::Text),
864 name: Some("this is not an EncString".to_owned()),
865 text: None,
866 file: None,
867 expiration_date: None,
868 creator_identifier: None,
869 };
870
871 let access_key = SendAccessKey::from_url_b64(URL_KEY).expect("key parses");
872 assert!(access_key.decrypt_response(response).is_err());
873 }
874
875 #[test]
876 fn send_key_len_matches_the_generated_send_key_length() {
877 let generated = bitwarden_crypto::generate_random_bytes::<[u8; SEND_KEY_LEN]>();
880 assert_eq!(generated.len(), SEND_KEY_LEN);
881 }
882
883 #[test]
887 fn send_access_view_serializes_in_camel_case() {
888 let view = crate::SendAccessView {
889 id: Some("access-id".to_owned()),
890 type_: Some(SendType::File),
891 name: Some("name".to_owned()),
892 text: None,
893 file: Some(crate::SendAccessFileView {
894 id: Some("file-id".to_owned()),
895 file_name: Some("secrets.txt".to_owned()),
896 size: Some("11".to_owned()),
897 size_name: Some("11 B".to_owned()),
898 }),
899 expiration_date: None,
900 creator_identifier: None,
901 };
902
903 let json = serde_json::to_value(&view).expect("serializes");
904 assert_eq!(json["type"], serde_json::json!(1));
905 assert_eq!(json["file"]["fileName"], serde_json::json!("secrets.txt"));
906 assert_eq!(json["file"]["sizeName"], serde_json::json!("11 B"));
907 assert_eq!(json["creatorIdentifier"], serde_json::Value::Null);
908 }
909 }
910}