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