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;
20use zeroize::Zeroizing;
21#[cfg(feature = "wasm")]
22use {tsify::Tsify, wasm_bindgen::prelude::*};
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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 }),
897 }
898 }
899}
900
901#[cfg(test)]
902pub(crate) mod tests {
903 use bitwarden_api_api::models::SendItemMetadataModel;
904 use bitwarden_core::key_management::create_test_crypto_with_user_key;
905 use bitwarden_crypto::SymmetricCryptoKey;
906 use bitwarden_vault::{
907 CipherRepromptType, CipherType, FieldType, FieldView, LoginView, PasswordHistoryView,
908 };
909
910 use super::*;
911
912 const TEST_USER_KEY: &str =
913 "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==";
914 const TEST_SEND_KEY: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
915 const TEST_SEND_KEY_B64: &str = "Pgui0FK85cNhBGWHAlBHBw";
916 pub(crate) const TEST_ITEM_ID: &str = "5d4fbf2b-7a36-4b3c-9f2e-1a6d8c0e9b71";
917
918 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==\"}";
921
922 fn item_send_cipher_view() -> CipherView {
925 CipherView {
926 id: TEST_ITEM_ID.parse().ok(),
927 organization_id: None,
928 folder_id: None,
929 collection_ids: Vec::new(),
930 key: None,
931 name: "Item Send".to_string(),
932 notes: Some("Item Send notes".to_string()),
933 r#type: CipherType::Login,
934 login: Some(LoginView {
935 username: Some("[email protected]".to_string()),
936 password: Some("hunter2".to_string()),
937 password_revision_date: None,
938 uris: None,
939 totp: None,
940 autofill_on_page_load: None,
941 fido2_credentials: None,
942 }),
943 identity: None,
944 card: None,
945 secure_note: None,
946 ssh_key: None,
947 bank_account: None,
948 drivers_license: None,
949 passport: None,
950 favorite: false,
951 reprompt: CipherRepromptType::None,
952 organization_use_totp: false,
953 edit: false,
954 permissions: None,
955 view_password: true,
956 local_data: None,
957 attachments: None,
958 attachment_decryption_failures: None,
959 fields: Some(vec![FieldView {
960 name: Some("field".to_string()),
961 value: Some("value".to_string()),
962 r#type: FieldType::Text,
963 linked_id: None,
964 }]),
965 password_history: Some(vec![PasswordHistoryView {
966 password: "old-password".to_string(),
967 last_used_date: "2024-01-01T00:00:00Z".parse().unwrap(),
968 }]),
969 creation_date: Default::default(),
970 deleted_date: None,
971 revision_date: Default::default(),
972 archived_date: None,
973 partial: false,
974 }
975 }
976
977 fn item_send_view() -> SendView {
978 SendView {
979 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
980 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
981 name: "Test".to_string(),
982 notes: None,
983 key: Some(TEST_SEND_KEY_B64.to_owned()),
984 new_password: None,
985 has_password: false,
986 r#type: SendType::Item,
987 file: None,
988 text: None,
989 data: Some(SendItemView {
990 data: item_send_cipher_view(),
991 }),
992 max_access_count: None,
993 access_count: 0,
994 disabled: false,
995 hide_email: false,
996 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
997 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
998 expiration_date: None,
999 emails: Vec::new(),
1000 auth_type: AuthType::None,
1001 }
1002 }
1003
1004 #[test]
1005 #[ignore = "Generates test vectors; run manually"]
1006 fn generate_item_send_test_vector() {
1007 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1008 let crypto = create_test_crypto_with_user_key(user_key);
1009 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1010 println!(
1011 "pub(crate) const TEST_VECTOR_ITEM_SEND_DATA: &str = {:?};",
1012 send.data.unwrap().data
1013 );
1014 }
1015
1016 #[test]
1017 fn test_item_send_test_vector() {
1018 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1019 let crypto = create_test_crypto_with_user_key(user_key);
1020
1021 let item = SendItem::try_from(SendDataModel {
1023 encryption_version: Some(SendEncryptionType::V1.into()),
1024 data: Some(TEST_VECTOR_ITEM_SEND_DATA.to_string()),
1025 metadata: Box::new(SendItemMetadataModel {
1026 item_id: TEST_ITEM_ID.parse().unwrap(),
1027 }),
1028 })
1029 .unwrap();
1030 let send = Send {
1031 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1032 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1033 r#type: SendType::Item,
1034 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1035 .unwrap(),
1036 notes: None,
1037 file: None,
1038 text: None,
1039 data: Some(item),
1040 key: TEST_SEND_KEY.parse().unwrap(),
1041 max_access_count: None,
1042 access_count: 0,
1043 password: None,
1044 disabled: false,
1045 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1046 expiration_date: None,
1047 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1048 hide_email: false,
1049 emails: None,
1050 auth_type: AuthType::None,
1051 };
1052
1053 let view: SendView = crypto.decrypt(&send).unwrap();
1054
1055 assert_eq!(view, item_send_view());
1056 }
1057
1058 #[test]
1059 fn test_item_send_encrypt_round_trip() {
1060 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1061 let crypto = create_test_crypto_with_user_key(user_key);
1062
1063 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1064 let item = send.data.as_ref().unwrap();
1065 assert!(serde_json::from_str::<bitwarden_vault::Cipher>(&item.data).is_err());
1067
1068 let view: SendView = crypto.decrypt(&send).unwrap();
1069 assert_eq!(view, item_send_view());
1070 }
1071
1072 #[test]
1073 fn test_item_send_metadata_round_trip() {
1074 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1075 let crypto = create_test_crypto_with_user_key(user_key);
1076
1077 let send: Send = crypto.encrypt(item_send_view()).unwrap();
1079 let item = send.data.as_ref().unwrap();
1080 assert_eq!(
1081 item.metadata,
1082 SendItemMetadata {
1083 item_id: TEST_ITEM_ID.parse().unwrap()
1084 }
1085 );
1086
1087 let decrypted: SendView = crypto.decrypt(&send).unwrap();
1088 assert_eq!(decrypted, item_send_view());
1089 }
1090
1091 #[test]
1092 fn test_item_send_encrypt_requires_item_id() {
1093 let user_key: SymmetricCryptoKey = TEST_USER_KEY.to_string().try_into().unwrap();
1094 let crypto = create_test_crypto_with_user_key(user_key);
1095
1096 let mut view = item_send_view();
1097 view.data.as_mut().unwrap().data.id = None;
1098
1099 let result: Result<Send, _> = crypto.encrypt(view);
1100 assert!(result.is_err());
1101 }
1102
1103 #[test]
1104 fn test_get_send_key() {
1105 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1107 let crypto = create_test_crypto_with_user_key(user_key);
1108 let mut ctx = crypto.context();
1109
1110 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
1111 .parse()
1112 .unwrap();
1113
1114 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
1116 #[allow(deprecated)]
1117 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
1118 let send_key_b64 = send_key.to_base64();
1119 assert_eq!(
1120 send_key_b64.to_string(),
1121 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
1122 );
1123 }
1124
1125 #[test]
1126 pub fn test_decrypt() {
1127 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1128 let crypto = create_test_crypto_with_user_key(user_key);
1129
1130 let send = Send {
1131 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1132 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1133 r#type: SendType::Text,
1134 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1135 .unwrap(),
1136 notes: None,
1137 file: None,
1138 text: Some(SendText {
1139 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
1140 hidden: false,
1141 }),
1142 data: None,
1143 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
1144 max_access_count: None,
1145 access_count: 0,
1146 password: None,
1147 disabled: false,
1148 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1149 expiration_date: None,
1150 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1151 hide_email: false,
1152 emails: None,
1153 auth_type: AuthType::None,
1154 };
1155
1156 let view: SendView = crypto.decrypt(&send).unwrap();
1157
1158 let expected = SendView {
1159 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1160 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1161 name: "Test".to_string(),
1162 notes: None,
1163 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1164 new_password: None,
1165 has_password: false,
1166 r#type: SendType::Text,
1167 file: None,
1168 text: Some(SendTextView {
1169 text: Some("This is a test".to_owned()),
1170 hidden: false,
1171 }),
1172 data: None,
1173 max_access_count: None,
1174 access_count: 0,
1175 disabled: false,
1176 hide_email: false,
1177 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1178 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1179 expiration_date: None,
1180 emails: Vec::new(),
1181 auth_type: AuthType::None,
1182 };
1183
1184 assert_eq!(view, expected);
1185 }
1186
1187 #[test]
1188 pub fn test_encrypt() {
1189 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1190 let crypto = create_test_crypto_with_user_key(user_key);
1191
1192 let view = SendView {
1193 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1194 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1195 name: "Test".to_string(),
1196 notes: None,
1197 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1198 new_password: None,
1199 has_password: false,
1200 r#type: SendType::Text,
1201 file: None,
1202 text: Some(SendTextView {
1203 text: Some("This is a test".to_owned()),
1204 hidden: false,
1205 }),
1206 data: None,
1207 max_access_count: None,
1208 access_count: 0,
1209 disabled: false,
1210 hide_email: false,
1211 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1212 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1213 expiration_date: None,
1214 emails: Vec::new(),
1215 auth_type: AuthType::None,
1216 };
1217
1218 let v: SendView = crypto
1220 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1221 .unwrap();
1222 assert_eq!(v, view);
1223 }
1224
1225 #[test]
1226 pub fn test_create() {
1227 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1228 let crypto = create_test_crypto_with_user_key(user_key);
1229
1230 let view = SendView {
1231 id: None,
1232 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1233 name: "Test".to_string(),
1234 notes: None,
1235 key: None,
1236 new_password: None,
1237 has_password: false,
1238 r#type: SendType::Text,
1239 file: None,
1240 text: Some(SendTextView {
1241 text: Some("This is a test".to_owned()),
1242 hidden: false,
1243 }),
1244 data: None,
1245 max_access_count: None,
1246 access_count: 0,
1247 disabled: false,
1248 hide_email: false,
1249 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1250 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1251 expiration_date: None,
1252 emails: Vec::new(),
1253 auth_type: AuthType::None,
1254 };
1255
1256 let v: SendView = crypto
1258 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1259 .unwrap();
1260
1261 let t = SendView { key: None, ..v };
1263 assert_eq!(t, view);
1264 }
1265
1266 #[test]
1267 pub fn test_create_password() {
1268 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1269 let crypto = create_test_crypto_with_user_key(user_key);
1270
1271 let view = SendView {
1272 id: None,
1273 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1274 name: "Test".to_owned(),
1275 notes: None,
1276 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1277 new_password: Some("abc123".to_owned()),
1278 has_password: false,
1279 r#type: SendType::Text,
1280 file: None,
1281 text: Some(SendTextView {
1282 text: Some("This is a test".to_owned()),
1283 hidden: false,
1284 }),
1285 data: None,
1286 max_access_count: None,
1287 access_count: 0,
1288 disabled: false,
1289 hide_email: false,
1290 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1291 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1292 expiration_date: None,
1293 emails: Vec::new(),
1294 auth_type: AuthType::Password,
1295 };
1296
1297 let send: Send = crypto.encrypt(view).unwrap();
1298
1299 assert_eq!(
1300 send.password,
1301 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
1302 );
1303 assert_eq!(send.auth_type, AuthType::Password);
1304
1305 let v: SendView = crypto.decrypt(&send).unwrap();
1306 assert_eq!(v.new_password, None);
1307 assert!(v.has_password);
1308 assert_eq!(v.auth_type, AuthType::Password);
1309 }
1310
1311 #[test]
1312 pub fn test_create_email_otp() {
1313 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1314 let crypto = create_test_crypto_with_user_key(user_key);
1315
1316 let view = SendView {
1317 id: None,
1318 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1319 name: "Test".to_owned(),
1320 notes: None,
1321 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1322 new_password: None,
1323 has_password: false,
1324 r#type: SendType::Text,
1325 file: None,
1326 text: Some(SendTextView {
1327 text: Some("This is a test".to_owned()),
1328 hidden: false,
1329 }),
1330 data: None,
1331 max_access_count: None,
1332 access_count: 0,
1333 disabled: false,
1334 hide_email: false,
1335 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1336 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1337 expiration_date: None,
1338 emails: vec![
1339 String::from("[email protected]"),
1340 String::from("[email protected]"),
1341 ],
1342 auth_type: AuthType::Email,
1343 };
1344
1345 let send: Send = crypto.encrypt(view.clone()).unwrap();
1346
1347 let v: SendView = crypto.decrypt(&send).unwrap();
1349
1350 assert_eq!(v, view);
1351 }
1352
1353 #[test]
1354 fn test_send_into_send_with_id_request_model() {
1355 let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
1356 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
1357 .unwrap()
1358 .with_timezone(&Utc);
1359 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
1360 .unwrap()
1361 .with_timezone(&Utc);
1362 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
1363 .unwrap()
1364 .with_timezone(&Utc);
1365
1366 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
1367 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1368 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
1369 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
1370 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1371
1372 let send = Send {
1373 id: Some(SendId::new(send_id)),
1374 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
1375 name: name.parse().unwrap(),
1376 notes: Some(notes.parse().unwrap()),
1377 key: key.parse().unwrap(),
1378 password: Some("hash".to_string()),
1379 r#type: SendType::File,
1380 file: Some(SendFile {
1381 id: Some("file-id".to_string()),
1382 file_name: file_name.parse().unwrap(),
1383 size: Some("1234".to_string()),
1384 size_name: Some("1.2 KB".to_string()),
1385 }),
1386 text: Some(SendText {
1387 text: Some(text_value.parse().unwrap()),
1388 hidden: true,
1389 }),
1390 data: None,
1391 max_access_count: Some(42),
1392 access_count: 0,
1393 disabled: true,
1394 hide_email: true,
1395 revision_date,
1396 deletion_date,
1397 expiration_date: Some(expiration_date),
1398 emails: Some("[email protected],[email protected]".to_string()),
1399 auth_type: AuthType::Email,
1400 };
1401
1402 let model: SendWithIdRequestModel = send.into();
1403
1404 assert_eq!(model.id, send_id);
1405 assert_eq!(
1406 model.r#type,
1407 Some(bitwarden_api_api::models::SendType::File)
1408 );
1409 assert_eq!(
1410 model.auth_type,
1411 Some(bitwarden_api_api::models::AuthType::Email)
1412 );
1413 assert_eq!(model.file_length, Some(1234));
1414 assert_eq!(model.name.as_deref(), Some(name));
1415 assert_eq!(model.notes.as_deref(), Some(notes));
1416 assert_eq!(model.key, key);
1417 assert_eq!(model.max_access_count, Some(42));
1418 assert_eq!(
1419 model
1420 .expiration_date
1421 .unwrap()
1422 .parse::<DateTime<Utc>>()
1423 .unwrap(),
1424 expiration_date
1425 );
1426 assert_eq!(
1427 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1428 deletion_date
1429 );
1430 assert_eq!(model.password.as_deref(), Some("hash"));
1431 assert_eq!(
1432 model.emails.as_deref(),
1433 Some("[email protected],[email protected]")
1434 );
1435 assert!(model.disabled);
1436 assert_eq!(model.hide_email, Some(true));
1437
1438 let file = model.file.unwrap();
1439 assert_eq!(file.id.as_deref(), Some("file-id"));
1440 assert_eq!(file.file_name.as_deref(), Some(file_name));
1441 assert_eq!(file.size.as_deref(), Some("1234"));
1442 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1443
1444 let text = model.text.unwrap();
1445 assert_eq!(text.text.as_deref(), Some(text_value));
1446 assert_eq!(text.hidden, Some(true));
1447 }
1448
1449 #[test]
1450 fn test_item_send_into_send_with_id_request_model() {
1451 let send = Send {
1452 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
1453 access_id: None,
1454 r#type: SendType::Item,
1455 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
1456 .unwrap(),
1457 notes: None,
1458 file: None,
1459 text: None,
1460 data: Some(SendItem {
1461 encryption_version: SendEncryptionType::V1,
1462 data: TEST_VECTOR_ITEM_SEND_DATA.to_string(),
1463 metadata: SendItemMetadata {
1464 item_id: TEST_ITEM_ID.parse().unwrap(),
1465 },
1466 }),
1467 key: TEST_SEND_KEY.parse().unwrap(),
1468 max_access_count: None,
1469 access_count: 0,
1470 password: None,
1471 disabled: false,
1472 revision_date: "2024-01-07T23:56:48Z".parse().unwrap(),
1473 expiration_date: None,
1474 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1475 hide_email: false,
1476 emails: None,
1477 auth_type: AuthType::None,
1478 };
1479
1480 let model: SendWithIdRequestModel = send.into();
1481
1482 let data = model.data.expect("Item Send request must carry its data");
1484 assert_eq!(
1485 data.encryption_version,
1486 Some(bitwarden_api_api::models::SendEncryptionType::V1)
1487 );
1488 assert_eq!(data.data.as_deref(), Some(TEST_VECTOR_ITEM_SEND_DATA));
1489 assert_eq!(data.metadata.item_id.to_string(), TEST_ITEM_ID);
1490 }
1491
1492 #[test]
1493 fn auth_data_hashed_password_returns_key_b64_verbatim() {
1494 let key_b64 = "pretend-this-is-a-pbkdf2-output==".to_string();
1499 let auth = SendAuthType::HashedPassword {
1500 key_b64: key_b64.clone(),
1501 };
1502
1503 let (password, emails) = auth.auth_data(b"any-send-key-bytes-here");
1504
1505 assert_eq!(password, Some(key_b64));
1506 assert_eq!(emails, None);
1507 }
1508
1509 #[test]
1510 fn auth_data_hashed_and_plaintext_diverge_for_same_input() {
1511 let same_string = "abc123".to_string();
1516 let send_key = b"send-key-salt-bytes";
1517
1518 let (plaintext_out, _) = SendAuthType::Password {
1519 password: same_string.clone(),
1520 }
1521 .auth_data(send_key);
1522 let (hashed_out, _) = SendAuthType::HashedPassword {
1523 key_b64: same_string,
1524 }
1525 .auth_data(send_key);
1526
1527 assert_ne!(
1528 plaintext_out, hashed_out,
1529 "Plaintext path must run PBKDF2; HashedPassword path must not"
1530 );
1531 }
1532
1533 #[test]
1534 fn auth_type_for_hashed_password_maps_to_password() {
1535 assert_eq!(
1538 SendAuthType::Password {
1539 password: "p".to_string()
1540 }
1541 .auth_type(),
1542 AuthType::Password,
1543 );
1544 assert_eq!(
1545 SendAuthType::HashedPassword {
1546 key_b64: "k".to_string()
1547 }
1548 .auth_type(),
1549 AuthType::Password,
1550 );
1551 }
1552
1553 #[test]
1557 fn send_auth_type_round_trips_through_json() {
1558 let cases = [
1559 (SendAuthType::None, serde_json::json!({"type": "none"})),
1560 (
1561 SendAuthType::Password {
1562 password: "hunter2".to_string(),
1563 },
1564 serde_json::json!({"type": "password", "password": "hunter2"}),
1565 ),
1566 (
1567 SendAuthType::HashedPassword {
1568 key_b64: "deadbeef==".to_string(),
1569 },
1570 serde_json::json!({"type": "hashedPassword", "keyB64": "deadbeef=="}),
1571 ),
1572 (
1573 SendAuthType::Emails {
1574 emails: vec!["[email protected]".to_string()],
1575 },
1576 serde_json::json!({"type": "emails", "emails": ["[email protected]"]}),
1577 ),
1578 ];
1579 for (value, expected) in cases {
1580 let serialized = serde_json::to_value(&value).expect("serialize");
1581 assert_eq!(serialized, expected, "wire shape mismatch for {value:?}");
1582 let deserialized: SendAuthType =
1583 serde_json::from_value(serialized).expect("round-trip");
1584 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
1585 }
1586 }
1587
1588 #[test]
1589 fn typed_constructors_produce_expected_variants() {
1590 assert_eq!(
1591 SendAuthType::from_plaintext_password("hunter2".to_string()),
1592 SendAuthType::Password {
1593 password: "hunter2".to_string()
1594 },
1595 );
1596 assert_eq!(
1597 SendAuthType::from_hashed_password("deadbeef==".to_string()),
1598 SendAuthType::HashedPassword {
1599 key_b64: "deadbeef==".to_string()
1600 },
1601 );
1602 }
1603}