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