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| {
670 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
671 B64::from(password.as_slice()).to_string()
672 }),
673
674 r#type: self.r#type,
675 file: self.file.encrypt_composite(ctx, send_key)?,
676 text: self.text.encrypt_composite(ctx, send_key)?,
677 data: self.data.encrypt_composite(ctx, send_key)?,
678
679 max_access_count: self.max_access_count,
680 access_count: self.access_count,
681 disabled: self.disabled,
682 hide_email: self.hide_email,
683
684 revision_date: self.revision_date,
685 deletion_date: self.deletion_date,
686 expiration_date: self.expiration_date,
687
688 emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
689 auth_type: self.auth_type,
690 })
691 }
692}
693
694impl TryFrom<SendResponseModel> for Send {
695 type Error = SendParseError;
696
697 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
698 let auth_type = match send.auth_type {
699 Some(t) => t.try_into()?,
700 None => {
701 if send.password.is_some() {
702 AuthType::Password
703 } else if send.emails.is_some() {
704 AuthType::Email
705 } else {
706 AuthType::None
707 }
708 }
709 };
710 Ok(Send {
711 id: send.id.map(SendId::new),
712 access_id: send.access_id,
713 name: require!(send.name).parse()?,
714 notes: EncString::try_from_optional(send.notes)?,
715 key: require!(send.key).parse()?,
716 password: send.password,
717 r#type: require!(send.r#type).try_into()?,
718 file: send.file.map(|f| (*f).try_into()).transpose()?,
719 text: send.text.map(|t| (*t).try_into()).transpose()?,
720 data: send.data.map(|d| (*d).try_into()).transpose()?,
721 max_access_count: send.max_access_count.map(|s| s as u32),
722 access_count: require!(send.access_count) as u32,
723 disabled: send.disabled.unwrap_or(false),
724 hide_email: send.hide_email.unwrap_or(false),
725 revision_date: require!(send.revision_date).parse()?,
726 deletion_date: require!(send.deletion_date).parse()?,
727 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
728 emails: send.emails,
729 auth_type,
730 })
731 }
732}
733
734impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
735 type Error = bitwarden_core::MissingFieldError;
736
737 fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
738 Ok(match t {
739 bitwarden_api_api::models::SendType::Text => SendType::Text,
740 bitwarden_api_api::models::SendType::File => SendType::File,
741 bitwarden_api_api::models::SendType::Item => SendType::Item,
742 bitwarden_api_api::models::SendType::__Unknown(_) => {
743 return Err(bitwarden_core::MissingFieldError("type"));
744 }
745 })
746 }
747}
748
749impl From<SendType> for bitwarden_api_api::models::SendType {
750 fn from(t: SendType) -> Self {
751 match t {
752 SendType::Text => bitwarden_api_api::models::SendType::Text,
753 SendType::File => bitwarden_api_api::models::SendType::File,
754 SendType::Item => bitwarden_api_api::models::SendType::Item,
755 }
756 }
757}
758
759impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
760 type Error = bitwarden_core::MissingFieldError;
761
762 fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
763 Ok(match value {
764 bitwarden_api_api::models::AuthType::Email => AuthType::Email,
765 bitwarden_api_api::models::AuthType::Password => AuthType::Password,
766 bitwarden_api_api::models::AuthType::None => AuthType::None,
767 bitwarden_api_api::models::AuthType::__Unknown(_) => {
768 return Err(bitwarden_core::MissingFieldError("auth_type"));
769 }
770 })
771 }
772}
773
774impl From<AuthType> for bitwarden_api_api::models::AuthType {
775 fn from(value: AuthType) -> Self {
776 match value {
777 AuthType::Email => bitwarden_api_api::models::AuthType::Email,
778 AuthType::Password => bitwarden_api_api::models::AuthType::Password,
779 AuthType::None => bitwarden_api_api::models::AuthType::None,
780 }
781 }
782}
783
784impl From<SendFile> for SendFileModel {
785 fn from(file: SendFile) -> Self {
786 SendFileModel {
787 id: file.id,
788 file_name: Some(file.file_name.to_string()),
789 size: file.size,
790 size_name: file.size_name,
791 }
792 }
793}
794
795impl From<SendEncryptionType> for bitwarden_api_api::models::SendEncryptionType {
796 fn from(t: SendEncryptionType) -> Self {
797 match t {
798 SendEncryptionType::V1 => bitwarden_api_api::models::SendEncryptionType::V1,
799 }
800 }
801}
802
803impl TryFrom<bitwarden_api_api::models::SendEncryptionType> for SendEncryptionType {
804 type Error = bitwarden_core::MissingFieldError;
805
806 fn try_from(value: bitwarden_api_api::models::SendEncryptionType) -> Result<Self, Self::Error> {
807 Ok(match value {
808 bitwarden_api_api::models::SendEncryptionType::V1 => SendEncryptionType::V1,
809 bitwarden_api_api::models::SendEncryptionType::__Unknown(_) => {
810 return Err(bitwarden_core::MissingFieldError("encryption_version"));
811 }
812 })
813 }
814}
815
816impl From<SendText> for SendTextModel {
817 fn from(text: SendText) -> Self {
818 SendTextModel {
819 text: text.text.map(|text| text.to_string()),
820 hidden: Some(text.hidden),
821 }
822 }
823}
824
825impl TryFrom<SendFileModel> for SendFile {
826 type Error = SendParseError;
827
828 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
829 Ok(SendFile {
830 id: file.id,
831 file_name: require!(file.file_name).parse()?,
832 size: file.size.map(|v| v.to_string()),
833 size_name: file.size_name,
834 })
835 }
836}
837
838impl TryFrom<SendTextModel> for SendText {
839 type Error = SendParseError;
840
841 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
842 Ok(SendText {
843 text: EncString::try_from_optional(text.text)?,
844 hidden: text.hidden.unwrap_or(false),
845 })
846 }
847}
848
849impl TryFrom<SendDataModel> for SendItem {
850 type Error = SendParseError;
851
852 fn try_from(data: SendDataModel) -> Result<Self, Self::Error> {
853 let cipher = serde_json::from_str::<Cipher>(data.data.unwrap_or("{}".to_string()).as_str());
854 match cipher {
855 Err(_e) => Err(SendParseError::DeserializationFailure(
856 SendItemDeserializationFailureError,
857 )),
858 Ok(c) => Ok(SendItem {
859 encryption_version: SendEncryptionType::try_from(
860 data.encryption_version
861 .unwrap_or(DEFAULT_SEND_ENCRYPTION.into()),
862 )?,
863 data: c,
864 }),
865 }
866 }
867}
868
869#[cfg(test)]
870mod tests {
871 use bitwarden_core::key_management::create_test_crypto_with_user_key;
872 use bitwarden_crypto::SymmetricCryptoKey;
873
874 use super::*;
875
876 #[test]
877 fn test_get_send_key() {
878 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
880 let crypto = create_test_crypto_with_user_key(user_key);
881 let mut ctx = crypto.context();
882
883 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
884 .parse()
885 .unwrap();
886
887 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
889 #[allow(deprecated)]
890 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
891 let send_key_b64 = send_key.to_base64();
892 assert_eq!(
893 send_key_b64.to_string(),
894 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
895 );
896 }
897
898 #[test]
899 pub fn test_decrypt() {
900 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
901 let crypto = create_test_crypto_with_user_key(user_key);
902
903 let send = Send {
904 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
905 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
906 r#type: SendType::Text,
907 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
908 .unwrap(),
909 notes: None,
910 file: None,
911 text: Some(SendText {
912 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
913 hidden: false,
914 }),
915 data: None,
916 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
917 max_access_count: None,
918 access_count: 0,
919 password: None,
920 disabled: false,
921 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
922 expiration_date: None,
923 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
924 hide_email: false,
925 emails: None,
926 auth_type: AuthType::None,
927 };
928
929 let view: SendView = crypto.decrypt(&send).unwrap();
930
931 let expected = SendView {
932 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
933 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
934 name: "Test".to_string(),
935 notes: None,
936 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
937 new_password: None,
938 has_password: false,
939 r#type: SendType::Text,
940 file: None,
941 text: Some(SendTextView {
942 text: Some("This is a test".to_owned()),
943 hidden: false,
944 }),
945 data: None,
946 max_access_count: None,
947 access_count: 0,
948 disabled: false,
949 hide_email: false,
950 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
951 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
952 expiration_date: None,
953 emails: Vec::new(),
954 auth_type: AuthType::None,
955 };
956
957 assert_eq!(view, expected);
958 }
959
960 #[test]
961 pub fn test_encrypt() {
962 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
963 let crypto = create_test_crypto_with_user_key(user_key);
964
965 let view = SendView {
966 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
967 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
968 name: "Test".to_string(),
969 notes: None,
970 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
971 new_password: None,
972 has_password: false,
973 r#type: SendType::Text,
974 file: None,
975 text: Some(SendTextView {
976 text: Some("This is a test".to_owned()),
977 hidden: false,
978 }),
979 data: None,
980 max_access_count: None,
981 access_count: 0,
982 disabled: false,
983 hide_email: false,
984 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
985 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
986 expiration_date: None,
987 emails: Vec::new(),
988 auth_type: AuthType::None,
989 };
990
991 let v: SendView = crypto
993 .decrypt(&crypto.encrypt(view.clone()).unwrap())
994 .unwrap();
995 assert_eq!(v, view);
996 }
997
998 #[test]
999 pub fn test_create() {
1000 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1001 let crypto = create_test_crypto_with_user_key(user_key);
1002
1003 let view = SendView {
1004 id: None,
1005 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1006 name: "Test".to_string(),
1007 notes: None,
1008 key: None,
1009 new_password: None,
1010 has_password: false,
1011 r#type: SendType::Text,
1012 file: None,
1013 text: Some(SendTextView {
1014 text: Some("This is a test".to_owned()),
1015 hidden: false,
1016 }),
1017 data: None,
1018 max_access_count: None,
1019 access_count: 0,
1020 disabled: false,
1021 hide_email: false,
1022 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1023 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1024 expiration_date: None,
1025 emails: Vec::new(),
1026 auth_type: AuthType::None,
1027 };
1028
1029 let v: SendView = crypto
1031 .decrypt(&crypto.encrypt(view.clone()).unwrap())
1032 .unwrap();
1033
1034 let t = SendView { key: None, ..v };
1036 assert_eq!(t, view);
1037 }
1038
1039 #[test]
1040 pub fn test_create_password() {
1041 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1042 let crypto = create_test_crypto_with_user_key(user_key);
1043
1044 let view = SendView {
1045 id: None,
1046 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1047 name: "Test".to_owned(),
1048 notes: None,
1049 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1050 new_password: Some("abc123".to_owned()),
1051 has_password: false,
1052 r#type: SendType::Text,
1053 file: None,
1054 text: Some(SendTextView {
1055 text: Some("This is a test".to_owned()),
1056 hidden: false,
1057 }),
1058 data: None,
1059 max_access_count: None,
1060 access_count: 0,
1061 disabled: false,
1062 hide_email: false,
1063 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1064 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1065 expiration_date: None,
1066 emails: Vec::new(),
1067 auth_type: AuthType::Password,
1068 };
1069
1070 let send: Send = crypto.encrypt(view).unwrap();
1071
1072 assert_eq!(
1073 send.password,
1074 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
1075 );
1076 assert_eq!(send.auth_type, AuthType::Password);
1077
1078 let v: SendView = crypto.decrypt(&send).unwrap();
1079 assert_eq!(v.new_password, None);
1080 assert!(v.has_password);
1081 assert_eq!(v.auth_type, AuthType::Password);
1082 }
1083
1084 #[test]
1085 pub fn test_create_email_otp() {
1086 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
1087 let crypto = create_test_crypto_with_user_key(user_key);
1088
1089 let view = SendView {
1090 id: None,
1091 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
1092 name: "Test".to_owned(),
1093 notes: None,
1094 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
1095 new_password: None,
1096 has_password: false,
1097 r#type: SendType::Text,
1098 file: None,
1099 text: Some(SendTextView {
1100 text: Some("This is a test".to_owned()),
1101 hidden: false,
1102 }),
1103 data: None,
1104 max_access_count: None,
1105 access_count: 0,
1106 disabled: false,
1107 hide_email: false,
1108 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
1109 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
1110 expiration_date: None,
1111 emails: vec![
1112 String::from("[email protected]"),
1113 String::from("[email protected]"),
1114 ],
1115 auth_type: AuthType::Email,
1116 };
1117
1118 let send: Send = crypto.encrypt(view.clone()).unwrap();
1119
1120 let v: SendView = crypto.decrypt(&send).unwrap();
1122
1123 assert_eq!(v, view);
1124 }
1125
1126 #[test]
1127 fn test_send_into_send_with_id_request_model() {
1128 let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
1129 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
1130 .unwrap()
1131 .with_timezone(&Utc);
1132 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
1133 .unwrap()
1134 .with_timezone(&Utc);
1135 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
1136 .unwrap()
1137 .with_timezone(&Utc);
1138
1139 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
1140 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1141 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
1142 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
1143 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
1144
1145 let send = Send {
1146 id: Some(SendId::new(send_id)),
1147 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
1148 name: name.parse().unwrap(),
1149 notes: Some(notes.parse().unwrap()),
1150 key: key.parse().unwrap(),
1151 password: Some("hash".to_string()),
1152 r#type: SendType::File,
1153 file: Some(SendFile {
1154 id: Some("file-id".to_string()),
1155 file_name: file_name.parse().unwrap(),
1156 size: Some("1234".to_string()),
1157 size_name: Some("1.2 KB".to_string()),
1158 }),
1159 text: Some(SendText {
1160 text: Some(text_value.parse().unwrap()),
1161 hidden: true,
1162 }),
1163 data: None,
1164 max_access_count: Some(42),
1165 access_count: 0,
1166 disabled: true,
1167 hide_email: true,
1168 revision_date,
1169 deletion_date,
1170 expiration_date: Some(expiration_date),
1171 emails: Some("[email protected],[email protected]".to_string()),
1172 auth_type: AuthType::Email,
1173 };
1174
1175 let model: SendWithIdRequestModel = send.into();
1176
1177 assert_eq!(model.id, send_id);
1178 assert_eq!(
1179 model.r#type,
1180 Some(bitwarden_api_api::models::SendType::File)
1181 );
1182 assert_eq!(
1183 model.auth_type,
1184 Some(bitwarden_api_api::models::AuthType::Email)
1185 );
1186 assert_eq!(model.file_length, Some(1234));
1187 assert_eq!(model.name.as_deref(), Some(name));
1188 assert_eq!(model.notes.as_deref(), Some(notes));
1189 assert_eq!(model.key, key);
1190 assert_eq!(model.max_access_count, Some(42));
1191 assert_eq!(
1192 model
1193 .expiration_date
1194 .unwrap()
1195 .parse::<DateTime<Utc>>()
1196 .unwrap(),
1197 expiration_date
1198 );
1199 assert_eq!(
1200 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1201 deletion_date
1202 );
1203 assert_eq!(model.password.as_deref(), Some("hash"));
1204 assert_eq!(
1205 model.emails.as_deref(),
1206 Some("[email protected],[email protected]")
1207 );
1208 assert!(model.disabled);
1209 assert_eq!(model.hide_email, Some(true));
1210
1211 let file = model.file.unwrap();
1212 assert_eq!(file.id.as_deref(), Some("file-id"));
1213 assert_eq!(file.file_name.as_deref(), Some(file_name));
1214 assert_eq!(file.size.as_deref(), Some("1234"));
1215 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1216
1217 let text = model.text.unwrap();
1218 assert_eq!(text.text.as_deref(), Some(text_value));
1219 assert_eq!(text.hidden, Some(true));
1220 }
1221
1222 #[test]
1223 fn auth_data_hashed_password_returns_key_b64_verbatim() {
1224 let key_b64 = "pretend-this-is-a-pbkdf2-output==".to_string();
1229 let auth = SendAuthType::HashedPassword {
1230 key_b64: key_b64.clone(),
1231 };
1232
1233 let (password, emails) = auth.auth_data(b"any-send-key-bytes-here");
1234
1235 assert_eq!(password, Some(key_b64));
1236 assert_eq!(emails, None);
1237 }
1238
1239 #[test]
1240 fn auth_data_hashed_and_plaintext_diverge_for_same_input() {
1241 let same_string = "abc123".to_string();
1246 let send_key = b"send-key-salt-bytes";
1247
1248 let (plaintext_out, _) = SendAuthType::Password {
1249 password: same_string.clone(),
1250 }
1251 .auth_data(send_key);
1252 let (hashed_out, _) = SendAuthType::HashedPassword {
1253 key_b64: same_string,
1254 }
1255 .auth_data(send_key);
1256
1257 assert_ne!(
1258 plaintext_out, hashed_out,
1259 "Plaintext path must run PBKDF2; HashedPassword path must not"
1260 );
1261 }
1262
1263 #[test]
1264 fn auth_type_for_hashed_password_maps_to_password() {
1265 assert_eq!(
1268 SendAuthType::Password {
1269 password: "p".to_string()
1270 }
1271 .auth_type(),
1272 AuthType::Password,
1273 );
1274 assert_eq!(
1275 SendAuthType::HashedPassword {
1276 key_b64: "k".to_string()
1277 }
1278 .auth_type(),
1279 AuthType::Password,
1280 );
1281 }
1282
1283 #[test]
1287 fn send_auth_type_round_trips_through_json() {
1288 let cases = [
1289 (SendAuthType::None, serde_json::json!({"type": "none"})),
1290 (
1291 SendAuthType::Password {
1292 password: "hunter2".to_string(),
1293 },
1294 serde_json::json!({"type": "password", "password": "hunter2"}),
1295 ),
1296 (
1297 SendAuthType::HashedPassword {
1298 key_b64: "deadbeef==".to_string(),
1299 },
1300 serde_json::json!({"type": "hashedPassword", "keyB64": "deadbeef=="}),
1301 ),
1302 (
1303 SendAuthType::Emails {
1304 emails: vec!["[email protected]".to_string()],
1305 },
1306 serde_json::json!({"type": "emails", "emails": ["[email protected]"]}),
1307 ),
1308 ];
1309 for (value, expected) in cases {
1310 let serialized = serde_json::to_value(&value).expect("serialize");
1311 assert_eq!(serialized, expected, "wire shape mismatch for {value:?}");
1312 let deserialized: SendAuthType =
1313 serde_json::from_value(serialized).expect("round-trip");
1314 assert_eq!(deserialized, value, "round-trip mismatch for {value:?}");
1315 }
1316 }
1317
1318 #[test]
1319 fn typed_constructors_produce_expected_variants() {
1320 assert_eq!(
1321 SendAuthType::from_plaintext_password("hunter2".to_string()),
1322 SendAuthType::Password {
1323 password: "hunter2".to_string()
1324 },
1325 );
1326 assert_eq!(
1327 SendAuthType::from_hashed_password("deadbeef==".to_string()),
1328 SendAuthType::HashedPassword {
1329 key_b64: "deadbeef==".to_string()
1330 },
1331 );
1332 }
1333}