1use bitwarden_api_api::models::{
2 SendDataModel, SendFileModel, SendResponseModel, SendTextModel, SendWithIdRequestModel,
3};
4use bitwarden_core::{
5 key_management::{KeySlotIds, SymmetricKeySlotId},
6 require,
7};
8use bitwarden_crypto::{
9 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
10 OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes,
11};
12use bitwarden_encoding::{B64, B64Url};
13use bitwarden_uuid::uuid_newtype;
14use bitwarden_vault::{Cipher, CipherView, EncryptMode};
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use serde_repr::{Deserialize_repr, Serialize_repr};
18use thiserror::Error;
19use zeroize::Zeroizing;
20#[cfg(feature = "wasm")]
21use {tsify::Tsify, wasm_bindgen::prelude::*};
22
23use crate::{SendParseError, access::SEND_KEY_LEN, error::SendItemDeserializationFailureError};
24pub const SEND_ITERATIONS: u32 = 100_000;
25pub const DEFAULT_SEND_ENCRYPTION: SendEncryptionType = SendEncryptionType::V1;
26
27uuid_newtype!(pub SendId);
28
29#[derive(Debug, Error)]
31#[error("Email authentication requires at least one email address")]
32pub struct EmptyEmailListError;
33
34#[derive(Serialize, Deserialize, Debug, Clone)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
38#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
39pub struct SendFile {
40 pub id: Option<String>,
42 pub file_name: EncString,
44 pub size: Option<String>,
46 pub size_name: Option<String>,
48}
49
50#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
55pub struct SendFileView {
56 pub id: Option<String>,
58 pub file_name: String,
60 pub size: Option<String>,
62 pub size_name: Option<String>,
64}
65
66#[derive(Serialize, Deserialize, Debug, Clone)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
70#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
71pub struct SendText {
72 pub text: Option<EncString>,
73 pub hidden: bool,
74}
75
76#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
80#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
81pub struct SendItemView {
82 pub data: CipherView,
84}
85
86#[derive(Serialize, Deserialize, Debug, Clone)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
91pub struct SendItem {
92 pub encryption_version: SendEncryptionType,
93 pub data: Cipher,
94}
95
96#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
100#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
101pub struct SendTextView {
102 pub text: Option<String>,
104 pub hidden: bool,
106}
107
108#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
110#[repr(u8)]
111#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
112#[cfg_attr(feature = "wasm", wasm_bindgen)]
113pub enum SendType {
114 Text = 0,
116 File = 1,
118 Item = 2,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
124#[repr(u8)]
125#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
126#[cfg_attr(feature = "wasm", wasm_bindgen)]
127pub enum AuthType {
128 Email = 0,
130
131 Password = 1,
133
134 None = 2,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
140#[repr(u8)]
141#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
142#[cfg_attr(feature = "wasm", wasm_bindgen)]
143pub enum SendEncryptionType {
144 V1 = 1,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(tag = "type", rename_all = "camelCase")]
152#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
153#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
154pub enum SendAuthType {
155 None,
157 Password {
160 password: String,
162 },
163 HashedPassword {
169 #[serde(rename = "keyB64")]
171 key_b64: String,
172 },
173 Emails {
175 emails: Vec<String>,
177 },
178}
179
180impl SendAuthType {
181 pub fn from_plaintext_password(password: String) -> Self {
184 SendAuthType::Password { password }
185 }
186
187 pub fn from_hashed_password(key_b64: String) -> Self {
191 SendAuthType::HashedPassword { key_b64 }
192 }
193
194 pub fn auth_type(&self) -> AuthType {
196 match self {
197 SendAuthType::None => AuthType::None,
198 SendAuthType::Password { .. } | SendAuthType::HashedPassword { .. } => {
199 AuthType::Password
200 }
201 SendAuthType::Emails { .. } => AuthType::Email,
202 }
203 }
204
205 pub(crate) fn validate(&self) -> Result<(), EmptyEmailListError> {
208 if let SendAuthType::Emails { emails } = self
209 && emails.is_empty()
210 {
211 return Err(EmptyEmailListError);
212 }
213 Ok(())
214 }
215
216 pub(crate) fn auth_data(&self, k: &[u8]) -> (Option<String>, Option<String>) {
220 match self {
221 SendAuthType::Password { password } => {
222 let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), k, SEND_ITERATIONS);
223 (Some(B64::from(hashed.as_slice()).to_string()), None)
224 }
225 SendAuthType::HashedPassword { key_b64 } => (Some(key_b64.clone()), None),
226 SendAuthType::Emails { emails } => {
227 let emails_str = if emails.is_empty() {
228 None
229 } else {
230 Some(emails.join(","))
231 };
232 (None, emails_str)
233 }
234 SendAuthType::None => (None, None),
235 }
236 }
237}
238
239#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
241#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
242pub enum SendViewType {
243 File(SendFileView),
245 Text(SendTextView),
247 Item(Box<SendItemView>),
249}
250
251type SendApiModels = (
253 bitwarden_api_api::models::SendType,
254 Option<Box<bitwarden_api_api::models::SendFileModel>>,
255 Option<Box<bitwarden_api_api::models::SendTextModel>>,
256 Option<Box<bitwarden_api_api::models::SendDataModel>>,
257);
258
259impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendApiModels> for SendViewType {
260 fn encrypt_composite(
261 &self,
262 ctx: &mut KeyStoreContext<KeySlotIds>,
263 key: SymmetricKeySlotId,
264 ) -> Result<SendApiModels, CryptoError> {
265 match self {
266 SendViewType::File(f) => Ok((
267 bitwarden_api_api::models::SendType::File,
268 Some(Box::new(bitwarden_api_api::models::SendFileModel {
269 id: f.id.clone(),
270 file_name: Some(f.file_name.encrypt(ctx, key)?.to_string()),
271 size: f.size.clone(),
272 size_name: f.size_name.clone(),
273 })),
274 None,
275 None,
276 )),
277 SendViewType::Text(t) => Ok((
278 bitwarden_api_api::models::SendType::Text,
279 None,
280 Some(Box::new(bitwarden_api_api::models::SendTextModel {
281 text: t
282 .text
283 .as_ref()
284 .map(|txt| txt.encrypt(ctx, key))
285 .transpose()?
286 .map(|e| e.to_string()),
287 hidden: Some(t.hidden),
288 })),
289 None,
290 )),
291 SendViewType::Item(i) => {
292 let encrypted = i.encrypt_composite(ctx, key)?;
293 let serialized_cipher =
294 serde_json::to_string(&encrypted.data).unwrap_or("{}".to_string());
295 Ok((
296 bitwarden_api_api::models::SendType::Item,
297 None,
298 None,
299 Some(Box::new(bitwarden_api_api::models::SendDataModel {
300 encryption_version: Some(DEFAULT_SEND_ENCRYPTION.into()),
301 data: Some(serialized_cipher),
302 })),
303 ))
304 }
305 }
306 }
307}
308
309#[allow(missing_docs)]
310#[derive(Serialize, Deserialize, Debug, Clone)]
311#[serde(rename_all = "camelCase", deny_unknown_fields)]
312#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
313#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
314pub struct Send {
315 pub id: Option<SendId>,
316 pub access_id: Option<String>,
317
318 pub name: EncString,
319 pub notes: Option<EncString>,
320 pub key: EncString,
321 pub password: Option<String>,
322
323 pub r#type: SendType,
324 pub file: Option<SendFile>,
325 pub text: Option<SendText>,
326 pub data: Option<SendItem>,
327
328 pub max_access_count: Option<u32>,
329 pub access_count: u32,
330 pub disabled: bool,
331 pub hide_email: bool,
332
333 pub revision_date: DateTime<Utc>,
334 pub deletion_date: DateTime<Utc>,
335 pub expiration_date: Option<DateTime<Utc>>,
336
337 pub emails: Option<String>,
342 pub auth_type: AuthType,
343}
344
345bitwarden_state::register_repository_item!(SendId => Send, "Send");
346
347impl From<Send> for SendWithIdRequestModel {
348 fn from(send: Send) -> Self {
349 let file_length = send.file.as_ref().and_then(|file| {
350 file.size
351 .as_deref()
352 .and_then(|size| size.parse::<i64>().ok())
353 });
354
355 SendWithIdRequestModel {
356 r#type: Some(send.r#type.into()),
357 auth_type: Some(send.auth_type.into()),
358 file_length,
359 name: Some(send.name.to_string()),
360 notes: send.notes.map(|notes| notes.to_string()),
361 key: send.key.to_string(),
362 max_access_count: send.max_access_count.map(|count| count as i32),
363 expiration_date: send.expiration_date.map(|date| date.to_rfc3339()),
364 deletion_date: send.deletion_date.to_rfc3339(),
365 file: send.file.map(|file| Box::new(file.into())),
366 text: send.text.map(|text| Box::new(text.into())),
367 data: None,
369 password: send.password,
370 emails: send.emails,
371 disabled: send.disabled,
372 hide_email: Some(send.hide_email),
373 id: send
374 .id
375 .expect("SendWithIdRequestModel conversion requires send id")
376 .into(),
377 }
378 }
379}
380
381#[allow(missing_docs)]
382#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
383#[serde(rename_all = "camelCase", deny_unknown_fields)]
384#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
385#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
386pub struct SendView {
387 pub id: Option<SendId>,
388 pub access_id: Option<String>,
389
390 pub name: String,
391 pub notes: Option<String>,
392 pub key: Option<String>,
394 pub new_password: Option<String>,
398 pub has_password: bool,
401
402 pub r#type: SendType,
403 pub file: Option<SendFileView>,
404 pub text: Option<SendTextView>,
405 pub data: Option<SendItemView>,
406
407 pub max_access_count: Option<u32>,
408 pub access_count: u32,
409 pub disabled: bool,
410 pub hide_email: bool,
411
412 pub revision_date: DateTime<Utc>,
413 pub deletion_date: DateTime<Utc>,
414 pub expiration_date: Option<DateTime<Utc>>,
415
416 pub emails: Vec<String>,
421 pub auth_type: AuthType,
422}
423
424#[allow(missing_docs)]
425#[derive(Serialize, Deserialize, Debug)]
426#[serde(rename_all = "camelCase", deny_unknown_fields)]
427#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
428#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
429pub struct SendListView {
430 pub id: Option<SendId>,
431 pub access_id: Option<String>,
432
433 pub name: String,
434
435 pub r#type: SendType,
436 pub disabled: bool,
437
438 pub revision_date: DateTime<Utc>,
439 pub deletion_date: DateTime<Utc>,
440 pub expiration_date: Option<DateTime<Utc>>,
441
442 pub auth_type: AuthType,
443}
444
445impl Send {
446 #[allow(missing_docs)]
447 pub fn get_key(
448 ctx: &mut KeyStoreContext<KeySlotIds>,
449 send_key: &EncString,
450 enc_key: SymmetricKeySlotId,
451 ) -> Result<SymmetricKeySlotId, CryptoError> {
452 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
453 Self::derive_shareable_key(ctx, &key)
454 }
455
456 pub(crate) fn derive_shareable_key(
457 ctx: &mut KeyStoreContext<KeySlotIds>,
458 key: &[u8],
459 ) -> Result<SymmetricKeySlotId, CryptoError> {
460 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
461 ctx.derive_shareable_key(key, "send", Some("send"))
462 }
463}
464
465impl IdentifyKey<SymmetricKeySlotId> for Send {
466 fn key_identifier(&self) -> SymmetricKeySlotId {
467 SymmetricKeySlotId::User
468 }
469}
470
471impl IdentifyKey<SymmetricKeySlotId> for SendView {
472 fn key_identifier(&self) -> SymmetricKeySlotId {
473 SymmetricKeySlotId::User
474 }
475}
476
477impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendTextView> for SendText {
478 fn decrypt(
479 &self,
480 ctx: &mut KeyStoreContext<KeySlotIds>,
481 key: SymmetricKeySlotId,
482 ) -> Result<SendTextView, CryptoError> {
483 Ok(SendTextView {
484 text: self.text.decrypt(ctx, key)?,
485 hidden: self.hidden,
486 })
487 }
488}
489
490impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendText> for SendTextView {
491 fn encrypt_composite(
492 &self,
493 ctx: &mut KeyStoreContext<KeySlotIds>,
494 key: SymmetricKeySlotId,
495 ) -> Result<SendText, CryptoError> {
496 Ok(SendText {
497 text: self.text.encrypt(ctx, key)?,
498 hidden: self.hidden,
499 })
500 }
501}
502
503impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendFileView> for SendFile {
504 fn decrypt(
505 &self,
506 ctx: &mut KeyStoreContext<KeySlotIds>,
507 key: SymmetricKeySlotId,
508 ) -> Result<SendFileView, CryptoError> {
509 Ok(SendFileView {
510 id: self.id.clone(),
511 file_name: self.file_name.decrypt(ctx, key)?,
512 size: self.size.clone(),
513 size_name: self.size_name.clone(),
514 })
515 }
516}
517
518impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendFile> for SendFileView {
519 fn encrypt_composite(
520 &self,
521 ctx: &mut KeyStoreContext<KeySlotIds>,
522 key: SymmetricKeySlotId,
523 ) -> Result<SendFile, CryptoError> {
524 Ok(SendFile {
525 id: self.id.clone(),
526 file_name: self.file_name.encrypt(ctx, key)?,
527 size: self.size.clone(),
528 size_name: self.size_name.clone(),
529 })
530 }
531}
532
533impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendItemView> for SendItem {
534 fn decrypt(
535 &self,
536 ctx: &mut KeyStoreContext<KeySlotIds>,
537 key: SymmetricKeySlotId,
538 ) -> Result<SendItemView, CryptoError> {
539 let data: CipherView = self.data.decrypt(ctx, key)?;
540 Ok(SendItemView { data })
541 }
542}
543
544impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendItem> for SendItemView {
545 fn encrypt_composite(
546 &self,
547 ctx: &mut KeyStoreContext<KeySlotIds>,
548 key: SymmetricKeySlotId,
549 ) -> Result<SendItem, CryptoError> {
550 let cipher: Cipher = EncryptMode::Legacy(self.data.clone()).encrypt_composite(ctx, key)?;
551 Ok(SendItem {
552 encryption_version: DEFAULT_SEND_ENCRYPTION,
553 data: cipher,
554 })
555 }
556}
557
558impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendView> for Send {
559 fn decrypt(
560 &self,
561 ctx: &mut KeyStoreContext<KeySlotIds>,
562 key: SymmetricKeySlotId,
563 ) -> Result<SendView, CryptoError> {
564 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
568 let key = Send::derive_shareable_key(ctx, &k)?;
569
570 Ok(SendView {
571 id: self.id,
572 access_id: self.access_id.clone(),
573
574 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
575 notes: self.notes.decrypt(ctx, key).ok().flatten(),
576 key: Some(B64Url::from(k).to_string()),
577 new_password: None,
578 has_password: self.password.is_some(),
579
580 r#type: self.r#type,
581 file: self.file.decrypt(ctx, key).ok().flatten(),
582 text: self.text.decrypt(ctx, key).ok().flatten(),
583 data: self.data.decrypt(ctx, key).ok().flatten(),
584
585 max_access_count: self.max_access_count,
586 access_count: self.access_count,
587 disabled: self.disabled,
588 hide_email: self.hide_email,
589
590 revision_date: self.revision_date,
591 deletion_date: self.deletion_date,
592 expiration_date: self.expiration_date,
593
594 emails: self
595 .emails
596 .as_deref()
597 .unwrap_or_default()
598 .split(',')
599 .map(|e| e.trim())
600 .filter(|e| !e.is_empty())
601 .map(String::from)
602 .collect(),
603 auth_type: self.auth_type,
604 })
605 }
606}
607
608impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendListView> for Send {
609 fn decrypt(
610 &self,
611 ctx: &mut KeyStoreContext<KeySlotIds>,
612 key: SymmetricKeySlotId,
613 ) -> Result<SendListView, CryptoError> {
614 let key = Send::get_key(ctx, &self.key, key)?;
618
619 Ok(SendListView {
620 id: self.id,
621 access_id: self.access_id.clone(),
622
623 name: self.name.decrypt(ctx, key)?,
624 r#type: self.r#type,
625
626 disabled: self.disabled,
627
628 revision_date: self.revision_date,
629 deletion_date: self.deletion_date,
630 expiration_date: self.expiration_date,
631
632 auth_type: self.auth_type,
633 })
634 }
635}
636
637impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Send> for SendView {
638 fn encrypt_composite(
639 &self,
640 ctx: &mut KeyStoreContext<KeySlotIds>,
641 key: SymmetricKeySlotId,
642 ) -> Result<Send, CryptoError> {
643 let k = match (&self.key, &self.id) {
647 (Some(k), _) => B64Url::try_from(k.as_str())
649 .map_err(|_| CryptoError::InvalidKey)?
650 .as_bytes()
651 .to_vec(),
652 (None, None) => {
654 let key = generate_random_bytes::<[u8; SEND_KEY_LEN]>();
655 key.to_vec()
656 }
657 _ => return Err(CryptoError::InvalidKey),
659 };
660 let send_key = Send::derive_shareable_key(ctx, &k)?;
661
662 Ok(Send {
663 id: self.id,
664 access_id: self.access_id.clone(),
665
666 name: self.name.encrypt(ctx, send_key)?,
667 notes: self.notes.encrypt(ctx, send_key)?,
668 key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
669 password: self.new_password.as_ref().map(|password| {
676 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
677 B64::from(password.as_slice()).to_string()
678 }),
679
680 r#type: self.r#type,
681 file: self.file.encrypt_composite(ctx, send_key)?,
682 text: self.text.encrypt_composite(ctx, send_key)?,
683 data: self.data.encrypt_composite(ctx, send_key)?,
684
685 max_access_count: self.max_access_count,
686 access_count: self.access_count,
687 disabled: self.disabled,
688 hide_email: self.hide_email,
689
690 revision_date: self.revision_date,
691 deletion_date: self.deletion_date,
692 expiration_date: self.expiration_date,
693
694 emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
695 auth_type: self.auth_type,
696 })
697 }
698}
699
700impl TryFrom<SendResponseModel> for Send {
701 type Error = SendParseError;
702
703 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
704 let auth_type = match send.auth_type {
705 Some(t) => t.try_into()?,
706 None => {
707 if send.password.is_some() {
708 AuthType::Password
709 } else if send.emails.is_some() {
710 AuthType::Email
711 } else {
712 AuthType::None
713 }
714 }
715 };
716 Ok(Send {
717 id: send.id.map(SendId::new),
718 access_id: send.access_id,
719 name: require!(send.name).parse()?,
720 notes: EncString::try_from_optional(send.notes)?,
721 key: require!(send.key).parse()?,
722 password: send.password,
723 r#type: require!(send.r#type).try_into()?,
724 file: send.file.map(|f| (*f).try_into()).transpose()?,
725 text: send.text.map(|t| (*t).try_into()).transpose()?,
726 data: send.data.map(|d| (*d).try_into()).transpose()?,
727 max_access_count: send.max_access_count.map(|s| s as u32),
728 access_count: require!(send.access_count) as u32,
729 disabled: send.disabled.unwrap_or(false),
730 hide_email: send.hide_email.unwrap_or(false),
731 revision_date: require!(send.revision_date).parse()?,
732 deletion_date: require!(send.deletion_date).parse()?,
733 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
734 emails: send.emails,
735 auth_type,
736 })
737 }
738}
739
740impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
741 type Error = bitwarden_core::MissingFieldError;
742
743 fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
744 Ok(match t {
745 bitwarden_api_api::models::SendType::Text => SendType::Text,
746 bitwarden_api_api::models::SendType::File => SendType::File,
747 bitwarden_api_api::models::SendType::Item => SendType::Item,
748 bitwarden_api_api::models::SendType::__Unknown(_) => {
749 return Err(bitwarden_core::MissingFieldError("type"));
750 }
751 })
752 }
753}
754
755impl From<SendType> for bitwarden_api_api::models::SendType {
756 fn from(t: SendType) -> Self {
757 match t {
758 SendType::Text => bitwarden_api_api::models::SendType::Text,
759 SendType::File => bitwarden_api_api::models::SendType::File,
760 SendType::Item => bitwarden_api_api::models::SendType::Item,
761 }
762 }
763}
764
765impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
766 type Error = bitwarden_core::MissingFieldError;
767
768 fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
769 Ok(match value {
770 bitwarden_api_api::models::AuthType::Email => AuthType::Email,
771 bitwarden_api_api::models::AuthType::Password => AuthType::Password,
772 bitwarden_api_api::models::AuthType::None => AuthType::None,
773 bitwarden_api_api::models::AuthType::__Unknown(_) => {
774 return Err(bitwarden_core::MissingFieldError("auth_type"));
775 }
776 })
777 }
778}
779
780impl From<AuthType> for bitwarden_api_api::models::AuthType {
781 fn from(value: AuthType) -> Self {
782 match value {
783 AuthType::Email => bitwarden_api_api::models::AuthType::Email,
784 AuthType::Password => bitwarden_api_api::models::AuthType::Password,
785 AuthType::None => bitwarden_api_api::models::AuthType::None,
786 }
787 }
788}
789
790impl From<SendFile> for SendFileModel {
791 fn from(file: SendFile) -> Self {
792 SendFileModel {
793 id: file.id,
794 file_name: Some(file.file_name.to_string()),
795 size: file.size,
796 size_name: file.size_name,
797 }
798 }
799}
800
801impl From<SendEncryptionType> for bitwarden_api_api::models::SendEncryptionType {
802 fn from(t: SendEncryptionType) -> Self {
803 match t {
804 SendEncryptionType::V1 => bitwarden_api_api::models::SendEncryptionType::V1,
805 }
806 }
807}
808
809impl TryFrom<bitwarden_api_api::models::SendEncryptionType> for SendEncryptionType {
810 type Error = bitwarden_core::MissingFieldError;
811
812 fn try_from(value: bitwarden_api_api::models::SendEncryptionType) -> Result<Self, Self::Error> {
813 Ok(match value {
814 bitwarden_api_api::models::SendEncryptionType::V1 => SendEncryptionType::V1,
815 bitwarden_api_api::models::SendEncryptionType::__Unknown(_) => {
816 return Err(bitwarden_core::MissingFieldError("encryption_version"));
817 }
818 })
819 }
820}
821
822impl From<SendText> for SendTextModel {
823 fn from(text: SendText) -> Self {
824 SendTextModel {
825 text: text.text.map(|text| text.to_string()),
826 hidden: Some(text.hidden),
827 }
828 }
829}
830
831impl TryFrom<SendFileModel> for SendFile {
832 type Error = SendParseError;
833
834 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
835 Ok(SendFile {
836 id: file.id,
837 file_name: require!(file.file_name).parse()?,
838 size: file.size.map(|v| v.to_string()),
839 size_name: file.size_name,
840 })
841 }
842}
843
844impl TryFrom<SendTextModel> for SendText {
845 type Error = SendParseError;
846
847 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
848 Ok(SendText {
849 text: EncString::try_from_optional(text.text)?,
850 hidden: text.hidden.unwrap_or(false),
851 })
852 }
853}
854
855impl TryFrom<SendDataModel> for SendItem {
856 type Error = SendParseError;
857
858 fn try_from(data: SendDataModel) -> Result<Self, Self::Error> {
859 let cipher = serde_json::from_str::<Cipher>(data.data.unwrap_or("{}".to_string()).as_str());
860 match cipher {
861 Err(_e) => Err(SendParseError::DeserializationFailure(
862 SendItemDeserializationFailureError,
863 )),
864 Ok(c) => Ok(SendItem {
865 encryption_version: SendEncryptionType::try_from(
866 data.encryption_version
867 .unwrap_or(DEFAULT_SEND_ENCRYPTION.into()),
868 )?,
869 data: c,
870 }),
871 }
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use bitwarden_core::key_management::create_test_crypto_with_user_key;
878 use bitwarden_crypto::SymmetricCryptoKey;
879
880 use super::*;
881
882 #[test]
883 fn test_get_send_key() {
884 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
886 let crypto = create_test_crypto_with_user_key(user_key);
887 let mut ctx = crypto.context();
888
889 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
890 .parse()
891 .unwrap();
892
893 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
895 #[allow(deprecated)]
896 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
897 let send_key_b64 = send_key.to_base64();
898 assert_eq!(
899 send_key_b64.to_string(),
900 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
901 );
902 }
903
904 #[test]
905 pub fn test_decrypt() {
906 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
907 let crypto = create_test_crypto_with_user_key(user_key);
908
909 let send = Send {
910 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
911 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
912 r#type: SendType::Text,
913 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
914 .unwrap(),
915 notes: None,
916 file: None,
917 text: Some(SendText {
918 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
919 hidden: false,
920 }),
921 data: None,
922 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
923 max_access_count: None,
924 access_count: 0,
925 password: None,
926 disabled: false,
927 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
928 expiration_date: None,
929 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
930 hide_email: false,
931 emails: None,
932 auth_type: AuthType::None,
933 };
934
935 let view: SendView = crypto.decrypt(&send).unwrap();
936
937 let expected = SendView {
938 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
939 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
940 name: "Test".to_string(),
941 notes: None,
942 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
943 new_password: None,
944 has_password: false,
945 r#type: SendType::Text,
946 file: None,
947 text: Some(SendTextView {
948 text: Some("This is a test".to_owned()),
949 hidden: false,
950 }),
951 data: None,
952 max_access_count: None,
953 access_count: 0,
954 disabled: false,
955 hide_email: false,
956 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
957 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
958 expiration_date: None,
959 emails: Vec::new(),
960 auth_type: AuthType::None,
961 };
962
963 assert_eq!(view, expected);
964 }
965
966 #[test]
967 pub fn test_encrypt() {
968 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
969 let crypto = create_test_crypto_with_user_key(user_key);
970
971 let view = SendView {
972 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
973 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
974 name: "Test".to_string(),
975 notes: None,
976 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
977 new_password: None,
978 has_password: false,
979 r#type: SendType::Text,
980 file: None,
981 text: Some(SendTextView {
982 text: Some("This is a test".to_owned()),
983 hidden: false,
984 }),
985 data: None,
986 max_access_count: None,
987 access_count: 0,
988 disabled: false,
989 hide_email: false,
990 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
991 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
992 expiration_date: None,
993 emails: Vec::new(),
994 auth_type: AuthType::None,
995 };
996
997 let v: SendView = crypto
999 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1000 .unwrap();
1001 assert_eq!(v, view);
1002 }
1003
1004 #[test]
1005 pub fn test_create() {
1006 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1007 let crypto = create_test_crypto_with_user_key(user_key);
1008
1009 let view = SendView {
1010 id: None,
1011 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1012 name: "Test".to_string(),
1013 notes: None,
1014 key: None,
1015 new_password: None,
1016 has_password: false,
1017 r#type: SendType::Text,
1018 file: None,
1019 text: Some(SendTextView {
1020 text: Some("This is a test".to_owned()),
1021 hidden: false,
1022 }),
1023 data: None,
1024 max_access_count: None,
1025 access_count: 0,
1026 disabled: false,
1027 hide_email: false,
1028 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1029 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1030 expiration_date: None,
1031 emails: Vec::new(),
1032 auth_type: AuthType::None,
1033 };
1034
1035 let v: SendView = crypto
1037 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1038 .unwrap();
1039
1040 let t = SendView { key: None, ..v };
1042 assert_eq!(t, view);
1043 }
1044
1045 #[test]
1046 pub fn test_create_password() {
1047 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1048 let crypto = create_test_crypto_with_user_key(user_key);
1049
1050 let view = SendView {
1051 id: None,
1052 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1053 name: "Test".to_owned(),
1054 notes: None,
1055 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1056 new_password: Some("abc123".to_owned()),
1057 has_password: false,
1058 r#type: SendType::Text,
1059 file: None,
1060 text: Some(SendTextView {
1061 text: Some("This is a test".to_owned()),
1062 hidden: false,
1063 }),
1064 data: None,
1065 max_access_count: None,
1066 access_count: 0,
1067 disabled: false,
1068 hide_email: false,
1069 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1070 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1071 expiration_date: None,
1072 emails: Vec::new(),
1073 auth_type: AuthType::Password,
1074 };
1075
1076 let send: Send = crypto.encrypt(view).unwrap();
1077
1078 assert_eq!(
1079 send.password,
1080 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
1081 );
1082 assert_eq!(send.auth_type, AuthType::Password);
1083
1084 let v: SendView = crypto.decrypt(&send).unwrap();
1085 assert_eq!(v.new_password, None);
1086 assert!(v.has_password);
1087 assert_eq!(v.auth_type, AuthType::Password);
1088 }
1089
1090 #[test]
1091 pub fn test_create_email_otp() {
1092 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1093 let crypto = create_test_crypto_with_user_key(user_key);
1094
1095 let view = SendView {
1096 id: None,
1097 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1098 name: "Test".to_owned(),
1099 notes: None,
1100 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1101 new_password: None,
1102 has_password: false,
1103 r#type: SendType::Text,
1104 file: None,
1105 text: Some(SendTextView {
1106 text: Some("This is a test".to_owned()),
1107 hidden: false,
1108 }),
1109 data: None,
1110 max_access_count: None,
1111 access_count: 0,
1112 disabled: false,
1113 hide_email: false,
1114 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1115 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1116 expiration_date: None,
1117 emails: vec![
1118 String::from("[email protected]"),
1119 String::from("[email protected]"),
1120 ],
1121 auth_type: AuthType::Email,
1122 };
1123
1124 let send: Send = crypto.encrypt(view.clone()).unwrap();
1125
1126 let v: SendView = crypto.decrypt(&send).unwrap();
1128
1129 assert_eq!(v, view);
1130 }
1131
1132 #[test]
1133 fn test_send_into_send_with_id_request_model() {
1134 let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
1135 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
1136 .unwrap()
1137 .with_timezone(&Utc);
1138 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
1139 .unwrap()
1140 .with_timezone(&Utc);
1141 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
1142 .unwrap()
1143 .with_timezone(&Utc);
1144
1145 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
1146 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1147 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
1148 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
1149 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1150
1151 let send = Send {
1152 id: Some(SendId::new(send_id)),
1153 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
1154 name: name.parse().unwrap(),
1155 notes: Some(notes.parse().unwrap()),
1156 key: key.parse().unwrap(),
1157 password: Some("hash".to_string()),
1158 r#type: SendType::File,
1159 file: Some(SendFile {
1160 id: Some("file-id".to_string()),
1161 file_name: file_name.parse().unwrap(),
1162 size: Some("1234".to_string()),
1163 size_name: Some("1.2 KB".to_string()),
1164 }),
1165 text: Some(SendText {
1166 text: Some(text_value.parse().unwrap()),
1167 hidden: true,
1168 }),
1169 data: None,
1170 max_access_count: Some(42),
1171 access_count: 0,
1172 disabled: true,
1173 hide_email: true,
1174 revision_date,
1175 deletion_date,
1176 expiration_date: Some(expiration_date),
1177 emails: Some("[email protected],[email protected]".to_string()),
1178 auth_type: AuthType::Email,
1179 };
1180
1181 let model: SendWithIdRequestModel = send.into();
1182
1183 assert_eq!(model.id, send_id);
1184 assert_eq!(
1185 model.r#type,
1186 Some(bitwarden_api_api::models::SendType::File)
1187 );
1188 assert_eq!(
1189 model.auth_type,
1190 Some(bitwarden_api_api::models::AuthType::Email)
1191 );
1192 assert_eq!(model.file_length, Some(1234));
1193 assert_eq!(model.name.as_deref(), Some(name));
1194 assert_eq!(model.notes.as_deref(), Some(notes));
1195 assert_eq!(model.key, key);
1196 assert_eq!(model.max_access_count, Some(42));
1197 assert_eq!(
1198 model
1199 .expiration_date
1200 .unwrap()
1201 .parse::<DateTime<Utc>>()
1202 .unwrap(),
1203 expiration_date
1204 );
1205 assert_eq!(
1206 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1207 deletion_date
1208 );
1209 assert_eq!(model.password.as_deref(), Some("hash"));
1210 assert_eq!(
1211 model.emails.as_deref(),
1212 Some("[email protected],[email protected]")
1213 );
1214 assert!(model.disabled);
1215 assert_eq!(model.hide_email, Some(true));
1216
1217 let file = model.file.unwrap();
1218 assert_eq!(file.id.as_deref(), Some("file-id"));
1219 assert_eq!(file.file_name.as_deref(), Some(file_name));
1220 assert_eq!(file.size.as_deref(), Some("1234"));
1221 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1222
1223 let text = model.text.unwrap();
1224 assert_eq!(text.text.as_deref(), Some(text_value));
1225 assert_eq!(text.hidden, Some(true));
1226 }
1227
1228 #[test]
1229 fn auth_data_hashed_password_returns_key_b64_verbatim() {
1230 let key_b64 = "pretend-this-is-a-pbkdf2-output==".to_string();
1235 let auth = SendAuthType::HashedPassword {
1236 key_b64: key_b64.clone(),
1237 };
1238
1239 let (password, emails) = auth.auth_data(b"any-send-key-bytes-here");
1240
1241 assert_eq!(password, Some(key_b64));
1242 assert_eq!(emails, None);
1243 }
1244
1245 #[test]
1246 fn auth_data_hashed_and_plaintext_diverge_for_same_input() {
1247 let same_string = "abc123".to_string();
1252 let send_key = b"send-key-salt-bytes";
1253
1254 let (plaintext_out, _) = SendAuthType::Password {
1255 password: same_string.clone(),
1256 }
1257 .auth_data(send_key);
1258 let (hashed_out, _) = SendAuthType::HashedPassword {
1259 key_b64: same_string,
1260 }
1261 .auth_data(send_key);
1262
1263 assert_ne!(
1264 plaintext_out, hashed_out,
1265 "Plaintext path must run PBKDF2; HashedPassword path must not"
1266 );
1267 }
1268
1269 #[test]
1270 fn auth_type_for_hashed_password_maps_to_password() {
1271 assert_eq!(
1274 SendAuthType::Password {
1275 password: "p".to_string()
1276 }
1277 .auth_type(),
1278 AuthType::Password,
1279 );
1280 assert_eq!(
1281 SendAuthType::HashedPassword {
1282 key_b64: "k".to_string()
1283 }
1284 .auth_type(),
1285 AuthType::Password,
1286 );
1287 }
1288
1289 #[test]
1293 fn send_auth_type_round_trips_through_json() {
1294 let cases = [
1295 (SendAuthType::None, serde_json::json!({"type": "none"})),
1296 (
1297 SendAuthType::Password {
1298 password: "hunter2".to_string(),
1299 },
1300 serde_json::json!({"type": "password", "password": "hunter2"}),
1301 ),
1302 (
1303 SendAuthType::HashedPassword {
1304 key_b64: "deadbeef==".to_string(),
1305 },
1306 serde_json::json!({"type": "hashedPassword", "keyB64": "deadbeef=="}),
1307 ),
1308 (
1309 SendAuthType::Emails {
1310 emails: vec!["[email protected]".to_string()],
1311 },
1312 serde_json::json!({"type": "emails", "emails": ["[email protected]"]}),
1313 ),
1314 ];
1315 for (value, expected) in cases {
1316 let serialized = serde_json::to_value(&value).expect("serialize");
1317 assert_eq!(serialized, expected, "wire shape mismatch for {value:?}");
1318 let deserialized: SendAuthType =
1319 serde_json::from_value(serialized).expect("round-trip");
1320 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
1321 }
1322 }
1323
1324 #[test]
1325 fn typed_constructors_produce_expected_variants() {
1326 assert_eq!(
1327 SendAuthType::from_plaintext_password("hunter2".to_string()),
1328 SendAuthType::Password {
1329 password: "hunter2".to_string()
1330 },
1331 );
1332 assert_eq!(
1333 SendAuthType::from_hashed_password("deadbeef==".to_string()),
1334 SendAuthType::HashedPassword {
1335 key_b64: "deadbeef==".to_string()
1336 },
1337 );
1338 }
1339}