1use bitwarden_api_api::models::{
2 SendDataModel, SendFileModel, SendItemMetadataModel, SendResponseModel, SendTextModel,
3 SendWithIdRequestModel,
4};
5use bitwarden_core::{
6 key_management::{KeySlotIds, SymmetricKeySlotId},
7 require,
8};
9use bitwarden_crypto::{
10 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
11 OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes,
12};
13use bitwarden_encoding::{B64, B64Url};
14use bitwarden_uuid::uuid_newtype;
15use bitwarden_vault::{CipherId, CipherView};
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use serde_repr::{Deserialize_repr, Serialize_repr};
19use thiserror::Error;
20#[cfg(feature = "wasm")]
21use wasm_bindgen::prelude::*;
22use zeroize::Zeroizing;
23
24use crate::{SendParseError, access::SEND_KEY_LEN, error::SendItemDeserializationFailureError};
25pub const SEND_ITERATIONS: u32 = 100_000;
26pub const DEFAULT_SEND_ENCRYPTION: SendEncryptionType = SendEncryptionType::V1;
27
28uuid_newtype!(pub SendId);
29
30#[derive(Debug, Error)]
32#[error("Email authentication requires at least one email address")]
33pub struct EmptyEmailListError;
34
35#[derive(Serialize, Deserialize, Debug, Clone)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
39#[bitwarden_ffi::wasm_record]
40pub struct SendFile {
41 pub id: Option<String>,
43 pub file_name: EncString,
45 pub size: Option<String>,
47 pub size_name: Option<String>,
49}
50
51#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
55#[bitwarden_ffi::wasm_record]
56pub struct SendFileView {
57 pub id: Option<String>,
59 pub file_name: String,
61 pub size: Option<String>,
63 pub size_name: Option<String>,
65}
66
67#[derive(Serialize, Deserialize, Debug, Clone)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
71#[bitwarden_ffi::wasm_record]
72pub struct SendText {
73 pub text: Option<EncString>,
74 pub hidden: bool,
75}
76
77#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
79#[serde(rename_all = "camelCase", deny_unknown_fields)]
80#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
81#[bitwarden_ffi::wasm_record]
82pub struct SendItemView {
83 pub data: CipherView,
85}
86
87#[derive(Serialize, Deserialize, Debug, Clone)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
91#[bitwarden_ffi::wasm_record]
92pub struct SendItem {
93 pub encryption_version: SendEncryptionType,
94 pub data: String,
96 pub metadata: SendItemMetadata,
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
103#[bitwarden_ffi::wasm_record]
104pub struct SendItemMetadata {
105 pub item_id: CipherId,
107}
108
109#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
111#[serde(rename_all = "camelCase", deny_unknown_fields)]
112#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
113#[bitwarden_ffi::wasm_record]
114pub struct SendTextView {
115 pub text: Option<String>,
117 pub hidden: bool,
119}
120
121#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
123#[repr(u8)]
124#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
125#[bitwarden_ffi::wasm_object]
126pub enum SendType {
127 Text = 0,
129 File = 1,
131 Item = 2,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
137#[repr(u8)]
138#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
139#[bitwarden_ffi::wasm_object]
140pub enum AuthType {
141 Email = 0,
143
144 Password = 1,
146
147 None = 2,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
153#[repr(u8)]
154#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
155#[bitwarden_ffi::wasm_object]
156pub enum SendEncryptionType {
157 V1 = 1,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(tag = "type", rename_all = "camelCase")]
165#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
166#[bitwarden_ffi::wasm_record]
167pub enum SendAuthType {
168 None,
170 Password {
173 password: String,
175 },
176 HashedPassword {
182 #[serde(rename = "keyB64")]
184 key_b64: String,
185 },
186 Emails {
188 emails: Vec<String>,
190 },
191}
192
193impl SendAuthType {
194 pub fn from_plaintext_password(password: String) -> Self {
197 SendAuthType::Password { password }
198 }
199
200 pub fn from_hashed_password(key_b64: String) -> Self {
204 SendAuthType::HashedPassword { key_b64 }
205 }
206
207 pub fn auth_type(&self) -> AuthType {
209 match self {
210 SendAuthType::None => AuthType::None,
211 SendAuthType::Password { .. } | SendAuthType::HashedPassword { .. } => {
212 AuthType::Password
213 }
214 SendAuthType::Emails { .. } => AuthType::Email,
215 }
216 }
217
218 pub(crate) fn validate(&self) -> Result<(), EmptyEmailListError> {
221 if let SendAuthType::Emails { emails } = self
222 && emails.is_empty()
223 {
224 return Err(EmptyEmailListError);
225 }
226 Ok(())
227 }
228
229 pub(crate) fn auth_data(&self, k: &[u8]) -> (Option<String>, Option<String>) {
233 match self {
234 SendAuthType::Password { password } => {
235 let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), k, SEND_ITERATIONS);
236 (Some(B64::from(hashed.as_slice()).to_string()), None)
237 }
238 SendAuthType::HashedPassword { key_b64 } => (Some(key_b64.clone()), None),
239 SendAuthType::Emails { emails } => {
240 let emails_str = if emails.is_empty() {
241 None
242 } else {
243 Some(emails.join(","))
244 };
245 (None, emails_str)
246 }
247 SendAuthType::None => (None, None),
248 }
249 }
250}
251
252#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
254#[bitwarden_ffi::wasm_record]
255pub enum SendViewType {
256 File(SendFileView),
258 Text(SendTextView),
260 Item(Box<SendItemView>),
262}
263
264type SendApiModels = (
266 bitwarden_api_api::models::SendType,
267 Option<Box<bitwarden_api_api::models::SendFileModel>>,
268 Option<Box<bitwarden_api_api::models::SendTextModel>>,
269 Option<Box<bitwarden_api_api::models::SendDataModel>>,
270);
271
272impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendApiModels> for SendViewType {
273 fn encrypt_composite(
274 &self,
275 ctx: &mut KeyStoreContext<KeySlotIds>,
276 key: SymmetricKeySlotId,
277 ) -> Result<SendApiModels, CryptoError> {
278 match self {
279 SendViewType::File(f) => Ok((
280 bitwarden_api_api::models::SendType::File,
281 Some(Box::new(bitwarden_api_api::models::SendFileModel {
282 id: f.id.clone(),
283 file_name: Some(f.file_name.encrypt(ctx, key)?.to_string()),
284 size: f.size.clone(),
285 size_name: f.size_name.clone(),
286 })),
287 None,
288 None,
289 )),
290 SendViewType::Text(t) => Ok((
291 bitwarden_api_api::models::SendType::Text,
292 None,
293 Some(Box::new(bitwarden_api_api::models::SendTextModel {
294 text: t
295 .text
296 .as_ref()
297 .map(|txt| txt.encrypt(ctx, key))
298 .transpose()?
299 .map(|e| e.to_string()),
300 hidden: Some(t.hidden),
301 })),
302 None,
303 )),
304 SendViewType::Item(i) => {
305 let encrypted = i.encrypt_composite(ctx, key)?;
306 Ok((
307 bitwarden_api_api::models::SendType::Item,
308 None,
309 None,
310 Some(Box::new(encrypted.into())),
311 ))
312 }
313 }
314 }
315}
316
317#[allow(missing_docs)]
318#[derive(Serialize, Deserialize, Debug, Clone)]
319#[serde(rename_all = "camelCase", deny_unknown_fields)]
320#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
321#[bitwarden_ffi::wasm_record]
322pub struct Send {
323 pub id: Option<SendId>,
324 pub access_id: Option<String>,
325
326 pub name: EncString,
327 pub notes: Option<EncString>,
328 pub key: EncString,
329 pub password: Option<String>,
330
331 pub r#type: SendType,
332 pub file: Option<SendFile>,
333 pub text: Option<SendText>,
334 pub data: Option<SendItem>,
335
336 pub max_access_count: Option<u32>,
337 pub access_count: u32,
338 pub disabled: bool,
339 pub hide_email: bool,
340
341 pub revision_date: DateTime<Utc>,
342 pub deletion_date: DateTime<Utc>,
343 pub expiration_date: Option<DateTime<Utc>>,
344
345 pub emails: Option<String>,
350 pub auth_type: AuthType,
351}
352
353bitwarden_state::register_repository_item!(SendId => Send, "Send");
354
355impl From<Send> for SendWithIdRequestModel {
356 fn from(send: Send) -> Self {
357 let file_length = send.file.as_ref().and_then(|file| {
358 file.size
359 .as_deref()
360 .and_then(|size| size.parse::<i64>().ok())
361 });
362
363 SendWithIdRequestModel {
364 r#type: Some(send.r#type.into()),
365 auth_type: Some(send.auth_type.into()),
366 file_length,
367 name: Some(send.name.to_string()),
368 notes: send.notes.map(|notes| notes.to_string()),
369 key: send.key.to_string(),
370 max_access_count: send.max_access_count.map(|count| count as i32),
371 expiration_date: send.expiration_date.map(|date| date.to_rfc3339()),
372 deletion_date: send.deletion_date.to_rfc3339(),
373 file: send.file.map(|file| Box::new(file.into())),
374 text: send.text.map(|text| Box::new(text.into())),
375 data: send.data.map(|data| Box::new(data.into())),
376 password: send.password,
377 emails: send.emails,
378 disabled: send.disabled,
379 hide_email: Some(send.hide_email),
380 id: send
381 .id
382 .expect("SendWithIdRequestModel conversion requires send id")
383 .into(),
384 }
385 }
386}
387
388#[allow(missing_docs)]
389#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
390#[serde(rename_all = "camelCase", deny_unknown_fields)]
391#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
392#[bitwarden_ffi::wasm_record]
393pub struct SendView {
394 pub id: Option<SendId>,
395 pub access_id: Option<String>,
396
397 pub name: String,
398 pub notes: Option<String>,
399 pub key: Option<String>,
401 pub new_password: Option<String>,
405 pub has_password: bool,
408
409 pub r#type: SendType,
410 pub file: Option<SendFileView>,
411 pub text: Option<SendTextView>,
412 pub data: Option<SendItemView>,
413
414 pub max_access_count: Option<u32>,
415 pub access_count: u32,
416 pub disabled: bool,
417 pub hide_email: bool,
418
419 pub revision_date: DateTime<Utc>,
420 pub deletion_date: DateTime<Utc>,
421 pub expiration_date: Option<DateTime<Utc>>,
422
423 pub emails: Vec<String>,
428 pub auth_type: AuthType,
429}
430
431#[allow(missing_docs)]
432#[derive(Serialize, Deserialize, Debug)]
433#[serde(rename_all = "camelCase", deny_unknown_fields)]
434#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
435#[bitwarden_ffi::wasm_record]
436pub struct SendListView {
437 pub id: Option<SendId>,
438 pub access_id: Option<String>,
439
440 pub name: String,
441
442 pub r#type: SendType,
443 pub disabled: bool,
444
445 pub revision_date: DateTime<Utc>,
446 pub deletion_date: DateTime<Utc>,
447 pub expiration_date: Option<DateTime<Utc>>,
448
449 pub auth_type: AuthType,
450}
451
452impl Send {
453 #[allow(missing_docs)]
454 pub fn get_key(
455 ctx: &mut KeyStoreContext<KeySlotIds>,
456 send_key: &EncString,
457 enc_key: SymmetricKeySlotId,
458 ) -> Result<SymmetricKeySlotId, CryptoError> {
459 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
460 Self::derive_shareable_key(ctx, &key)
461 }
462
463 pub(crate) fn derive_shareable_key(
464 ctx: &mut KeyStoreContext<KeySlotIds>,
465 key: &[u8],
466 ) -> Result<SymmetricKeySlotId, CryptoError> {
467 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
468 ctx.derive_shareable_key(key, "send", Some("send"))
469 }
470}
471
472impl IdentifyKey<SymmetricKeySlotId> for Send {
473 fn key_identifier(&self) -> SymmetricKeySlotId {
474 SymmetricKeySlotId::User
475 }
476}
477
478impl IdentifyKey<SymmetricKeySlotId> for SendView {
479 fn key_identifier(&self) -> SymmetricKeySlotId {
480 SymmetricKeySlotId::User
481 }
482}
483
484impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendTextView> for SendText {
485 fn decrypt(
486 &self,
487 ctx: &mut KeyStoreContext<KeySlotIds>,
488 key: SymmetricKeySlotId,
489 ) -> Result<SendTextView, CryptoError> {
490 Ok(SendTextView {
491 text: self.text.decrypt(ctx, key)?,
492 hidden: self.hidden,
493 })
494 }
495}
496
497impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendText> for SendTextView {
498 fn encrypt_composite(
499 &self,
500 ctx: &mut KeyStoreContext<KeySlotIds>,
501 key: SymmetricKeySlotId,
502 ) -> Result<SendText, CryptoError> {
503 Ok(SendText {
504 text: self.text.encrypt(ctx, key)?,
505 hidden: self.hidden,
506 })
507 }
508}
509
510impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendFileView> for SendFile {
511 fn decrypt(
512 &self,
513 ctx: &mut KeyStoreContext<KeySlotIds>,
514 key: SymmetricKeySlotId,
515 ) -> Result<SendFileView, CryptoError> {
516 Ok(SendFileView {
517 id: self.id.clone(),
518 file_name: self.file_name.decrypt(ctx, key)?,
519 size: self.size.clone(),
520 size_name: self.size_name.clone(),
521 })
522 }
523}
524
525impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendFile> for SendFileView {
526 fn encrypt_composite(
527 &self,
528 ctx: &mut KeyStoreContext<KeySlotIds>,
529 key: SymmetricKeySlotId,
530 ) -> Result<SendFile, CryptoError> {
531 Ok(SendFile {
532 id: self.id.clone(),
533 file_name: self.file_name.encrypt(ctx, key)?,
534 size: self.size.clone(),
535 size_name: self.size_name.clone(),
536 })
537 }
538}
539
540impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendItemView> for SendItem {
541 fn decrypt(
542 &self,
543 ctx: &mut KeyStoreContext<KeySlotIds>,
544 key: SymmetricKeySlotId,
545 ) -> Result<SendItemView, CryptoError> {
546 let mut data = CipherView::unseal_blob_for_item_sends(&self.data, ctx, key)?;
547 data.id = Some(self.metadata.item_id);
549 Ok(SendItemView { data })
550 }
551}
552
553impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendItem> for SendItemView {
554 fn encrypt_composite(
555 &self,
556 ctx: &mut KeyStoreContext<KeySlotIds>,
557 key: SymmetricKeySlotId,
558 ) -> Result<SendItem, CryptoError> {
559 Ok(SendItem {
560 encryption_version: DEFAULT_SEND_ENCRYPTION,
561 data: self.data.seal_blob_for_item_sends(ctx, key)?,
562 metadata: SendItemMetadata {
563 item_id: self.data.id.ok_or(CryptoError::MissingField("id"))?,
564 },
565 })
566 }
567}
568
569impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendView> for Send {
570 fn decrypt(
571 &self,
572 ctx: &mut KeyStoreContext<KeySlotIds>,
573 key: SymmetricKeySlotId,
574 ) -> Result<SendView, CryptoError> {
575 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
579 let key = Send::derive_shareable_key(ctx, &k)?;
580
581 Ok(SendView {
582 id: self.id,
583 access_id: self.access_id.clone(),
584
585 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
586 notes: self.notes.decrypt(ctx, key).ok().flatten(),
587 key: Some(B64Url::from(k).to_string()),
588 new_password: None,
589 has_password: self.password.is_some(),
590
591 r#type: self.r#type,
592 file: self.file.decrypt(ctx, key).ok().flatten(),
593 text: self.text.decrypt(ctx, key).ok().flatten(),
594 data: self.data.decrypt(ctx, key).ok().flatten(),
595
596 max_access_count: self.max_access_count,
597 access_count: self.access_count,
598 disabled: self.disabled,
599 hide_email: self.hide_email,
600
601 revision_date: self.revision_date,
602 deletion_date: self.deletion_date,
603 expiration_date: self.expiration_date,
604
605 emails: self
606 .emails
607 .as_deref()
608 .unwrap_or_default()
609 .split(',')
610 .map(|e| e.trim())
611 .filter(|e| !e.is_empty())
612 .map(String::from)
613 .collect(),
614 auth_type: self.auth_type,
615 })
616 }
617}
618
619impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendListView> for Send {
620 fn decrypt(
621 &self,
622 ctx: &mut KeyStoreContext<KeySlotIds>,
623 key: SymmetricKeySlotId,
624 ) -> Result<SendListView, CryptoError> {
625 let key = Send::get_key(ctx, &self.key, key)?;
629
630 Ok(SendListView {
631 id: self.id,
632 access_id: self.access_id.clone(),
633
634 name: self.name.decrypt(ctx, key)?,
635 r#type: self.r#type,
636
637 disabled: self.disabled,
638
639 revision_date: self.revision_date,
640 deletion_date: self.deletion_date,
641 expiration_date: self.expiration_date,
642
643 auth_type: self.auth_type,
644 })
645 }
646}
647
648impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Send> for SendView {
649 fn encrypt_composite(
650 &self,
651 ctx: &mut KeyStoreContext<KeySlotIds>,
652 key: SymmetricKeySlotId,
653 ) -> Result<Send, CryptoError> {
654 let k = match (&self.key, &self.id) {
658 (Some(k), _) => B64Url::try_from(k.as_str())
660 .map_err(|_| CryptoError::InvalidKey)?
661 .as_bytes()
662 .to_vec(),
663 (None, None) => {
665 let key = generate_random_bytes::<[u8; SEND_KEY_LEN]>();
666 key.to_vec()
667 }
668 _ => return Err(CryptoError::InvalidKey),
670 };
671 let send_key = Send::derive_shareable_key(ctx, &k)?;
672
673 Ok(Send {
674 id: self.id,
675 access_id: self.access_id.clone(),
676
677 name: self.name.encrypt(ctx, send_key)?,
678 notes: self.notes.encrypt(ctx, send_key)?,
679 key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
680 password: self.new_password.as_ref().map(|password| {
687 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
688 B64::from(password.as_slice()).to_string()
689 }),
690
691 r#type: self.r#type,
692 file: self.file.encrypt_composite(ctx, send_key)?,
693 text: self.text.encrypt_composite(ctx, send_key)?,
694 data: self.data.encrypt_composite(ctx, send_key)?,
695
696 max_access_count: self.max_access_count,
697 access_count: self.access_count,
698 disabled: self.disabled,
699 hide_email: self.hide_email,
700
701 revision_date: self.revision_date,
702 deletion_date: self.deletion_date,
703 expiration_date: self.expiration_date,
704
705 emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
706 auth_type: self.auth_type,
707 })
708 }
709}
710
711impl TryFrom<SendResponseModel> for Send {
712 type Error = SendParseError;
713
714 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
715 let auth_type = match send.auth_type {
716 Some(t) => t.try_into()?,
717 None => {
718 if send.password.is_some() {
719 AuthType::Password
720 } else if send.emails.is_some() {
721 AuthType::Email
722 } else {
723 AuthType::None
724 }
725 }
726 };
727 Ok(Send {
728 id: send.id.map(SendId::new),
729 access_id: send.access_id,
730 name: require!(send.name).parse()?,
731 notes: EncString::try_from_optional(send.notes)?,
732 key: require!(send.key).parse()?,
733 password: send.password,
734 r#type: require!(send.r#type).try_into()?,
735 file: send.file.map(|f| (*f).try_into()).transpose()?,
736 text: send.text.map(|t| (*t).try_into()).transpose()?,
737 data: send.data.map(|d| (*d).try_into()).transpose()?,
738 max_access_count: send.max_access_count.map(|s| s as u32),
739 access_count: require!(send.access_count) as u32,
740 disabled: send.disabled.unwrap_or(false),
741 hide_email: send.hide_email.unwrap_or(false),
742 revision_date: require!(send.revision_date).parse()?,
743 deletion_date: require!(send.deletion_date).parse()?,
744 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
745 emails: send.emails,
746 auth_type,
747 })
748 }
749}
750
751impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
752 type Error = bitwarden_core::MissingFieldError;
753
754 fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
755 Ok(match t {
756 bitwarden_api_api::models::SendType::Text => SendType::Text,
757 bitwarden_api_api::models::SendType::File => SendType::File,
758 bitwarden_api_api::models::SendType::Item => SendType::Item,
759 bitwarden_api_api::models::SendType::__Unknown(_) => {
760 return Err(bitwarden_core::MissingFieldError("type"));
761 }
762 })
763 }
764}
765
766impl From<SendType> for bitwarden_api_api::models::SendType {
767 fn from(t: SendType) -> Self {
768 match t {
769 SendType::Text => bitwarden_api_api::models::SendType::Text,
770 SendType::File => bitwarden_api_api::models::SendType::File,
771 SendType::Item => bitwarden_api_api::models::SendType::Item,
772 }
773 }
774}
775
776impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
777 type Error = bitwarden_core::MissingFieldError;
778
779 fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
780 Ok(match value {
781 bitwarden_api_api::models::AuthType::Email => AuthType::Email,
782 bitwarden_api_api::models::AuthType::Password => AuthType::Password,
783 bitwarden_api_api::models::AuthType::None => AuthType::None,
784 bitwarden_api_api::models::AuthType::__Unknown(_) => {
785 return Err(bitwarden_core::MissingFieldError("auth_type"));
786 }
787 })
788 }
789}
790
791impl From<AuthType> for bitwarden_api_api::models::AuthType {
792 fn from(value: AuthType) -> Self {
793 match value {
794 AuthType::Email => bitwarden_api_api::models::AuthType::Email,
795 AuthType::Password => bitwarden_api_api::models::AuthType::Password,
796 AuthType::None => bitwarden_api_api::models::AuthType::None,
797 }
798 }
799}
800
801impl From<SendFile> for SendFileModel {
802 fn from(file: SendFile) -> Self {
803 SendFileModel {
804 id: file.id,
805 file_name: Some(file.file_name.to_string()),
806 size: file.size,
807 size_name: file.size_name,
808 }
809 }
810}
811
812impl From<SendEncryptionType> for bitwarden_api_api::models::SendEncryptionType {
813 fn from(t: SendEncryptionType) -> Self {
814 match t {
815 SendEncryptionType::V1 => bitwarden_api_api::models::SendEncryptionType::V1,
816 }
817 }
818}
819
820impl TryFrom<bitwarden_api_api::models::SendEncryptionType> for SendEncryptionType {
821 type Error = bitwarden_core::MissingFieldError;
822
823 fn try_from(value: bitwarden_api_api::models::SendEncryptionType) -> Result<Self, Self::Error> {
824 Ok(match value {
825 bitwarden_api_api::models::SendEncryptionType::V1 => SendEncryptionType::V1,
826 bitwarden_api_api::models::SendEncryptionType::__Unknown(_) => {
827 return Err(bitwarden_core::MissingFieldError("encryption_version"));
828 }
829 })
830 }
831}
832
833impl From<SendText> for SendTextModel {
834 fn from(text: SendText) -> Self {
835 SendTextModel {
836 text: text.text.map(|text| text.to_string()),
837 hidden: Some(text.hidden),
838 }
839 }
840}
841
842impl TryFrom<SendFileModel> for SendFile {
843 type Error = SendParseError;
844
845 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
846 Ok(SendFile {
847 id: file.id,
848 file_name: require!(file.file_name).parse()?,
849 size: file.size.map(|v| v.to_string()),
850 size_name: file.size_name,
851 })
852 }
853}
854
855impl TryFrom<SendTextModel> for SendText {
856 type Error = SendParseError;
857
858 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
859 Ok(SendText {
860 text: EncString::try_from_optional(text.text)?,
861 hidden: text.hidden.unwrap_or(false),
862 })
863 }
864}
865
866impl TryFrom<SendDataModel> for SendItem {
867 type Error = SendParseError;
868
869 fn try_from(data: SendDataModel) -> Result<Self, Self::Error> {
870 let Some(sealed) = data.data else {
871 return Err(SendParseError::DeserializationFailure(
872 SendItemDeserializationFailureError,
873 ));
874 };
875
876 Ok(SendItem {
877 encryption_version: SendEncryptionType::try_from(
878 data.encryption_version
879 .unwrap_or(DEFAULT_SEND_ENCRYPTION.into()),
880 )?,
881 data: sealed,
882 metadata: SendItemMetadata {
883 item_id: CipherId::new(data.metadata.item_id),
884 },
885 })
886 }
887}
888
889impl From<SendItem> for SendDataModel {
890 fn from(item: SendItem) -> Self {
891 SendDataModel {
892 encryption_version: Some(item.encryption_version.into()),
893 data: Some(item.data),
894 metadata: Box::new(SendItemMetadataModel {
895 item_id: item.metadata.item_id.into(),
896 folder_name: None,
897 organization_name: None,
898 collection_names: None,
899 creation_date: None,
900 revision_date: None,
901 }),
902 }
903 }
904}
905
906#[cfg(test)]
907pub(crate) mod tests {
908 use bitwarden_api_api::models::SendItemMetadataModel;
909 use bitwarden_core::key_management::create_test_crypto_with_user_key;
910 use bitwarden_crypto::SymmetricCryptoKey;
911 use bitwarden_vault::{
912 CipherRepromptType, CipherType, FieldType, FieldView, LoginView, PasswordHistoryView,
913 };
914
915 use super::*;
916
917 const TEST_USER_KEY: &str =
918 "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
919 const TEST_SEND_KEY: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
920 const TEST_SEND_KEY_B64: &str = "Pgui0FK85cNhBGWHAlBHBw";
921 pub(crate) const TEST_ITEM_ID: &str = "5d4fbf2b-7a36-4b3c-9f2e-1a6d8c0e9b71";
922
923 pub(crate) const TEST_VECTOR_ITEM_SEND_DATA: &str = "{\"format_version\":1,\"wrapped_cek\":\"2.e/m5UvBFEh4JEHYgnAVONQ==|Cl7wnKMdT9NxeisUg1Xx3OmOyZr7Z77luoLPCBxuo1EVAjf69q3yaFO25InB8swQgHdKgz/PVqtX6JmmbR4xu2PKZtNFNmRRUVnX5BWvvjE=|+PW2Knoda9s1qVKMAEcXDsw5ij/wUZ/GfR9xVDnpPSw=\",\"envelope\":\"g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUCSl5i37B6J7uBZ8Ge91nw86AAE4gQI6AAE4gAGhBUxDZ7isjgG0Zt2UEERZATT9Jcf0kWC5y8qsWWn4iNEv9kbjf1jPeolS0FdxBu4y11Yez9MT1cPaJ8hxCjRztX5VgGzEKMnOcc491fwZXQByT0M9MLDDpJD3HDOOCzQ2gdk7VZktEmc8nhZoAGnZP0GmeoJh/my3WDukSsa2vOiOLE2KGIfF8OHa7nwXds9Z1aIhlavFSNAiqDWAdOk65OhqrvE0BPN7WdW7+NbuviPiEKa3wbCIhmjfQI1nW5simSqTMx4/ikLCqH2F3gLt4nk0SJ3KAbbQA3ENWMFef8s+m5uNWPIsALXeauC5X8XwvhOI2a1XNldR2r9LCEgg0vqzi+yCLVZpfKRQVFIfBmRwBtBObo1hFLBgbCkH8hXsX/eeU9qhL8oskb7s6HCGX0IGXoPzBLkUJH2IohWh3FMYVPd4Yw==\"}";
926
927 fn item_send_cipher_view() -> CipherView {
930 CipherView {
931 id: TEST_ITEM_ID.parse().ok(),
932 organization_id: None,
933 folder_id: None,
934 collection_ids: Vec::new(),
935 key: None,
936 name: "Item Send".to_string(),
937 notes: Some("Item Send notes".to_string()),
938 r#type: CipherType::Login,
939 login: Some(LoginView {
940 username: Some("[email protected]".to_string()),
941 password: Some("hunter2".to_string()),
942 password_revision_date: None,
943 uris: None,
944 totp: None,
945 autofill_on_page_load: None,
946 fido2_credentials: None,
947 }),
948 identity: None,
949 card: None,
950 secure_note: None,
951 ssh_key: None,
952 bank_account: None,
953 drivers_license: None,
954 passport: None,
955 favorite: false,
956 reprompt: CipherRepromptType::None,
957 organization_use_totp: false,
958 edit: false,
959 permissions: None,
960 view_password: true,
961 local_data: None,
962 attachments: None,
963 attachment_decryption_failures: None,
964 fields: Some(vec![FieldView {
965 name: Some("field".to_string()),
966 value: Some("value".to_string()),
967 r#type: FieldType::Text,
968 linked_id: None,
969 }]),
970 password_history: Some(vec![PasswordHistoryView {
971 password: "old-password".to_string(),
972 last_used_date: "2024-01-01T00:00:00Z".parse().unwrap(),
973 }]),
974 creation_date: Default::default(),
975 deleted_date: None,
976 revision_date: Default::default(),
977 archived_date: None,
978 partial: false,
979 }
980 }
981
982 fn item_send_view() -> SendView {
983 SendView {
984 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
985 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
986 name: "Test".to_string(),
987 notes: None,
988 key: Some(TEST_SEND_KEY_B64.to_owned()),
989 new_password: None,
990 has_password: false,
991 r#type: SendType::Item,
992 file: None,
993 text: None,
994 data: Some(SendItemView {
995 data: item_send_cipher_view(),
996 }),
997 max_access_count: None,
998 access_count: 0,
999 disabled: false,
1000 hide_email: false,
1001 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1002 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1003 expiration_date: None,
1004 emails: Vec::new(),
1005 auth_type: AuthType::None,
1006 }
1007 }
1008
1009 #[test]
1010 #[ignore = "Generates test vectors; run manually"]
1011 fn generate_item_send_test_vector() {
1012 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1013 let crypto = create_test_crypto_with_user_key(user_key);
1014 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1015 println!(
1016 "pub(crate) const TEST_VECTOR_ITEM_SEND_DATA: &str = {:?};",
1017 send.data.unwrap().data
1018 );
1019 }
1020
1021 #[test]
1022 fn test_item_send_test_vector() {
1023 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1024 let crypto = create_test_crypto_with_user_key(user_key);
1025
1026 let item = SendItem::try_from(SendDataModel {
1028 encryption_version: Some(SendEncryptionType::V1.into()),
1029 data: Some(TEST_VECTOR_ITEM_SEND_DATA.to_string()),
1030 metadata: Box::new(SendItemMetadataModel {
1031 item_id: TEST_ITEM_ID.parse().unwrap(),
1032 folder_name: None,
1033 organization_name: None,
1034 collection_names: None,
1035 creation_date: None,
1036 revision_date: None,
1037 }),
1038 })
1039 .unwrap();
1040 let send = Send {
1041 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1042 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1043 r#type: SendType::Item,
1044 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1045 .unwrap(),
1046 notes: None,
1047 file: None,
1048 text: None,
1049 data: Some(item),
1050 key: TEST_SEND_KEY.parse().unwrap(),
1051 max_access_count: None,
1052 access_count: 0,
1053 password: None,
1054 disabled: false,
1055 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1056 expiration_date: None,
1057 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1058 hide_email: false,
1059 emails: None,
1060 auth_type: AuthType::None,
1061 };
1062
1063 let view: SendView = crypto.decrypt(&send).unwrap();
1064
1065 assert_eq!(view, item_send_view());
1066 }
1067
1068 #[test]
1069 fn test_item_send_encrypt_round_trip() {
1070 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1071 let crypto = create_test_crypto_with_user_key(user_key);
1072
1073 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1074 let item = send.data.as_ref().unwrap();
1075 assert!(serde_json::from_str::<bitwarden_vault::Cipher>(&item.data).is_err());
1077
1078 let view: SendView = crypto.decrypt(&send).unwrap();
1079 assert_eq!(view, item_send_view());
1080 }
1081
1082 #[test]
1083 fn test_item_send_metadata_round_trip() {
1084 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1085 let crypto = create_test_crypto_with_user_key(user_key);
1086
1087 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1089 let item = send.data.as_ref().unwrap();
1090 assert_eq!(
1091 item.metadata,
1092 SendItemMetadata {
1093 item_id: TEST_ITEM_ID.parse().unwrap()
1094 }
1095 );
1096
1097 let decrypted: SendView = crypto.decrypt(&send).unwrap();
1098 assert_eq!(decrypted, item_send_view());
1099 }
1100
1101 #[test]
1102 fn test_item_send_encrypt_requires_item_id() {
1103 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1104 let crypto = create_test_crypto_with_user_key(user_key);
1105
1106 let mut view = item_send_view();
1107 view.data.as_mut().unwrap().data.id = None;
1108
1109 let result: Result<Send, _> = crypto.encrypt(view);
1110 assert!(result.is_err());
1111 }
1112
1113 #[test]
1114 fn test_get_send_key() {
1115 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1117 let crypto = create_test_crypto_with_user_key(user_key);
1118 let mut ctx = crypto.context();
1119
1120 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
1121 .parse()
1122 .unwrap();
1123
1124 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
1126 #[allow(deprecated)]
1127 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
1128 let send_key_b64 = send_key.to_base64();
1129 assert_eq!(
1130 send_key_b64.to_string(),
1131 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
1132 );
1133 }
1134
1135 #[test]
1136 pub fn test_decrypt() {
1137 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1138 let crypto = create_test_crypto_with_user_key(user_key);
1139
1140 let send = Send {
1141 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1142 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1143 r#type: SendType::Text,
1144 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1145 .unwrap(),
1146 notes: None,
1147 file: None,
1148 text: Some(SendText {
1149 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
1150 hidden: false,
1151 }),
1152 data: None,
1153 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
1154 max_access_count: None,
1155 access_count: 0,
1156 password: None,
1157 disabled: false,
1158 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1159 expiration_date: None,
1160 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1161 hide_email: false,
1162 emails: None,
1163 auth_type: AuthType::None,
1164 };
1165
1166 let view: SendView = crypto.decrypt(&send).unwrap();
1167
1168 let expected = SendView {
1169 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1170 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1171 name: "Test".to_string(),
1172 notes: None,
1173 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1174 new_password: None,
1175 has_password: false,
1176 r#type: SendType::Text,
1177 file: None,
1178 text: Some(SendTextView {
1179 text: Some("This is a test".to_owned()),
1180 hidden: false,
1181 }),
1182 data: None,
1183 max_access_count: None,
1184 access_count: 0,
1185 disabled: false,
1186 hide_email: false,
1187 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1188 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1189 expiration_date: None,
1190 emails: Vec::new(),
1191 auth_type: AuthType::None,
1192 };
1193
1194 assert_eq!(view, expected);
1195 }
1196
1197 #[test]
1198 pub fn test_encrypt() {
1199 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1200 let crypto = create_test_crypto_with_user_key(user_key);
1201
1202 let view = SendView {
1203 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1204 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1205 name: "Test".to_string(),
1206 notes: None,
1207 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1208 new_password: None,
1209 has_password: false,
1210 r#type: SendType::Text,
1211 file: None,
1212 text: Some(SendTextView {
1213 text: Some("This is a test".to_owned()),
1214 hidden: false,
1215 }),
1216 data: None,
1217 max_access_count: None,
1218 access_count: 0,
1219 disabled: false,
1220 hide_email: false,
1221 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1222 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1223 expiration_date: None,
1224 emails: Vec::new(),
1225 auth_type: AuthType::None,
1226 };
1227
1228 let v: SendView = crypto
1230 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1231 .unwrap();
1232 assert_eq!(v, view);
1233 }
1234
1235 #[test]
1236 pub fn test_create() {
1237 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1238 let crypto = create_test_crypto_with_user_key(user_key);
1239
1240 let view = SendView {
1241 id: None,
1242 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1243 name: "Test".to_string(),
1244 notes: None,
1245 key: None,
1246 new_password: None,
1247 has_password: false,
1248 r#type: SendType::Text,
1249 file: None,
1250 text: Some(SendTextView {
1251 text: Some("This is a test".to_owned()),
1252 hidden: false,
1253 }),
1254 data: None,
1255 max_access_count: None,
1256 access_count: 0,
1257 disabled: false,
1258 hide_email: false,
1259 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1260 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1261 expiration_date: None,
1262 emails: Vec::new(),
1263 auth_type: AuthType::None,
1264 };
1265
1266 let v: SendView = crypto
1268 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1269 .unwrap();
1270
1271 let t = SendView { key: None, ..v };
1273 assert_eq!(t, view);
1274 }
1275
1276 #[test]
1277 pub fn test_create_password() {
1278 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1279 let crypto = create_test_crypto_with_user_key(user_key);
1280
1281 let view = SendView {
1282 id: None,
1283 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1284 name: "Test".to_owned(),
1285 notes: None,
1286 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1287 new_password: Some("abc123".to_owned()),
1288 has_password: false,
1289 r#type: SendType::Text,
1290 file: None,
1291 text: Some(SendTextView {
1292 text: Some("This is a test".to_owned()),
1293 hidden: false,
1294 }),
1295 data: None,
1296 max_access_count: None,
1297 access_count: 0,
1298 disabled: false,
1299 hide_email: false,
1300 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1301 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1302 expiration_date: None,
1303 emails: Vec::new(),
1304 auth_type: AuthType::Password,
1305 };
1306
1307 let send: Send = crypto.encrypt(view).unwrap();
1308
1309 assert_eq!(
1310 send.password,
1311 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
1312 );
1313 assert_eq!(send.auth_type, AuthType::Password);
1314
1315 let v: SendView = crypto.decrypt(&send).unwrap();
1316 assert_eq!(v.new_password, None);
1317 assert!(v.has_password);
1318 assert_eq!(v.auth_type, AuthType::Password);
1319 }
1320
1321 #[test]
1322 pub fn test_create_email_otp() {
1323 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1324 let crypto = create_test_crypto_with_user_key(user_key);
1325
1326 let view = SendView {
1327 id: None,
1328 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1329 name: "Test".to_owned(),
1330 notes: None,
1331 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1332 new_password: None,
1333 has_password: false,
1334 r#type: SendType::Text,
1335 file: None,
1336 text: Some(SendTextView {
1337 text: Some("This is a test".to_owned()),
1338 hidden: false,
1339 }),
1340 data: None,
1341 max_access_count: None,
1342 access_count: 0,
1343 disabled: false,
1344 hide_email: false,
1345 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1346 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1347 expiration_date: None,
1348 emails: vec![
1349 String::from("[email protected]"),
1350 String::from("[email protected]"),
1351 ],
1352 auth_type: AuthType::Email,
1353 };
1354
1355 let send: Send = crypto.encrypt(view.clone()).unwrap();
1356
1357 let v: SendView = crypto.decrypt(&send).unwrap();
1359
1360 assert_eq!(v, view);
1361 }
1362
1363 #[test]
1364 fn test_send_into_send_with_id_request_model() {
1365 let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
1366 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
1367 .unwrap()
1368 .with_timezone(&Utc);
1369 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
1370 .unwrap()
1371 .with_timezone(&Utc);
1372 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
1373 .unwrap()
1374 .with_timezone(&Utc);
1375
1376 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
1377 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1378 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
1379 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
1380 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1381
1382 let send = Send {
1383 id: Some(SendId::new(send_id)),
1384 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
1385 name: name.parse().unwrap(),
1386 notes: Some(notes.parse().unwrap()),
1387 key: key.parse().unwrap(),
1388 password: Some("hash".to_string()),
1389 r#type: SendType::File,
1390 file: Some(SendFile {
1391 id: Some("file-id".to_string()),
1392 file_name: file_name.parse().unwrap(),
1393 size: Some("1234".to_string()),
1394 size_name: Some("1.2 KB".to_string()),
1395 }),
1396 text: Some(SendText {
1397 text: Some(text_value.parse().unwrap()),
1398 hidden: true,
1399 }),
1400 data: None,
1401 max_access_count: Some(42),
1402 access_count: 0,
1403 disabled: true,
1404 hide_email: true,
1405 revision_date,
1406 deletion_date,
1407 expiration_date: Some(expiration_date),
1408 emails: Some("[email protected],[email protected]".to_string()),
1409 auth_type: AuthType::Email,
1410 };
1411
1412 let model: SendWithIdRequestModel = send.into();
1413
1414 assert_eq!(model.id, send_id);
1415 assert_eq!(
1416 model.r#type,
1417 Some(bitwarden_api_api::models::SendType::File)
1418 );
1419 assert_eq!(
1420 model.auth_type,
1421 Some(bitwarden_api_api::models::AuthType::Email)
1422 );
1423 assert_eq!(model.file_length, Some(1234));
1424 assert_eq!(model.name.as_deref(), Some(name));
1425 assert_eq!(model.notes.as_deref(), Some(notes));
1426 assert_eq!(model.key, key);
1427 assert_eq!(model.max_access_count, Some(42));
1428 assert_eq!(
1429 model
1430 .expiration_date
1431 .unwrap()
1432 .parse::<DateTime<Utc>>()
1433 .unwrap(),
1434 expiration_date
1435 );
1436 assert_eq!(
1437 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1438 deletion_date
1439 );
1440 assert_eq!(model.password.as_deref(), Some("hash"));
1441 assert_eq!(
1442 model.emails.as_deref(),
1443 Some("[email protected],[email protected]")
1444 );
1445 assert!(model.disabled);
1446 assert_eq!(model.hide_email, Some(true));
1447
1448 let file = model.file.unwrap();
1449 assert_eq!(file.id.as_deref(), Some("file-id"));
1450 assert_eq!(file.file_name.as_deref(), Some(file_name));
1451 assert_eq!(file.size.as_deref(), Some("1234"));
1452 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1453
1454 let text = model.text.unwrap();
1455 assert_eq!(text.text.as_deref(), Some(text_value));
1456 assert_eq!(text.hidden, Some(true));
1457 }
1458
1459 #[test]
1460 fn test_item_send_into_send_with_id_request_model() {
1461 let send = Send {
1462 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1463 access_id: None,
1464 r#type: SendType::Item,
1465 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1466 .unwrap(),
1467 notes: None,
1468 file: None,
1469 text: None,
1470 data: Some(SendItem {
1471 encryption_version: SendEncryptionType::V1,
1472 data: TEST_VECTOR_ITEM_SEND_DATA.to_string(),
1473 metadata: SendItemMetadata {
1474 item_id: TEST_ITEM_ID.parse().unwrap(),
1475 },
1476 }),
1477 key: TEST_SEND_KEY.parse().unwrap(),
1478 max_access_count: None,
1479 access_count: 0,
1480 password: None,
1481 disabled: false,
1482 revision_date: "2024-01-07T23:56:48Z".parse().unwrap(),
1483 expiration_date: None,
1484 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1485 hide_email: false,
1486 emails: None,
1487 auth_type: AuthType::None,
1488 };
1489
1490 let model: SendWithIdRequestModel = send.into();
1491
1492 let data = model.data.expect("Item Send request must carry its data");
1494 assert_eq!(
1495 data.encryption_version,
1496 Some(bitwarden_api_api::models::SendEncryptionType::V1)
1497 );
1498 assert_eq!(data.data.as_deref(), Some(TEST_VECTOR_ITEM_SEND_DATA));
1499 assert_eq!(data.metadata.item_id.to_string(), TEST_ITEM_ID);
1500 }
1501
1502 #[test]
1503 fn auth_data_hashed_password_returns_key_b64_verbatim() {
1504 let key_b64 = "pretend-this-is-a-pbkdf2-output==".to_string();
1509 let auth = SendAuthType::HashedPassword {
1510 key_b64: key_b64.clone(),
1511 };
1512
1513 let (password, emails) = auth.auth_data(b"any-send-key-bytes-here");
1514
1515 assert_eq!(password, Some(key_b64));
1516 assert_eq!(emails, None);
1517 }
1518
1519 #[test]
1520 fn auth_data_hashed_and_plaintext_diverge_for_same_input() {
1521 let same_string = "abc123".to_string();
1526 let send_key = b"send-key-salt-bytes";
1527
1528 let (plaintext_out, _) = SendAuthType::Password {
1529 password: same_string.clone(),
1530 }
1531 .auth_data(send_key);
1532 let (hashed_out, _) = SendAuthType::HashedPassword {
1533 key_b64: same_string,
1534 }
1535 .auth_data(send_key);
1536
1537 assert_ne!(
1538 plaintext_out, hashed_out,
1539 "Plaintext path must run PBKDF2; HashedPassword path must not"
1540 );
1541 }
1542
1543 #[test]
1544 fn auth_type_for_hashed_password_maps_to_password() {
1545 assert_eq!(
1548 SendAuthType::Password {
1549 password: "p".to_string()
1550 }
1551 .auth_type(),
1552 AuthType::Password,
1553 );
1554 assert_eq!(
1555 SendAuthType::HashedPassword {
1556 key_b64: "k".to_string()
1557 }
1558 .auth_type(),
1559 AuthType::Password,
1560 );
1561 }
1562
1563 #[test]
1567 fn send_auth_type_round_trips_through_json() {
1568 let cases = [
1569 (SendAuthType::None, serde_json::json!({"type": "none"})),
1570 (
1571 SendAuthType::Password {
1572 password: "hunter2".to_string(),
1573 },
1574 serde_json::json!({"type": "password", "password": "hunter2"}),
1575 ),
1576 (
1577 SendAuthType::HashedPassword {
1578 key_b64: "deadbeef==".to_string(),
1579 },
1580 serde_json::json!({"type": "hashedPassword", "keyB64": "deadbeef=="}),
1581 ),
1582 (
1583 SendAuthType::Emails {
1584 emails: vec!["[email protected]".to_string()],
1585 },
1586 serde_json::json!({"type": "emails", "emails": ["[email protected]"]}),
1587 ),
1588 ];
1589 for (value, expected) in cases {
1590 let serialized = serde_json::to_value(&value).expect("serialize");
1591 assert_eq!(serialized, expected, "wire shape mismatch for {value:?}");
1592 let deserialized: SendAuthType =
1593 serde_json::from_value(serialized).expect("round-trip");
1594 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
1595 }
1596 }
1597
1598 #[test]
1599 fn typed_constructors_produce_expected_variants() {
1600 assert_eq!(
1601 SendAuthType::from_plaintext_password("hunter2".to_string()),
1602 SendAuthType::Password {
1603 password: "hunter2".to_string()
1604 },
1605 );
1606 assert_eq!(
1607 SendAuthType::from_hashed_password("deadbeef==".to_string()),
1608 SendAuthType::HashedPassword {
1609 key_b64: "deadbeef==".to_string()
1610 },
1611 );
1612 }
1613}