bitwarden_send/
send.rs

1use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel};
2use bitwarden_core::{
3    key_management::{KeyIds, SymmetricKeyId},
4    require,
5};
6use bitwarden_crypto::{
7    CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
8    OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes,
9};
10use bitwarden_encoding::{B64, B64Url};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14use uuid::Uuid;
15use zeroize::Zeroizing;
16
17use crate::SendParseError;
18
19const SEND_ITERATIONS: u32 = 100_000;
20
21#[derive(Serialize, Deserialize, Debug)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24pub struct SendFile {
25    pub id: Option<String>,
26    pub file_name: EncString,
27    pub size: Option<String>,
28    /// Readable size, ex: "4.2 KB" or "1.43 GB"
29    pub size_name: Option<String>,
30}
31
32#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
35pub struct SendFileView {
36    pub id: Option<String>,
37    pub file_name: String,
38    pub size: Option<String>,
39    /// Readable size, ex: "4.2 KB" or "1.43 GB"
40    pub size_name: Option<String>,
41}
42
43#[derive(Serialize, Deserialize, Debug)]
44#[serde(rename_all = "camelCase", deny_unknown_fields)]
45#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
46pub struct SendText {
47    pub text: Option<EncString>,
48    pub hidden: bool,
49}
50
51#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54pub struct SendTextView {
55    pub text: Option<String>,
56    pub hidden: bool,
57}
58
59#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
60#[repr(u8)]
61#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
62pub enum SendType {
63    Text = 0,
64    File = 1,
65}
66
67/// Indicates the authentication strategy to use when accessing a Send
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
69#[repr(u8)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
71pub enum AuthType {
72    /// Email-based OTP authentication
73    Email = 0,
74
75    /// Password-based authentication
76    Password = 1,
77
78    /// No authentication required
79    None = 2,
80}
81
82#[allow(missing_docs)]
83#[derive(Serialize, Deserialize, Debug)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
86pub struct Send {
87    pub id: Option<Uuid>,
88    pub access_id: Option<String>,
89
90    pub name: EncString,
91    pub notes: Option<EncString>,
92    pub key: EncString,
93    pub password: Option<String>,
94
95    pub r#type: SendType,
96    pub file: Option<SendFile>,
97    pub text: Option<SendText>,
98
99    pub max_access_count: Option<u32>,
100    pub access_count: u32,
101    pub disabled: bool,
102    pub hide_email: bool,
103
104    pub revision_date: DateTime<Utc>,
105    pub deletion_date: DateTime<Utc>,
106    pub expiration_date: Option<DateTime<Utc>>,
107
108    /// Email addresses for OTP authentication.
109    /// **Note**: Mutually exclusive with `new_password`. If both are set,
110    /// only password authentication will be used.
111    pub emails: Option<String>,
112    pub auth_type: AuthType,
113}
114
115#[allow(missing_docs)]
116#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
117#[serde(rename_all = "camelCase", deny_unknown_fields)]
118#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
119pub struct SendView {
120    pub id: Option<Uuid>,
121    pub access_id: Option<String>,
122
123    pub name: String,
124    pub notes: Option<String>,
125    /// Base64 encoded key
126    pub key: Option<String>,
127    /// Replace or add a password to an existing send. The SDK will always return None when
128    /// decrypting a [Send]
129    /// TODO: We should revisit this, one variant is to have `[Create, Update]SendView` DTOs.
130    pub new_password: Option<String>,
131    /// Denote if an existing send has a password. The SDK will ignore this value when creating or
132    /// updating sends.
133    pub has_password: bool,
134
135    pub r#type: SendType,
136    pub file: Option<SendFileView>,
137    pub text: Option<SendTextView>,
138
139    pub max_access_count: Option<u32>,
140    pub access_count: u32,
141    pub disabled: bool,
142    pub hide_email: bool,
143
144    pub revision_date: DateTime<Utc>,
145    pub deletion_date: DateTime<Utc>,
146    pub expiration_date: Option<DateTime<Utc>>,
147
148    /// Email addresses for OTP authentication.
149    /// **Note**: Mutually exclusive with `new_password`. If both are set,
150    /// only password authentication will be used.
151    pub emails: Vec<String>,
152    pub auth_type: AuthType,
153}
154
155#[allow(missing_docs)]
156#[derive(Serialize, Deserialize, Debug)]
157#[serde(rename_all = "camelCase", deny_unknown_fields)]
158#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
159pub struct SendListView {
160    pub id: Option<Uuid>,
161    pub access_id: Option<String>,
162
163    pub name: String,
164
165    pub r#type: SendType,
166    pub disabled: bool,
167
168    pub revision_date: DateTime<Utc>,
169    pub deletion_date: DateTime<Utc>,
170    pub expiration_date: Option<DateTime<Utc>>,
171}
172
173impl Send {
174    #[allow(missing_docs)]
175    pub fn get_key(
176        ctx: &mut KeyStoreContext<KeyIds>,
177        send_key: &EncString,
178        enc_key: SymmetricKeyId,
179    ) -> Result<SymmetricKeyId, CryptoError> {
180        let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
181        Self::derive_shareable_key(ctx, &key)
182    }
183
184    fn derive_shareable_key(
185        ctx: &mut KeyStoreContext<KeyIds>,
186        key: &[u8],
187    ) -> Result<SymmetricKeyId, CryptoError> {
188        let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
189        ctx.derive_shareable_key(key, "send", Some("send"))
190    }
191}
192
193impl IdentifyKey<SymmetricKeyId> for Send {
194    fn key_identifier(&self) -> SymmetricKeyId {
195        SymmetricKeyId::User
196    }
197}
198
199impl IdentifyKey<SymmetricKeyId> for SendView {
200    fn key_identifier(&self) -> SymmetricKeyId {
201        SymmetricKeyId::User
202    }
203}
204
205impl Decryptable<KeyIds, SymmetricKeyId, SendTextView> for SendText {
206    fn decrypt(
207        &self,
208        ctx: &mut KeyStoreContext<KeyIds>,
209        key: SymmetricKeyId,
210    ) -> Result<SendTextView, CryptoError> {
211        Ok(SendTextView {
212            text: self.text.decrypt(ctx, key)?,
213            hidden: self.hidden,
214        })
215    }
216}
217
218impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendText> for SendTextView {
219    fn encrypt_composite(
220        &self,
221        ctx: &mut KeyStoreContext<KeyIds>,
222        key: SymmetricKeyId,
223    ) -> Result<SendText, CryptoError> {
224        Ok(SendText {
225            text: self.text.encrypt(ctx, key)?,
226            hidden: self.hidden,
227        })
228    }
229}
230
231impl Decryptable<KeyIds, SymmetricKeyId, SendFileView> for SendFile {
232    fn decrypt(
233        &self,
234        ctx: &mut KeyStoreContext<KeyIds>,
235        key: SymmetricKeyId,
236    ) -> Result<SendFileView, CryptoError> {
237        Ok(SendFileView {
238            id: self.id.clone(),
239            file_name: self.file_name.decrypt(ctx, key)?,
240            size: self.size.clone(),
241            size_name: self.size_name.clone(),
242        })
243    }
244}
245
246impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendFile> for SendFileView {
247    fn encrypt_composite(
248        &self,
249        ctx: &mut KeyStoreContext<KeyIds>,
250        key: SymmetricKeyId,
251    ) -> Result<SendFile, CryptoError> {
252        Ok(SendFile {
253            id: self.id.clone(),
254            file_name: self.file_name.encrypt(ctx, key)?,
255            size: self.size.clone(),
256            size_name: self.size_name.clone(),
257        })
258    }
259}
260
261impl Decryptable<KeyIds, SymmetricKeyId, SendView> for Send {
262    fn decrypt(
263        &self,
264        ctx: &mut KeyStoreContext<KeyIds>,
265        key: SymmetricKeyId,
266    ) -> Result<SendView, CryptoError> {
267        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
268        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
269        // the stretched key
270        let k: Vec<u8> = self.key.decrypt(ctx, key)?;
271        let key = Send::derive_shareable_key(ctx, &k)?;
272
273        Ok(SendView {
274            id: self.id,
275            access_id: self.access_id.clone(),
276
277            name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
278            notes: self.notes.decrypt(ctx, key).ok().flatten(),
279            key: Some(B64Url::from(k).to_string()),
280            new_password: None,
281            has_password: self.password.is_some(),
282
283            r#type: self.r#type,
284            file: self.file.decrypt(ctx, key).ok().flatten(),
285            text: self.text.decrypt(ctx, key).ok().flatten(),
286
287            max_access_count: self.max_access_count,
288            access_count: self.access_count,
289            disabled: self.disabled,
290            hide_email: self.hide_email,
291
292            revision_date: self.revision_date,
293            deletion_date: self.deletion_date,
294            expiration_date: self.expiration_date,
295
296            emails: self
297                .emails
298                .as_deref()
299                .unwrap_or_default()
300                .split(',')
301                .map(|e| e.trim())
302                .filter(|e| !e.is_empty())
303                .map(String::from)
304                .collect(),
305            auth_type: self.auth_type,
306        })
307    }
308}
309
310impl Decryptable<KeyIds, SymmetricKeyId, SendListView> for Send {
311    fn decrypt(
312        &self,
313        ctx: &mut KeyStoreContext<KeyIds>,
314        key: SymmetricKeyId,
315    ) -> Result<SendListView, CryptoError> {
316        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
317        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
318        // the stretched key
319        let key = Send::get_key(ctx, &self.key, key)?;
320
321        Ok(SendListView {
322            id: self.id,
323            access_id: self.access_id.clone(),
324
325            name: self.name.decrypt(ctx, key)?,
326            r#type: self.r#type,
327
328            disabled: self.disabled,
329
330            revision_date: self.revision_date,
331            deletion_date: self.deletion_date,
332            expiration_date: self.expiration_date,
333        })
334    }
335}
336
337impl CompositeEncryptable<KeyIds, SymmetricKeyId, Send> for SendView {
338    fn encrypt_composite(
339        &self,
340        ctx: &mut KeyStoreContext<KeyIds>,
341        key: SymmetricKeyId,
342    ) -> Result<Send, CryptoError> {
343        // For sends, we first decrypt the send key with the user key, and stretch it to it's full
344        // size For the rest of the fields, we ignore the provided SymmetricCryptoKey and
345        // the stretched key
346        let k = match (&self.key, &self.id) {
347            // Existing send, decrypt key
348            (Some(k), _) => B64Url::try_from(k.as_str())
349                .map_err(|_| CryptoError::InvalidKey)?
350                .as_bytes()
351                .to_vec(),
352            // New send, generate random key
353            (None, None) => {
354                let key = generate_random_bytes::<[u8; 16]>();
355                key.to_vec()
356            }
357            // Existing send without key
358            _ => return Err(CryptoError::InvalidKey),
359        };
360        let send_key = Send::derive_shareable_key(ctx, &k)?;
361
362        Ok(Send {
363            id: self.id,
364            access_id: self.access_id.clone(),
365
366            name: self.name.encrypt(ctx, send_key)?,
367            notes: self.notes.encrypt(ctx, send_key)?,
368            key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
369            password: self.new_password.as_ref().map(|password| {
370                let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
371                B64::from(password.as_slice()).to_string()
372            }),
373
374            r#type: self.r#type,
375            file: self.file.encrypt_composite(ctx, send_key)?,
376            text: self.text.encrypt_composite(ctx, send_key)?,
377
378            max_access_count: self.max_access_count,
379            access_count: self.access_count,
380            disabled: self.disabled,
381            hide_email: self.hide_email,
382
383            revision_date: self.revision_date,
384            deletion_date: self.deletion_date,
385            expiration_date: self.expiration_date,
386
387            emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
388            auth_type: self.auth_type,
389        })
390    }
391}
392
393impl TryFrom<SendResponseModel> for Send {
394    type Error = SendParseError;
395
396    fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
397        let auth_type = match send.auth_type {
398            Some(t) => t.into(),
399            None => {
400                if send.password.is_some() {
401                    AuthType::Password
402                } else if send.emails.is_some() {
403                    AuthType::Email
404                } else {
405                    AuthType::None
406                }
407            }
408        };
409        Ok(Send {
410            id: send.id,
411            access_id: send.access_id,
412            name: require!(send.name).parse()?,
413            notes: EncString::try_from_optional(send.notes)?,
414            key: require!(send.key).parse()?,
415            password: send.password,
416            r#type: require!(send.r#type).into(),
417            file: send.file.map(|f| (*f).try_into()).transpose()?,
418            text: send.text.map(|t| (*t).try_into()).transpose()?,
419            max_access_count: send.max_access_count.map(|s| s as u32),
420            access_count: require!(send.access_count) as u32,
421            disabled: send.disabled.unwrap_or(false),
422            hide_email: send.hide_email.unwrap_or(false),
423            revision_date: require!(send.revision_date).parse()?,
424            deletion_date: require!(send.deletion_date).parse()?,
425            expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
426            emails: send.emails,
427            auth_type,
428        })
429    }
430}
431
432impl From<bitwarden_api_api::models::SendType> for SendType {
433    fn from(t: bitwarden_api_api::models::SendType) -> Self {
434        match t {
435            bitwarden_api_api::models::SendType::Text => SendType::Text,
436            bitwarden_api_api::models::SendType::File => SendType::File,
437        }
438    }
439}
440
441impl From<bitwarden_api_api::models::AuthType> for AuthType {
442    fn from(value: bitwarden_api_api::models::AuthType) -> Self {
443        match value {
444            bitwarden_api_api::models::AuthType::Email => AuthType::Email,
445            bitwarden_api_api::models::AuthType::Password => AuthType::Password,
446            bitwarden_api_api::models::AuthType::None => AuthType::None,
447        }
448    }
449}
450
451impl TryFrom<SendFileModel> for SendFile {
452    type Error = SendParseError;
453
454    fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
455        Ok(SendFile {
456            id: file.id,
457            file_name: require!(file.file_name).parse()?,
458            size: file.size.map(|v| v.to_string()),
459            size_name: file.size_name,
460        })
461    }
462}
463
464impl TryFrom<SendTextModel> for SendText {
465    type Error = SendParseError;
466
467    fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
468        Ok(SendText {
469            text: EncString::try_from_optional(text.text)?,
470            hidden: text.hidden.unwrap_or(false),
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use bitwarden_core::key_management::create_test_crypto_with_user_key;
478    use bitwarden_crypto::SymmetricCryptoKey;
479
480    use super::*;
481
482    #[test]
483    fn test_get_send_key() {
484        // Initialize user encryption with some test data
485        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
486        let crypto = create_test_crypto_with_user_key(user_key);
487        let mut ctx = crypto.context();
488
489        let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
490            .parse()
491            .unwrap();
492
493        // Get the send key
494        let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeyId::User).unwrap();
495        #[allow(deprecated)]
496        let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
497        let send_key_b64 = send_key.to_base64();
498        assert_eq!(
499            send_key_b64.to_string(),
500            "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
501        );
502    }
503
504    #[test]
505    pub fn test_decrypt() {
506        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
507        let crypto = create_test_crypto_with_user_key(user_key);
508
509        let send = Send {
510            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
511            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
512            r#type: SendType::Text,
513            name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
514                .unwrap(),
515            notes: None,
516            file: None,
517            text: Some(SendText {
518                text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
519                hidden: false,
520            }),
521            key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
522            max_access_count: None,
523            access_count: 0,
524            password: None,
525            disabled: false,
526            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
527            expiration_date: None,
528            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
529            hide_email: false,
530            emails: None,
531            auth_type: AuthType::None,
532        };
533
534        let view: SendView = crypto.decrypt(&send).unwrap();
535
536        let expected = SendView {
537            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
538            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
539            name: "Test".to_string(),
540            notes: None,
541            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
542            new_password: None,
543            has_password: false,
544            r#type: SendType::Text,
545            file: None,
546            text: Some(SendTextView {
547                text: Some("This is a test".to_owned()),
548                hidden: false,
549            }),
550            max_access_count: None,
551            access_count: 0,
552            disabled: false,
553            hide_email: false,
554            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
555            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
556            expiration_date: None,
557            emails: Vec::new(),
558            auth_type: AuthType::None,
559        };
560
561        assert_eq!(view, expected);
562    }
563
564    #[test]
565    pub fn test_encrypt() {
566        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
567        let crypto = create_test_crypto_with_user_key(user_key);
568
569        let view = SendView {
570            id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
571            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
572            name: "Test".to_string(),
573            notes: None,
574            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
575            new_password: None,
576            has_password: false,
577            r#type: SendType::Text,
578            file: None,
579            text: Some(SendTextView {
580                text: Some("This is a test".to_owned()),
581                hidden: false,
582            }),
583            max_access_count: None,
584            access_count: 0,
585            disabled: false,
586            hide_email: false,
587            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
588            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
589            expiration_date: None,
590            emails: Vec::new(),
591            auth_type: AuthType::None,
592        };
593
594        // Re-encrypt and decrypt again to ensure encrypt works
595        let v: SendView = crypto
596            .decrypt(&crypto.encrypt(view.clone()).unwrap())
597            .unwrap();
598        assert_eq!(v, view);
599    }
600
601    #[test]
602    pub fn test_create() {
603        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
604        let crypto = create_test_crypto_with_user_key(user_key);
605
606        let view = SendView {
607            id: None,
608            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
609            name: "Test".to_string(),
610            notes: None,
611            key: None,
612            new_password: None,
613            has_password: false,
614            r#type: SendType::Text,
615            file: None,
616            text: Some(SendTextView {
617                text: Some("This is a test".to_owned()),
618                hidden: false,
619            }),
620            max_access_count: None,
621            access_count: 0,
622            disabled: false,
623            hide_email: false,
624            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
625            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
626            expiration_date: None,
627            emails: Vec::new(),
628            auth_type: AuthType::None,
629        };
630
631        // Re-encrypt and decrypt again to ensure encrypt works
632        let v: SendView = crypto
633            .decrypt(&crypto.encrypt(view.clone()).unwrap())
634            .unwrap();
635
636        // Ignore key when comparing
637        let t = SendView { key: None, ..v };
638        assert_eq!(t, view);
639    }
640
641    #[test]
642    pub fn test_create_password() {
643        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
644        let crypto = create_test_crypto_with_user_key(user_key);
645
646        let view = SendView {
647            id: None,
648            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
649            name: "Test".to_owned(),
650            notes: None,
651            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
652            new_password: Some("abc123".to_owned()),
653            has_password: false,
654            r#type: SendType::Text,
655            file: None,
656            text: Some(SendTextView {
657                text: Some("This is a test".to_owned()),
658                hidden: false,
659            }),
660            max_access_count: None,
661            access_count: 0,
662            disabled: false,
663            hide_email: false,
664            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
665            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
666            expiration_date: None,
667            emails: Vec::new(),
668            auth_type: AuthType::Password,
669        };
670
671        let send: Send = crypto.encrypt(view).unwrap();
672
673        assert_eq!(
674            send.password,
675            Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
676        );
677        assert_eq!(send.auth_type, AuthType::Password);
678
679        let v: SendView = crypto.decrypt(&send).unwrap();
680        assert_eq!(v.new_password, None);
681        assert!(v.has_password);
682        assert_eq!(v.auth_type, AuthType::Password);
683    }
684
685    #[test]
686    pub fn test_create_email_otp() {
687        let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
688        let crypto = create_test_crypto_with_user_key(user_key);
689
690        let view = SendView {
691            id: None,
692            access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
693            name: "Test".to_owned(),
694            notes: None,
695            key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
696            new_password: None,
697            has_password: false,
698            r#type: SendType::Text,
699            file: None,
700            text: Some(SendTextView {
701                text: Some("This is a test".to_owned()),
702                hidden: false,
703            }),
704            max_access_count: None,
705            access_count: 0,
706            disabled: false,
707            hide_email: false,
708            revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
709            deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
710            expiration_date: None,
711            emails: vec![
712                String::from("[email protected]"),
713                String::from("[email protected]"),
714            ],
715            auth_type: AuthType::Email,
716        };
717
718        let send: Send = crypto.encrypt(view).unwrap();
719
720        assert_eq!(
721            send.emails,
722            Some("[email protected],[email protected]".to_string())
723        );
724        assert_eq!(send.auth_type, AuthType::Email);
725
726        let v: SendView = crypto.decrypt(&send).unwrap();
727        assert_eq!(
728            v.emails,
729            vec!(
730                String::from("[email protected]"),
731                String::from("[email protected]")
732            )
733        );
734        assert_eq!(v.auth_type, AuthType::Email);
735    }
736}