1use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel};
2use bitwarden_core::{
3 key_management::{KeyIds, SymmetricKeyId},
4 require,
5};
6use bitwarden_crypto::{
7 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
8 OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes,
9};
10use bitwarden_encoding::{B64, B64Url};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14use uuid::Uuid;
15use zeroize::Zeroizing;
16
17use crate::SendParseError;
18
19const SEND_ITERATIONS: u32 = 100_000;
20
21#[derive(Serialize, Deserialize, Debug)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24pub struct SendFile {
25 pub id: Option<String>,
26 pub file_name: EncString,
27 pub size: Option<String>,
28 pub size_name: Option<String>,
30}
31
32#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
35pub struct SendFileView {
36 pub id: Option<String>,
37 pub file_name: String,
38 pub size: Option<String>,
39 pub size_name: Option<String>,
41}
42
43#[derive(Serialize, Deserialize, Debug)]
44#[serde(rename_all = "camelCase", deny_unknown_fields)]
45#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
46pub struct SendText {
47 pub text: Option<EncString>,
48 pub hidden: bool,
49}
50
51#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54pub struct SendTextView {
55 pub text: Option<String>,
56 pub hidden: bool,
57}
58
59#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
60#[repr(u8)]
61#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
62pub enum SendType {
63 Text = 0,
64 File = 1,
65}
66
67#[allow(missing_docs)]
68#[derive(Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
71pub struct Send {
72 pub id: Option<Uuid>,
73 pub access_id: Option<String>,
74
75 pub name: EncString,
76 pub notes: Option<EncString>,
77 pub key: EncString,
78 pub password: Option<String>,
79
80 pub r#type: SendType,
81 pub file: Option<SendFile>,
82 pub text: Option<SendText>,
83
84 pub max_access_count: Option<u32>,
85 pub access_count: u32,
86 pub disabled: bool,
87 pub hide_email: bool,
88
89 pub revision_date: DateTime<Utc>,
90 pub deletion_date: DateTime<Utc>,
91 pub expiration_date: Option<DateTime<Utc>>,
92}
93
94#[allow(missing_docs)]
95#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
96#[serde(rename_all = "camelCase", deny_unknown_fields)]
97#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
98pub struct SendView {
99 pub id: Option<Uuid>,
100 pub access_id: Option<String>,
101
102 pub name: String,
103 pub notes: Option<String>,
104 pub key: Option<String>,
106 pub new_password: Option<String>,
110 pub has_password: bool,
113
114 pub r#type: SendType,
115 pub file: Option<SendFileView>,
116 pub text: Option<SendTextView>,
117
118 pub max_access_count: Option<u32>,
119 pub access_count: u32,
120 pub disabled: bool,
121 pub hide_email: bool,
122
123 pub revision_date: DateTime<Utc>,
124 pub deletion_date: DateTime<Utc>,
125 pub expiration_date: Option<DateTime<Utc>>,
126}
127
128#[allow(missing_docs)]
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
146impl Send {
147 #[allow(missing_docs)]
148 pub fn get_key(
149 ctx: &mut KeyStoreContext<KeyIds>,
150 send_key: &EncString,
151 enc_key: SymmetricKeyId,
152 ) -> Result<SymmetricKeyId, CryptoError> {
153 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
154 Self::derive_shareable_key(ctx, &key)
155 }
156
157 fn derive_shareable_key(
158 ctx: &mut KeyStoreContext<KeyIds>,
159 key: &[u8],
160 ) -> Result<SymmetricKeyId, CryptoError> {
161 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
162 ctx.derive_shareable_key(key, "send", Some("send"))
163 }
164}
165
166impl IdentifyKey<SymmetricKeyId> for Send {
167 fn key_identifier(&self) -> SymmetricKeyId {
168 SymmetricKeyId::User
169 }
170}
171
172impl IdentifyKey<SymmetricKeyId> for SendView {
173 fn key_identifier(&self) -> SymmetricKeyId {
174 SymmetricKeyId::User
175 }
176}
177
178impl Decryptable<KeyIds, SymmetricKeyId, SendTextView> for SendText {
179 fn decrypt(
180 &self,
181 ctx: &mut KeyStoreContext<KeyIds>,
182 key: SymmetricKeyId,
183 ) -> Result<SendTextView, CryptoError> {
184 Ok(SendTextView {
185 text: self.text.decrypt(ctx, key)?,
186 hidden: self.hidden,
187 })
188 }
189}
190
191impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendText> for SendTextView {
192 fn encrypt_composite(
193 &self,
194 ctx: &mut KeyStoreContext<KeyIds>,
195 key: SymmetricKeyId,
196 ) -> Result<SendText, CryptoError> {
197 Ok(SendText {
198 text: self.text.encrypt(ctx, key)?,
199 hidden: self.hidden,
200 })
201 }
202}
203
204impl Decryptable<KeyIds, SymmetricKeyId, SendFileView> for SendFile {
205 fn decrypt(
206 &self,
207 ctx: &mut KeyStoreContext<KeyIds>,
208 key: SymmetricKeyId,
209 ) -> Result<SendFileView, CryptoError> {
210 Ok(SendFileView {
211 id: self.id.clone(),
212 file_name: self.file_name.decrypt(ctx, key)?,
213 size: self.size.clone(),
214 size_name: self.size_name.clone(),
215 })
216 }
217}
218
219impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendFile> for SendFileView {
220 fn encrypt_composite(
221 &self,
222 ctx: &mut KeyStoreContext<KeyIds>,
223 key: SymmetricKeyId,
224 ) -> Result<SendFile, CryptoError> {
225 Ok(SendFile {
226 id: self.id.clone(),
227 file_name: self.file_name.encrypt(ctx, key)?,
228 size: self.size.clone(),
229 size_name: self.size_name.clone(),
230 })
231 }
232}
233
234impl Decryptable<KeyIds, SymmetricKeyId, SendView> for Send {
235 fn decrypt(
236 &self,
237 ctx: &mut KeyStoreContext<KeyIds>,
238 key: SymmetricKeyId,
239 ) -> Result<SendView, CryptoError> {
240 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
244 let key = Send::derive_shareable_key(ctx, &k)?;
245
246 Ok(SendView {
247 id: self.id,
248 access_id: self.access_id.clone(),
249
250 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
251 notes: self.notes.decrypt(ctx, key).ok().flatten(),
252 key: Some(B64Url::from(k).to_string()),
253 new_password: None,
254 has_password: self.password.is_some(),
255
256 r#type: self.r#type,
257 file: self.file.decrypt(ctx, key).ok().flatten(),
258 text: self.text.decrypt(ctx, key).ok().flatten(),
259
260 max_access_count: self.max_access_count,
261 access_count: self.access_count,
262 disabled: self.disabled,
263 hide_email: self.hide_email,
264
265 revision_date: self.revision_date,
266 deletion_date: self.deletion_date,
267 expiration_date: self.expiration_date,
268 })
269 }
270}
271
272impl Decryptable<KeyIds, SymmetricKeyId, SendListView> for Send {
273 fn decrypt(
274 &self,
275 ctx: &mut KeyStoreContext<KeyIds>,
276 key: SymmetricKeyId,
277 ) -> Result<SendListView, CryptoError> {
278 let key = Send::get_key(ctx, &self.key, key)?;
282
283 Ok(SendListView {
284 id: self.id,
285 access_id: self.access_id.clone(),
286
287 name: self.name.decrypt(ctx, key)?,
288 r#type: self.r#type,
289
290 disabled: self.disabled,
291
292 revision_date: self.revision_date,
293 deletion_date: self.deletion_date,
294 expiration_date: self.expiration_date,
295 })
296 }
297}
298
299impl CompositeEncryptable<KeyIds, SymmetricKeyId, Send> for SendView {
300 fn encrypt_composite(
301 &self,
302 ctx: &mut KeyStoreContext<KeyIds>,
303 key: SymmetricKeyId,
304 ) -> Result<Send, CryptoError> {
305 let k = match (&self.key, &self.id) {
309 (Some(k), _) => B64Url::try_from(k.as_str())
311 .map_err(|_| CryptoError::InvalidKey)?
312 .as_bytes()
313 .to_vec(),
314 (None, None) => {
316 let key = generate_random_bytes::<[u8; 16]>();
317 key.to_vec()
318 }
319 _ => 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: OctetStreamBytes::from(k.clone()).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 B64::from(password.as_slice()).to_string()
334 }),
335
336 r#type: self.r#type,
337 file: self.file.encrypt_composite(ctx, send_key)?,
338 text: self.text.encrypt_composite(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 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 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!(
434 send_key_b64.to_string(),
435 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
436 );
437 }
438
439 #[test]
440 pub fn test_decrypt() {
441 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
442 let crypto = create_test_crypto_with_user_key(user_key);
443
444 let send = Send {
445 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
446 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
447 r#type: SendType::Text,
448 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
449 .unwrap(),
450 notes: None,
451 file: None,
452 text: Some(SendText {
453 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
454 hidden: false,
455 }),
456 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
457 max_access_count: None,
458 access_count: 0,
459 password: None,
460 disabled: false,
461 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
462 expiration_date: None,
463 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
464 hide_email: false,
465 };
466
467 let view: SendView = crypto.decrypt(&send).unwrap();
468
469 let expected = SendView {
470 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
471 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
472 name: "Test".to_string(),
473 notes: None,
474 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
475 new_password: None,
476 has_password: false,
477 r#type: SendType::Text,
478 file: None,
479 text: Some(SendTextView {
480 text: Some("This is a test".to_owned()),
481 hidden: false,
482 }),
483 max_access_count: None,
484 access_count: 0,
485 disabled: false,
486 hide_email: false,
487 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
488 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
489 expiration_date: None,
490 };
491
492 assert_eq!(view, expected);
493 }
494
495 #[test]
496 pub fn test_encrypt() {
497 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
498 let crypto = create_test_crypto_with_user_key(user_key);
499
500 let view = SendView {
501 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
502 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
503 name: "Test".to_string(),
504 notes: None,
505 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
506 new_password: None,
507 has_password: false,
508 r#type: SendType::Text,
509 file: None,
510 text: Some(SendTextView {
511 text: Some("This is a test".to_owned()),
512 hidden: false,
513 }),
514 max_access_count: None,
515 access_count: 0,
516 disabled: false,
517 hide_email: false,
518 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
519 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
520 expiration_date: None,
521 };
522
523 let v: SendView = crypto
525 .decrypt(&crypto.encrypt(view.clone()).unwrap())
526 .unwrap();
527 assert_eq!(v, view);
528 }
529
530 #[test]
531 pub fn test_create() {
532 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
533 let crypto = create_test_crypto_with_user_key(user_key);
534
535 let view = SendView {
536 id: None,
537 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
538 name: "Test".to_string(),
539 notes: None,
540 key: None,
541 new_password: None,
542 has_password: false,
543 r#type: SendType::Text,
544 file: None,
545 text: Some(SendTextView {
546 text: Some("This is a test".to_owned()),
547 hidden: false,
548 }),
549 max_access_count: None,
550 access_count: 0,
551 disabled: false,
552 hide_email: false,
553 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
554 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
555 expiration_date: None,
556 };
557
558 let v: SendView = crypto
560 .decrypt(&crypto.encrypt(view.clone()).unwrap())
561 .unwrap();
562
563 let t = SendView { key: None, ..v };
565 assert_eq!(t, view);
566 }
567
568 #[test]
569 pub fn test_create_password() {
570 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
571 let crypto = create_test_crypto_with_user_key(user_key);
572
573 let view = SendView {
574 id: None,
575 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
576 name: "Test".to_owned(),
577 notes: None,
578 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
579 new_password: Some("abc123".to_owned()),
580 has_password: false,
581 r#type: SendType::Text,
582 file: None,
583 text: Some(SendTextView {
584 text: Some("This is a test".to_owned()),
585 hidden: false,
586 }),
587 max_access_count: None,
588 access_count: 0,
589 disabled: false,
590 hide_email: false,
591 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
592 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
593 expiration_date: None,
594 };
595
596 let send: Send = crypto.encrypt(view).unwrap();
597
598 assert_eq!(
599 send.password,
600 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
601 );
602
603 let v: SendView = crypto.decrypt(&send).unwrap();
604 assert_eq!(v.new_password, None);
605 assert!(v.has_password);
606 }
607}