bitwarden_send/
send.rs

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