Skip to main content

bitwarden_send/
send.rs

1use bitwarden_api_api::models::{
2    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 chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use serde_repr::{Deserialize_repr, Serialize_repr};
17use thiserror::Error;
18use zeroize::Zeroizing;
19#[cfg(feature = "wasm")]
20use {tsify::Tsify, wasm_bindgen::prelude::*};
21
22use crate::SendParseError;
23pub const SEND_ITERATIONS: u32 = 100_000;
24
25uuid_newtype!(pub SendId);
26
27/// Error returned when `SendAuthType::Emails` is constructed with an empty email list.
28#[derive(Debug, Error)]
29#[error("Email authentication requires at least one email address")]
30pub struct EmptyEmailListError;
31
32/// File-based send content
33#[derive(Serialize, Deserialize, Debug, Clone)]
34#[serde(rename_all = "camelCase", deny_unknown_fields)]
35#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
36#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
37pub struct SendFile {
38    /// The file's ID
39    pub id: Option<String>,
40    /// The encrypted file name
41    pub file_name: EncString,
42    /// The file size in bytes as a string
43    pub size: Option<String>,
44    /// Readable size, ex: "4.2 KB" or "1.43 GB"
45    pub size_name: Option<String>,
46}
47
48/// View model for decrypted SendFile
49#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53pub struct SendFileView {
54    /// The file's ID
55    pub id: Option<String>,
56    /// The file name
57    pub file_name: String,
58    /// The file size in bytes as a string
59    pub size: Option<String>,
60    /// Readable size, ex: "4.2 KB" or "1.43 GB"
61    pub size_name: Option<String>,
62}
63
64/// Text-based send content
65#[derive(Serialize, Deserialize, Debug, Clone)]
66#[serde(rename_all = "camelCase", deny_unknown_fields)]
67#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
68#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
69pub struct SendText {
70    pub text: Option<EncString>,
71    pub hidden: bool,
72}
73
74/// View model for decrypted SendText
75#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
78#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
79pub struct SendTextView {
80    /// The text content of the send
81    pub text: Option<String>,
82    /// Whether the text is hidden-by-default (masked as ********).
83    pub hidden: bool,
84}
85
86/// The type of Send, either text or file
87#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
88#[repr(u8)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
90#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
91pub enum SendType {
92    /// Text-based send
93    Text = 0,
94    /// File-based send
95    File = 1,
96}
97
98/// Indicates the authentication strategy to use when accessing a Send
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
100#[repr(u8)]
101#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
102#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
103pub enum AuthType {
104    /// Email-based OTP authentication
105    Email = 0,
106
107    /// Password-based authentication
108    Password = 1,
109
110    /// No authentication required
111    None = 2,
112}
113
114/// Type-safe authentication method for a Send, including the authentication data.
115/// This ensures that password and email authentication are mutually exclusive.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "camelCase")]
118#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
119#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
120pub enum SendAuthType {
121    /// No authentication required
122    None,
123    /// Password-based authentication
124    Password {
125        /// The password required to access the Send
126        password: String,
127    },
128    /// Email-based OTP authentication
129    Emails {
130        /// List of email addresses that will receive OTP codes
131        emails: Vec<String>,
132    },
133}
134
135impl SendAuthType {
136    /// Returns the AuthType discriminant for this authentication method
137    pub fn auth_type(&self) -> AuthType {
138        match self {
139            SendAuthType::None => AuthType::None,
140            SendAuthType::Password { .. } => AuthType::Password,
141            SendAuthType::Emails { .. } => AuthType::Email,
142        }
143    }
144
145    /// Validates that the auth configuration is valid.
146    /// Returns an error if `Emails` is used with an empty list.
147    pub(crate) fn validate(&self) -> Result<(), EmptyEmailListError> {
148        if let SendAuthType::Emails { emails } = self
149            && emails.is_empty()
150        {
151            return Err(EmptyEmailListError);
152        }
153        Ok(())
154    }
155
156    /// Returns the password if this is a Password variant, emails if this is an Emails variant, or
157    /// None otherwise
158    pub(crate) fn auth_data(&self, k: &[u8]) -> (Option<String>, Option<String>) {
159        match self {
160            SendAuthType::Password { password } => {
161                let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), k, SEND_ITERATIONS);
162                (Some(B64::from(hashed.as_slice()).to_string()), None)
163            }
164            SendAuthType::Emails { emails } => {
165                let emails_str = if emails.is_empty() {
166                    None
167                } else {
168                    Some(emails.join(","))
169                };
170                (None, emails_str)
171            }
172            SendAuthType::None => (None, None),
173        }
174    }
175}
176
177/// View model for decrypted Send type
178#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
179#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
180pub enum SendViewType {
181    /// File-based send
182    File(SendFileView),
183    /// Text-based send
184    Text(SendTextView),
185}
186
187/// Type alias for the tuple returned by SendViewType::into_api_models
188type SendApiModels = (
189    bitwarden_api_api::models::SendType,
190    Option<Box<bitwarden_api_api::models::SendFileModel>>,
191    Option<Box<bitwarden_api_api::models::SendTextModel>>,
192);
193
194impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendApiModels> for SendViewType {
195    fn encrypt_composite(
196        &self,
197        ctx: &mut KeyStoreContext<KeySlotIds>,
198        key: SymmetricKeySlotId,
199    ) -> Result<SendApiModels, CryptoError> {
200        match self {
201            SendViewType::File(f) => Ok((
202                bitwarden_api_api::models::SendType::File,
203                Some(Box::new(bitwarden_api_api::models::SendFileModel {
204                    id: f.id.clone(),
205                    file_name: Some(f.file_name.encrypt(ctx, key)?.to_string()),
206                    size: f.size.clone(),
207                    size_name: f.size_name.clone(),
208                })),
209                None,
210            )),
211            SendViewType::Text(t) => Ok((
212                bitwarden_api_api::models::SendType::Text,
213                None,
214                Some(Box::new(bitwarden_api_api::models::SendTextModel {
215                    text: t
216                        .text
217                        .as_ref()
218                        .map(|txt| txt.encrypt(ctx, key))
219                        .transpose()?
220                        .map(|e| e.to_string()),
221                    hidden: Some(t.hidden),
222                })),
223            )),
224        }
225    }
226}
227
228#[allow(missing_docs)]
229#[derive(Serialize, Deserialize, Debug, Clone)]
230#[serde(rename_all = "camelCase", deny_unknown_fields)]
231#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
232#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
233pub struct Send {
234    pub id: Option<SendId>,
235    pub access_id: Option<String>,
236
237    pub name: EncString,
238    pub notes: Option<EncString>,
239    pub key: EncString,
240    pub password: Option<String>,
241
242    pub r#type: SendType,
243    pub file: Option<SendFile>,
244    pub text: Option<SendText>,
245
246    pub max_access_count: Option<u32>,
247    pub access_count: u32,
248    pub disabled: bool,
249    pub hide_email: bool,
250
251    pub revision_date: DateTime<Utc>,
252    pub deletion_date: DateTime<Utc>,
253    pub expiration_date: Option<DateTime<Utc>>,
254
255    /// Email addresses for OTP authentication (comma-separated).
256    ///
257    /// **Note**: Mutually exclusive with `password`. If both `password` and `emails` are
258    /// set, password authentication takes precedence and email OTP is ignored.
259    pub emails: Option<String>,
260    pub auth_type: AuthType,
261}
262
263bitwarden_state::register_repository_item!(SendId => Send, "Send");
264
265impl From<Send> for SendWithIdRequestModel {
266    fn from(send: Send) -> Self {
267        let file_length = send.file.as_ref().and_then(|file| {
268            file.size
269                .as_deref()
270                .and_then(|size| size.parse::<i64>().ok())
271        });
272
273        SendWithIdRequestModel {
274            r#type: Some(send.r#type.into()),
275            auth_type: Some(send.auth_type.into()),
276            file_length,
277            name: Some(send.name.to_string()),
278            notes: send.notes.map(|notes| notes.to_string()),
279            key: send.key.to_string(),
280            max_access_count: send.max_access_count.map(|count| count as i32),
281            expiration_date: send.expiration_date.map(|date| date.to_rfc3339()),
282            deletion_date: send.deletion_date.to_rfc3339(),
283            file: send.file.map(|file| Box::new(file.into())),
284            text: send.text.map(|text| Box::new(text.into())),
285            password: send.password,
286            emails: send.emails,
287            disabled: send.disabled,
288            hide_email: Some(send.hide_email),
289            id: send
290                .id
291                .expect("SendWithIdRequestModel conversion requires send id")
292                .into(),
293        }
294    }
295}
296
297#[allow(missing_docs)]
298#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
299#[serde(rename_all = "camelCase", deny_unknown_fields)]
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
302pub struct SendView {
303    pub id: Option<SendId>,
304    pub access_id: Option<String>,
305
306    pub name: String,
307    pub notes: Option<String>,
308    /// Base64 encoded key
309    pub key: Option<String>,
310    /// Replace or add a password to an existing send. The SDK will always return None when
311    /// decrypting a [Send]
312    /// TODO: We should revisit this, one variant is to have `[Create, Update]SendView` DTOs.
313    pub new_password: Option<String>,
314    /// Denote if an existing send has a password. The SDK will ignore this value when creating or
315    /// updating sends.
316    pub has_password: bool,
317
318    pub r#type: SendType,
319    pub file: Option<SendFileView>,
320    pub text: Option<SendTextView>,
321
322    pub max_access_count: Option<u32>,
323    pub access_count: u32,
324    pub disabled: bool,
325    pub hide_email: bool,
326
327    pub revision_date: DateTime<Utc>,
328    pub deletion_date: DateTime<Utc>,
329    pub expiration_date: Option<DateTime<Utc>>,
330
331    /// Email addresses for OTP authentication.
332    /// **Note**: Mutually exclusive with `new_password`. If both are set, only password
333    /// authentication will be used. When creating or editing sends, use [crate::SendAuthType]
334    /// to ensure mutual exclusivity at the type level.
335    pub emails: Vec<String>,
336    pub auth_type: AuthType,
337}
338
339#[allow(missing_docs)]
340#[derive(Serialize, Deserialize, Debug)]
341#[serde(rename_all = "camelCase", deny_unknown_fields)]
342#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
343#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
344pub struct SendListView {
345    pub id: Option<SendId>,
346    pub access_id: Option<String>,
347
348    pub name: String,
349
350    pub r#type: SendType,
351    pub disabled: bool,
352
353    pub revision_date: DateTime<Utc>,
354    pub deletion_date: DateTime<Utc>,
355    pub expiration_date: Option<DateTime<Utc>>,
356
357    pub auth_type: AuthType,
358}
359
360impl Send {
361    #[allow(missing_docs)]
362    pub fn get_key(
363        ctx: &mut KeyStoreContext<KeySlotIds>,
364        send_key: &EncString,
365        enc_key: SymmetricKeySlotId,
366    ) -> Result<SymmetricKeySlotId, CryptoError> {
367        let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
368        Self::derive_shareable_key(ctx, &key)
369    }
370
371    pub(crate) fn derive_shareable_key(
372        ctx: &mut KeyStoreContext<KeySlotIds>,
373        key: &[u8],
374    ) -> Result<SymmetricKeySlotId, CryptoError> {
375        let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
376        ctx.derive_shareable_key(key, "send", Some("send"))
377    }
378}
379
380impl IdentifyKey<SymmetricKeySlotId> for Send {
381    fn key_identifier(&self) -> SymmetricKeySlotId {
382        SymmetricKeySlotId::User
383    }
384}
385
386impl IdentifyKey<SymmetricKeySlotId> for SendView {
387    fn key_identifier(&self) -> SymmetricKeySlotId {
388        SymmetricKeySlotId::User
389    }
390}
391
392impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendTextView> for SendText {
393    fn decrypt(
394        &self,
395        ctx: &mut KeyStoreContext<KeySlotIds>,
396        key: SymmetricKeySlotId,
397    ) -> Result<SendTextView, CryptoError> {
398        Ok(SendTextView {
399            text: self.text.decrypt(ctx, key)?,
400            hidden: self.hidden,
401        })
402    }
403}
404
405impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendText> for SendTextView {
406    fn encrypt_composite(
407        &self,
408        ctx: &mut KeyStoreContext<KeySlotIds>,
409        key: SymmetricKeySlotId,
410    ) -> Result<SendText, CryptoError> {
411        Ok(SendText {
412            text: self.text.encrypt(ctx, key)?,
413            hidden: self.hidden,
414        })
415    }
416}
417
418impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendFileView> for SendFile {
419    fn decrypt(
420        &self,
421        ctx: &mut KeyStoreContext<KeySlotIds>,
422        key: SymmetricKeySlotId,
423    ) -> Result<SendFileView, CryptoError> {
424        Ok(SendFileView {
425            id: self.id.clone(),
426            file_name: self.file_name.decrypt(ctx, key)?,
427            size: self.size.clone(),
428            size_name: self.size_name.clone(),
429        })
430    }
431}
432
433impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendFile> for SendFileView {
434    fn encrypt_composite(
435        &self,
436        ctx: &mut KeyStoreContext<KeySlotIds>,
437        key: SymmetricKeySlotId,
438    ) -> Result<SendFile, CryptoError> {
439        Ok(SendFile {
440            id: self.id.clone(),
441            file_name: self.file_name.encrypt(ctx, key)?,
442            size: self.size.clone(),
443            size_name: self.size_name.clone(),
444        })
445    }
446}
447
448impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendView> for Send {
449    fn decrypt(
450        &self,
451        ctx: &mut KeyStoreContext<KeySlotIds>,
452        key: SymmetricKeySlotId,
453    ) -> Result<SendView, CryptoError> {
454        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
455        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
456        // the stretched key
457        let k: Vec<u8> = self.key.decrypt(ctx, key)?;
458        let key = Send::derive_shareable_key(ctx, &k)?;
459
460        Ok(SendView {
461            id: self.id,
462            access_id: self.access_id.clone(),
463
464            name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
465            notes: self.notes.decrypt(ctx, key).ok().flatten(),
466            key: Some(B64Url::from(k).to_string()),
467            new_password: None,
468            has_password: self.password.is_some(),
469
470            r#type: self.r#type,
471            file: self.file.decrypt(ctx, key).ok().flatten(),
472            text: self.text.decrypt(ctx, key).ok().flatten(),
473
474            max_access_count: self.max_access_count,
475            access_count: self.access_count,
476            disabled: self.disabled,
477            hide_email: self.hide_email,
478
479            revision_date: self.revision_date,
480            deletion_date: self.deletion_date,
481            expiration_date: self.expiration_date,
482
483            emails: self
484                .emails
485                .as_deref()
486                .unwrap_or_default()
487                .split(',')
488                .map(|e| e.trim())
489                .filter(|e| !e.is_empty())
490                .map(String::from)
491                .collect(),
492            auth_type: self.auth_type,
493        })
494    }
495}
496
497impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendListView> for Send {
498    fn decrypt(
499        &self,
500        ctx: &mut KeyStoreContext<KeySlotIds>,
501        key: SymmetricKeySlotId,
502    ) -> Result<SendListView, CryptoError> {
503        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
504        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
505        // the stretched key
506        let key = Send::get_key(ctx, &self.key, key)?;
507
508        Ok(SendListView {
509            id: self.id,
510            access_id: self.access_id.clone(),
511
512            name: self.name.decrypt(ctx, key)?,
513            r#type: self.r#type,
514
515            disabled: self.disabled,
516
517            revision_date: self.revision_date,
518            deletion_date: self.deletion_date,
519            expiration_date: self.expiration_date,
520
521            auth_type: self.auth_type,
522        })
523    }
524}
525
526impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Send> for SendView {
527    fn encrypt_composite(
528        &self,
529        ctx: &mut KeyStoreContext<KeySlotIds>,
530        key: SymmetricKeySlotId,
531    ) -> Result<Send, CryptoError> {
532        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
533        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
534        // the stretched key
535        let k = match (&self.key, &self.id) {
536            // Existing send, decrypt key
537            (Some(k), _) => B64Url::try_from(k.as_str())
538                .map_err(|_| CryptoError::InvalidKey)?
539                .as_bytes()
540                .to_vec(),
541            // New send, generate random key
542            (None, None) => {
543                let key = generate_random_bytes::<[u8; 16]>();
544                key.to_vec()
545            }
546            // Existing send without key
547            _ => return Err(CryptoError::InvalidKey),
548        };
549        let send_key = Send::derive_shareable_key(ctx, &k)?;
550
551        Ok(Send {
552            id: self.id,
553            access_id: self.access_id.clone(),
554
555            name: self.name.encrypt(ctx, send_key)?,
556            notes: self.notes.encrypt(ctx, send_key)?,
557            key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
558            password: self.new_password.as_ref().map(|password| {
559                let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
560                B64::from(password.as_slice()).to_string()
561            }),
562
563            r#type: self.r#type,
564            file: self.file.encrypt_composite(ctx, send_key)?,
565            text: self.text.encrypt_composite(ctx, send_key)?,
566
567            max_access_count: self.max_access_count,
568            access_count: self.access_count,
569            disabled: self.disabled,
570            hide_email: self.hide_email,
571
572            revision_date: self.revision_date,
573            deletion_date: self.deletion_date,
574            expiration_date: self.expiration_date,
575
576            emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
577            auth_type: self.auth_type,
578        })
579    }
580}
581
582impl TryFrom<SendResponseModel> for Send {
583    type Error = SendParseError;
584
585    fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
586        let auth_type = match send.auth_type {
587            Some(t) => t.try_into()?,
588            None => {
589                if send.password.is_some() {
590                    AuthType::Password
591                } else if send.emails.is_some() {
592                    AuthType::Email
593                } else {
594                    AuthType::None
595                }
596            }
597        };
598        Ok(Send {
599            id: send.id.map(SendId::new),
600            access_id: send.access_id,
601            name: require!(send.name).parse()?,
602            notes: EncString::try_from_optional(send.notes)?,
603            key: require!(send.key).parse()?,
604            password: send.password,
605            r#type: require!(send.r#type).try_into()?,
606            file: send.file.map(|f| (*f).try_into()).transpose()?,
607            text: send.text.map(|t| (*t).try_into()).transpose()?,
608            max_access_count: send.max_access_count.map(|s| s as u32),
609            access_count: require!(send.access_count) as u32,
610            disabled: send.disabled.unwrap_or(false),
611            hide_email: send.hide_email.unwrap_or(false),
612            revision_date: require!(send.revision_date).parse()?,
613            deletion_date: require!(send.deletion_date).parse()?,
614            expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
615            emails: send.emails,
616            auth_type,
617        })
618    }
619}
620
621impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
622    type Error = bitwarden_core::MissingFieldError;
623
624    fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
625        Ok(match t {
626            bitwarden_api_api::models::SendType::Text => SendType::Text,
627            bitwarden_api_api::models::SendType::File => SendType::File,
628            bitwarden_api_api::models::SendType::__Unknown(_) => {
629                return Err(bitwarden_core::MissingFieldError("type"));
630            }
631        })
632    }
633}
634
635impl From<SendType> for bitwarden_api_api::models::SendType {
636    fn from(t: SendType) -> Self {
637        match t {
638            SendType::Text => bitwarden_api_api::models::SendType::Text,
639            SendType::File => bitwarden_api_api::models::SendType::File,
640        }
641    }
642}
643
644impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
645    type Error = bitwarden_core::MissingFieldError;
646
647    fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
648        Ok(match value {
649            bitwarden_api_api::models::AuthType::Email => AuthType::Email,
650            bitwarden_api_api::models::AuthType::Password => AuthType::Password,
651            bitwarden_api_api::models::AuthType::None => AuthType::None,
652            bitwarden_api_api::models::AuthType::__Unknown(_) => {
653                return Err(bitwarden_core::MissingFieldError("auth_type"));
654            }
655        })
656    }
657}
658
659impl From<AuthType> for bitwarden_api_api::models::AuthType {
660    fn from(value: AuthType) -> Self {
661        match value {
662            AuthType::Email => bitwarden_api_api::models::AuthType::Email,
663            AuthType::Password => bitwarden_api_api::models::AuthType::Password,
664            AuthType::None => bitwarden_api_api::models::AuthType::None,
665        }
666    }
667}
668
669impl From<SendFile> for SendFileModel {
670    fn from(file: SendFile) -> Self {
671        SendFileModel {
672            id: file.id,
673            file_name: Some(file.file_name.to_string()),
674            size: file.size,
675            size_name: file.size_name,
676        }
677    }
678}
679
680impl From<SendText> for SendTextModel {
681    fn from(text: SendText) -> Self {
682        SendTextModel {
683            text: text.text.map(|text| text.to_string()),
684            hidden: Some(text.hidden),
685        }
686    }
687}
688
689impl TryFrom<SendFileModel> for SendFile {
690    type Error = SendParseError;
691
692    fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
693        Ok(SendFile {
694            id: file.id,
695            file_name: require!(file.file_name).parse()?,
696            size: file.size.map(|v| v.to_string()),
697            size_name: file.size_name,
698        })
699    }
700}
701
702impl TryFrom<SendTextModel> for SendText {
703    type Error = SendParseError;
704
705    fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
706        Ok(SendText {
707            text: EncString::try_from_optional(text.text)?,
708            hidden: text.hidden.unwrap_or(false),
709        })
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use bitwarden_core::key_management::create_test_crypto_with_user_key;
716    use bitwarden_crypto::SymmetricCryptoKey;
717
718    use super::*;
719
720    #[test]
721    fn test_get_send_key() {
722        // Initialize user encryption with some test data
723        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
724        let crypto = create_test_crypto_with_user_key(user_key);
725        let mut ctx = crypto.context();
726
727        let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
728            .parse()
729            .unwrap();
730
731        // Get the send key
732        let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
733        #[allow(deprecated)]
734        let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
735        let send_key_b64 = send_key.to_base64();
736        assert_eq!(
737            send_key_b64.to_string(),
738            "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
739        );
740    }
741
742    #[test]
743    pub fn test_decrypt() {
744        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
745        let crypto = create_test_crypto_with_user_key(user_key);
746
747        let send = Send {
748            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
749            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
750            r#type: SendType::Text,
751            name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
752                .unwrap(),
753            notes: None,
754            file: None,
755            text: Some(SendText {
756                text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
757                hidden: false,
758            }),
759            key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
760            max_access_count: None,
761            access_count: 0,
762            password: None,
763            disabled: false,
764            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
765            expiration_date: None,
766            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
767            hide_email: false,
768            emails: None,
769            auth_type: AuthType::None,
770        };
771
772        let view: SendView = crypto.decrypt(&send).unwrap();
773
774        let expected = SendView {
775            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
776            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
777            name: "Test".to_string(),
778            notes: None,
779            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
780            new_password: None,
781            has_password: false,
782            r#type: SendType::Text,
783            file: None,
784            text: Some(SendTextView {
785                text: Some("This is a test".to_owned()),
786                hidden: false,
787            }),
788            max_access_count: None,
789            access_count: 0,
790            disabled: false,
791            hide_email: false,
792            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
793            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
794            expiration_date: None,
795            emails: Vec::new(),
796            auth_type: AuthType::None,
797        };
798
799        assert_eq!(view, expected);
800    }
801
802    #[test]
803    pub fn test_encrypt() {
804        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
805        let crypto = create_test_crypto_with_user_key(user_key);
806
807        let view = SendView {
808            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
809            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
810            name: "Test".to_string(),
811            notes: None,
812            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
813            new_password: None,
814            has_password: false,
815            r#type: SendType::Text,
816            file: None,
817            text: Some(SendTextView {
818                text: Some("This is a test".to_owned()),
819                hidden: false,
820            }),
821            max_access_count: None,
822            access_count: 0,
823            disabled: false,
824            hide_email: false,
825            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
826            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
827            expiration_date: None,
828            emails: Vec::new(),
829            auth_type: AuthType::None,
830        };
831
832        // Re-encrypt and decrypt again to ensure encrypt works
833        let v: SendView = crypto
834            .decrypt(&crypto.encrypt(view.clone()).unwrap())
835            .unwrap();
836        assert_eq!(v, view);
837    }
838
839    #[test]
840    pub fn test_create() {
841        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
842        let crypto = create_test_crypto_with_user_key(user_key);
843
844        let view = SendView {
845            id: None,
846            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
847            name: "Test".to_string(),
848            notes: None,
849            key: None,
850            new_password: None,
851            has_password: false,
852            r#type: SendType::Text,
853            file: None,
854            text: Some(SendTextView {
855                text: Some("This is a test".to_owned()),
856                hidden: false,
857            }),
858            max_access_count: None,
859            access_count: 0,
860            disabled: false,
861            hide_email: false,
862            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
863            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
864            expiration_date: None,
865            emails: Vec::new(),
866            auth_type: AuthType::None,
867        };
868
869        // Re-encrypt and decrypt again to ensure encrypt works
870        let v: SendView = crypto
871            .decrypt(&crypto.encrypt(view.clone()).unwrap())
872            .unwrap();
873
874        // Ignore key when comparing
875        let t = SendView { key: None, ..v };
876        assert_eq!(t, view);
877    }
878
879    #[test]
880    pub fn test_create_password() {
881        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
882        let crypto = create_test_crypto_with_user_key(user_key);
883
884        let view = SendView {
885            id: None,
886            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
887            name: "Test".to_owned(),
888            notes: None,
889            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
890            new_password: Some("abc123".to_owned()),
891            has_password: false,
892            r#type: SendType::Text,
893            file: None,
894            text: Some(SendTextView {
895                text: Some("This is a test".to_owned()),
896                hidden: false,
897            }),
898            max_access_count: None,
899            access_count: 0,
900            disabled: false,
901            hide_email: false,
902            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
903            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
904            expiration_date: None,
905            emails: Vec::new(),
906            auth_type: AuthType::Password,
907        };
908
909        let send: Send = crypto.encrypt(view).unwrap();
910
911        assert_eq!(
912            send.password,
913            Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
914        );
915        assert_eq!(send.auth_type, AuthType::Password);
916
917        let v: SendView = crypto.decrypt(&send).unwrap();
918        assert_eq!(v.new_password, None);
919        assert!(v.has_password);
920        assert_eq!(v.auth_type, AuthType::Password);
921    }
922
923    #[test]
924    pub fn test_create_email_otp() {
925        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
926        let crypto = create_test_crypto_with_user_key(user_key);
927
928        let view = SendView {
929            id: None,
930            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
931            name: "Test".to_owned(),
932            notes: None,
933            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
934            new_password: None,
935            has_password: false,
936            r#type: SendType::Text,
937            file: None,
938            text: Some(SendTextView {
939                text: Some("This is a test".to_owned()),
940                hidden: false,
941            }),
942            max_access_count: None,
943            access_count: 0,
944            disabled: false,
945            hide_email: false,
946            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
947            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
948            expiration_date: None,
949            emails: vec![
950                String::from("[email protected]"),
951                String::from("[email protected]"),
952            ],
953            auth_type: AuthType::Email,
954        };
955
956        let send: Send = crypto.encrypt(view.clone()).unwrap();
957
958        // Verify decrypted view matches original prior to encrypting
959        let v: SendView = crypto.decrypt(&send).unwrap();
960
961        assert_eq!(v, view);
962    }
963
964    #[test]
965    fn test_send_into_send_with_id_request_model() {
966        let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
967        let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
968            .unwrap()
969            .with_timezone(&Utc);
970        let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
971            .unwrap()
972            .with_timezone(&Utc);
973        let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
974            .unwrap()
975            .with_timezone(&Utc);
976
977        let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
978        let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
979        let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
980        let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
981        let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
982
983        let send = Send {
984            id: Some(SendId::new(send_id)),
985            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
986            name: name.parse().unwrap(),
987            notes: Some(notes.parse().unwrap()),
988            key: key.parse().unwrap(),
989            password: Some("hash".to_string()),
990            r#type: SendType::File,
991            file: Some(SendFile {
992                id: Some("file-id".to_string()),
993                file_name: file_name.parse().unwrap(),
994                size: Some("1234".to_string()),
995                size_name: Some("1.2 KB".to_string()),
996            }),
997            text: Some(SendText {
998                text: Some(text_value.parse().unwrap()),
999                hidden: true,
1000            }),
1001            max_access_count: Some(42),
1002            access_count: 0,
1003            disabled: true,
1004            hide_email: true,
1005            revision_date,
1006            deletion_date,
1007            expiration_date: Some(expiration_date),
1008            emails: Some("[email protected],[email protected]".to_string()),
1009            auth_type: AuthType::Email,
1010        };
1011
1012        let model: SendWithIdRequestModel = send.into();
1013
1014        assert_eq!(model.id, send_id);
1015        assert_eq!(
1016            model.r#type,
1017            Some(bitwarden_api_api::models::SendType::File)
1018        );
1019        assert_eq!(
1020            model.auth_type,
1021            Some(bitwarden_api_api::models::AuthType::Email)
1022        );
1023        assert_eq!(model.file_length, Some(1234));
1024        assert_eq!(model.name.as_deref(), Some(name));
1025        assert_eq!(model.notes.as_deref(), Some(notes));
1026        assert_eq!(model.key, key);
1027        assert_eq!(model.max_access_count, Some(42));
1028        assert_eq!(
1029            model
1030                .expiration_date
1031                .unwrap()
1032                .parse::<DateTime<Utc>>()
1033                .unwrap(),
1034            expiration_date
1035        );
1036        assert_eq!(
1037            model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1038            deletion_date
1039        );
1040        assert_eq!(model.password.as_deref(), Some("hash"));
1041        assert_eq!(
1042            model.emails.as_deref(),
1043            Some("[email protected],[email protected]")
1044        );
1045        assert!(model.disabled);
1046        assert_eq!(model.hide_email, Some(true));
1047
1048        let file = model.file.unwrap();
1049        assert_eq!(file.id.as_deref(), Some("file-id"));
1050        assert_eq!(file.file_name.as_deref(), Some(file_name));
1051        assert_eq!(file.size.as_deref(), Some("1234"));
1052        assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1053
1054        let text = model.text.unwrap();
1055        assert_eq!(text.text.as_deref(), Some(text_value));
1056        assert_eq!(text.hidden, Some(true));
1057    }
1058}