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#[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 pub key: Option<String>,
107 pub new_password: Option<String>,
111 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 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 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 let k = match (&self.key, &self.id) {
310 (Some(k), _) => URL_SAFE_NO_PAD
312 .decode(k)
313 .map_err(|_| CryptoError::InvalidKey)?,
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: 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 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!(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 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 let v: SendView = crypto
557 .decrypt(&crypto.encrypt(view.clone()).unwrap())
558 .unwrap();
559
560 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}