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
146const SEND_KEY: SymmetricKeyId = SymmetricKeyId::Local("send_key");
147
148impl Send {
149 #[allow(missing_docs)]
150 pub fn get_key(
151 ctx: &mut KeyStoreContext<KeyIds>,
152 send_key: &EncString,
153 enc_key: SymmetricKeyId,
154 ) -> Result<SymmetricKeyId, CryptoError> {
155 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
156 Self::derive_shareable_key(ctx, &key)
157 }
158
159 fn derive_shareable_key(
160 ctx: &mut KeyStoreContext<KeyIds>,
161 key: &[u8],
162 ) -> Result<SymmetricKeyId, CryptoError> {
163 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
164 ctx.derive_shareable_key(SEND_KEY, key, "send", Some("send"))
165 }
166}
167
168impl IdentifyKey<SymmetricKeyId> for Send {
169 fn key_identifier(&self) -> SymmetricKeyId {
170 SymmetricKeyId::User
171 }
172}
173
174impl IdentifyKey<SymmetricKeyId> for SendView {
175 fn key_identifier(&self) -> SymmetricKeyId {
176 SymmetricKeyId::User
177 }
178}
179
180impl Decryptable<KeyIds, SymmetricKeyId, SendTextView> for SendText {
181 fn decrypt(
182 &self,
183 ctx: &mut KeyStoreContext<KeyIds>,
184 key: SymmetricKeyId,
185 ) -> Result<SendTextView, CryptoError> {
186 Ok(SendTextView {
187 text: self.text.decrypt(ctx, key)?,
188 hidden: self.hidden,
189 })
190 }
191}
192
193impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendText> for SendTextView {
194 fn encrypt_composite(
195 &self,
196 ctx: &mut KeyStoreContext<KeyIds>,
197 key: SymmetricKeyId,
198 ) -> Result<SendText, CryptoError> {
199 Ok(SendText {
200 text: self.text.encrypt(ctx, key)?,
201 hidden: self.hidden,
202 })
203 }
204}
205
206impl Decryptable<KeyIds, SymmetricKeyId, SendFileView> for SendFile {
207 fn decrypt(
208 &self,
209 ctx: &mut KeyStoreContext<KeyIds>,
210 key: SymmetricKeyId,
211 ) -> Result<SendFileView, CryptoError> {
212 Ok(SendFileView {
213 id: self.id.clone(),
214 file_name: self.file_name.decrypt(ctx, key)?,
215 size: self.size.clone(),
216 size_name: self.size_name.clone(),
217 })
218 }
219}
220
221impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendFile> for SendFileView {
222 fn encrypt_composite(
223 &self,
224 ctx: &mut KeyStoreContext<KeyIds>,
225 key: SymmetricKeyId,
226 ) -> Result<SendFile, CryptoError> {
227 Ok(SendFile {
228 id: self.id.clone(),
229 file_name: self.file_name.encrypt(ctx, key)?,
230 size: self.size.clone(),
231 size_name: self.size_name.clone(),
232 })
233 }
234}
235
236impl Decryptable<KeyIds, SymmetricKeyId, SendView> for Send {
237 fn decrypt(
238 &self,
239 ctx: &mut KeyStoreContext<KeyIds>,
240 key: SymmetricKeyId,
241 ) -> Result<SendView, CryptoError> {
242 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
246 let key = Send::derive_shareable_key(ctx, &k)?;
247
248 Ok(SendView {
249 id: self.id,
250 access_id: self.access_id.clone(),
251
252 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
253 notes: self.notes.decrypt(ctx, key).ok().flatten(),
254 key: Some(B64Url::from(k).to_string()),
255 new_password: None,
256 has_password: self.password.is_some(),
257
258 r#type: self.r#type,
259 file: self.file.decrypt(ctx, key).ok().flatten(),
260 text: self.text.decrypt(ctx, key).ok().flatten(),
261
262 max_access_count: self.max_access_count,
263 access_count: self.access_count,
264 disabled: self.disabled,
265 hide_email: self.hide_email,
266
267 revision_date: self.revision_date,
268 deletion_date: self.deletion_date,
269 expiration_date: self.expiration_date,
270 })
271 }
272}
273
274impl Decryptable<KeyIds, SymmetricKeyId, SendListView> for Send {
275 fn decrypt(
276 &self,
277 ctx: &mut KeyStoreContext<KeyIds>,
278 key: SymmetricKeyId,
279 ) -> Result<SendListView, CryptoError> {
280 let key = Send::get_key(ctx, &self.key, key)?;
284
285 Ok(SendListView {
286 id: self.id,
287 access_id: self.access_id.clone(),
288
289 name: self.name.decrypt(ctx, key)?,
290 r#type: self.r#type,
291
292 disabled: self.disabled,
293
294 revision_date: self.revision_date,
295 deletion_date: self.deletion_date,
296 expiration_date: self.expiration_date,
297 })
298 }
299}
300
301impl CompositeEncryptable<KeyIds, SymmetricKeyId, Send> for SendView {
302 fn encrypt_composite(
303 &self,
304 ctx: &mut KeyStoreContext<KeyIds>,
305 key: SymmetricKeyId,
306 ) -> Result<Send, CryptoError> {
307 let k = match (&self.key, &self.id) {
311 (Some(k), _) => B64Url::try_from(k.as_str())
313 .map_err(|_| CryptoError::InvalidKey)?
314 .as_bytes()
315 .to_vec(),
316 (None, None) => {
318 let key = generate_random_bytes::<[u8; 16]>();
319 key.to_vec()
320 }
321 _ => return Err(CryptoError::InvalidKey),
323 };
324 let send_key = Send::derive_shareable_key(ctx, &k)?;
325
326 Ok(Send {
327 id: self.id,
328 access_id: self.access_id.clone(),
329
330 name: self.name.encrypt(ctx, send_key)?,
331 notes: self.notes.encrypt(ctx, send_key)?,
332 key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
333 password: self.new_password.as_ref().map(|password| {
334 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
335 B64::from(password.as_slice()).to_string()
336 }),
337
338 r#type: self.r#type,
339 file: self.file.encrypt_composite(ctx, send_key)?,
340 text: self.text.encrypt_composite(ctx, send_key)?,
341
342 max_access_count: self.max_access_count,
343 access_count: self.access_count,
344 disabled: self.disabled,
345 hide_email: self.hide_email,
346
347 revision_date: self.revision_date,
348 deletion_date: self.deletion_date,
349 expiration_date: self.expiration_date,
350 })
351 }
352}
353
354impl TryFrom<SendResponseModel> for Send {
355 type Error = SendParseError;
356
357 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
358 Ok(Send {
359 id: send.id,
360 access_id: send.access_id,
361 name: require!(send.name).parse()?,
362 notes: EncString::try_from_optional(send.notes)?,
363 key: require!(send.key).parse()?,
364 password: send.password,
365 r#type: require!(send.r#type).into(),
366 file: send.file.map(|f| (*f).try_into()).transpose()?,
367 text: send.text.map(|t| (*t).try_into()).transpose()?,
368 max_access_count: send.max_access_count.map(|s| s as u32),
369 access_count: require!(send.access_count) as u32,
370 disabled: send.disabled.unwrap_or(false),
371 hide_email: send.hide_email.unwrap_or(false),
372 revision_date: require!(send.revision_date).parse()?,
373 deletion_date: require!(send.deletion_date).parse()?,
374 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
375 })
376 }
377}
378
379impl From<bitwarden_api_api::models::SendType> for SendType {
380 fn from(t: bitwarden_api_api::models::SendType) -> Self {
381 match t {
382 bitwarden_api_api::models::SendType::Text => SendType::Text,
383 bitwarden_api_api::models::SendType::File => SendType::File,
384 }
385 }
386}
387
388impl TryFrom<SendFileModel> for SendFile {
389 type Error = SendParseError;
390
391 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
392 Ok(SendFile {
393 id: file.id,
394 file_name: require!(file.file_name).parse()?,
395 size: file.size.map(|v| v.to_string()),
396 size_name: file.size_name,
397 })
398 }
399}
400
401impl TryFrom<SendTextModel> for SendText {
402 type Error = SendParseError;
403
404 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
405 Ok(SendText {
406 text: EncString::try_from_optional(text.text)?,
407 hidden: text.hidden.unwrap_or(false),
408 })
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use bitwarden_core::key_management::create_test_crypto_with_user_key;
415 use bitwarden_crypto::SymmetricCryptoKey;
416
417 use super::*;
418
419 #[test]
420 fn test_get_send_key() {
421 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
423 let crypto = create_test_crypto_with_user_key(user_key);
424 let mut ctx = crypto.context();
425
426 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
427 .parse()
428 .unwrap();
429
430 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeyId::User).unwrap();
432 #[allow(deprecated)]
433 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
434 let send_key_b64 = send_key.to_base64();
435 assert_eq!(
436 send_key_b64.to_string(),
437 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
438 );
439 }
440
441 #[test]
442 pub fn test_decrypt() {
443 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
444 let crypto = create_test_crypto_with_user_key(user_key);
445
446 let send = Send {
447 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
448 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
449 r#type: SendType::Text,
450 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
451 .unwrap(),
452 notes: None,
453 file: None,
454 text: Some(SendText {
455 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
456 hidden: false,
457 }),
458 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
459 max_access_count: None,
460 access_count: 0,
461 password: None,
462 disabled: false,
463 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
464 expiration_date: None,
465 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
466 hide_email: false,
467 };
468
469 let view: SendView = crypto.decrypt(&send).unwrap();
470
471 let expected = SendView {
472 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
473 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
474 name: "Test".to_string(),
475 notes: None,
476 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
477 new_password: None,
478 has_password: false,
479 r#type: SendType::Text,
480 file: None,
481 text: Some(SendTextView {
482 text: Some("This is a test".to_owned()),
483 hidden: false,
484 }),
485 max_access_count: None,
486 access_count: 0,
487 disabled: false,
488 hide_email: false,
489 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
490 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
491 expiration_date: None,
492 };
493
494 assert_eq!(view, expected);
495 }
496
497 #[test]
498 pub fn test_encrypt() {
499 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
500 let crypto = create_test_crypto_with_user_key(user_key);
501
502 let view = SendView {
503 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
504 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
505 name: "Test".to_string(),
506 notes: None,
507 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
508 new_password: None,
509 has_password: false,
510 r#type: SendType::Text,
511 file: None,
512 text: Some(SendTextView {
513 text: Some("This is a test".to_owned()),
514 hidden: false,
515 }),
516 max_access_count: None,
517 access_count: 0,
518 disabled: false,
519 hide_email: false,
520 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
521 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
522 expiration_date: None,
523 };
524
525 let v: SendView = crypto
527 .decrypt(&crypto.encrypt(view.clone()).unwrap())
528 .unwrap();
529 assert_eq!(v, view);
530 }
531
532 #[test]
533 pub fn test_create() {
534 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
535 let crypto = create_test_crypto_with_user_key(user_key);
536
537 let view = SendView {
538 id: None,
539 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
540 name: "Test".to_string(),
541 notes: None,
542 key: None,
543 new_password: None,
544 has_password: false,
545 r#type: SendType::Text,
546 file: None,
547 text: Some(SendTextView {
548 text: Some("This is a test".to_owned()),
549 hidden: false,
550 }),
551 max_access_count: None,
552 access_count: 0,
553 disabled: false,
554 hide_email: false,
555 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
556 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
557 expiration_date: None,
558 };
559
560 let v: SendView = crypto
562 .decrypt(&crypto.encrypt(view.clone()).unwrap())
563 .unwrap();
564
565 let t = SendView { key: None, ..v };
567 assert_eq!(t, view);
568 }
569
570 #[test]
571 pub fn test_create_password() {
572 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
573 let crypto = create_test_crypto_with_user_key(user_key);
574
575 let view = SendView {
576 id: None,
577 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
578 name: "Test".to_owned(),
579 notes: None,
580 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
581 new_password: Some("abc123".to_owned()),
582 has_password: false,
583 r#type: SendType::Text,
584 file: None,
585 text: Some(SendTextView {
586 text: Some("This is a test".to_owned()),
587 hidden: false,
588 }),
589 max_access_count: None,
590 access_count: 0,
591 disabled: false,
592 hide_email: false,
593 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
594 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
595 expiration_date: None,
596 };
597
598 let send: Send = crypto.encrypt(view).unwrap();
599
600 assert_eq!(
601 send.password,
602 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
603 );
604
605 let v: SendView = crypto.decrypt(&send).unwrap();
606 assert_eq!(v.new_password, None);
607 assert!(v.has_password);
608 }
609}