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