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 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 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 pub key: Option<String>,
109 pub new_password: Option<String>,
113 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 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 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 let k = match (&self.key, &self.id) {
314 (Some(k), _) => URL_SAFE_NO_PAD
316 .decode(k)
317 .map_err(|_| CryptoError::InvalidKey)?,
318 (None, None) => {
320 let key = generate_random_bytes::<[u8; 16]>();
321 key.to_vec()
322 }
323 _ => 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 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 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 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 let v: SendView = crypto
561 .decrypt(&crypto.encrypt(view.clone()).unwrap())
562 .unwrap();
563
564 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}