1use bitwarden_api_api::models::{
2 SendFileModel, SendResponseModel, SendTextModel, SendWithIdRequestModel,
3};
4use bitwarden_core::{
5 key_management::{KeyIds, SymmetricKeyId},
6 require,
7};
8use bitwarden_crypto::{
9 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
10 OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes,
11};
12use bitwarden_encoding::{B64, B64Url};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use serde_repr::{Deserialize_repr, Serialize_repr};
16use uuid::Uuid;
17use zeroize::Zeroizing;
18
19use crate::SendParseError;
20
21const SEND_ITERATIONS: u32 = 100_000;
22
23#[derive(Serialize, Deserialize, Debug)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
26pub struct SendFile {
27 pub id: Option<String>,
28 pub file_name: EncString,
29 pub size: Option<String>,
30 pub size_name: Option<String>,
32}
33
34#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
37pub struct SendFileView {
38 pub id: Option<String>,
39 pub file_name: String,
40 pub size: Option<String>,
41 pub size_name: Option<String>,
43}
44
45#[derive(Serialize, Deserialize, Debug)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
48pub struct SendText {
49 pub text: Option<EncString>,
50 pub hidden: bool,
51}
52
53#[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>,
60 pub hidden: bool,
62}
63
64#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
66#[repr(u8)]
67#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
68pub enum SendType {
69 Text = 0,
71 File = 1,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
77#[repr(u8)]
78#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
79pub enum AuthType {
80 Email = 0,
82
83 Password = 1,
85
86 None = 2,
88}
89
90#[allow(missing_docs)]
91#[derive(Serialize, Deserialize, Debug)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
94pub struct Send {
95 pub id: Option<Uuid>,
96 pub access_id: Option<String>,
97
98 pub name: EncString,
99 pub notes: Option<EncString>,
100 pub key: EncString,
101 pub password: Option<String>,
102
103 pub r#type: SendType,
104 pub file: Option<SendFile>,
105 pub text: Option<SendText>,
106
107 pub max_access_count: Option<u32>,
108 pub access_count: u32,
109 pub disabled: bool,
110 pub hide_email: bool,
111
112 pub revision_date: DateTime<Utc>,
113 pub deletion_date: DateTime<Utc>,
114 pub expiration_date: Option<DateTime<Utc>>,
115
116 pub emails: Option<String>,
120 pub auth_type: AuthType,
121}
122
123impl From<Send> for SendWithIdRequestModel {
124 fn from(send: Send) -> Self {
125 let file_length = send.file.as_ref().and_then(|file| {
126 file.size
127 .as_deref()
128 .and_then(|size| size.parse::<i64>().ok())
129 });
130
131 SendWithIdRequestModel {
132 r#type: Some(send.r#type.into()),
133 auth_type: Some(send.auth_type.into()),
134 file_length,
135 name: Some(send.name.to_string()),
136 notes: send.notes.map(|notes| notes.to_string()),
137 key: send.key.to_string(),
138 max_access_count: send.max_access_count.map(|count| count as i32),
139 expiration_date: send.expiration_date.map(|date| date.to_rfc3339()),
140 deletion_date: send.deletion_date.to_rfc3339(),
141 file: send.file.map(|file| Box::new(file.into())),
142 text: send.text.map(|text| Box::new(text.into())),
143 password: send.password,
144 emails: send.emails,
145 disabled: send.disabled,
146 hide_email: Some(send.hide_email),
147 id: send
148 .id
149 .expect("SendWithIdRequestModel conversion requires send id"),
150 }
151 }
152}
153
154#[allow(missing_docs)]
155#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
156#[serde(rename_all = "camelCase", deny_unknown_fields)]
157#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
158pub struct SendView {
159 pub id: Option<Uuid>,
160 pub access_id: Option<String>,
161
162 pub name: String,
163 pub notes: Option<String>,
164 pub key: Option<String>,
166 pub new_password: Option<String>,
170 pub has_password: bool,
173
174 pub r#type: SendType,
175 pub file: Option<SendFileView>,
176 pub text: Option<SendTextView>,
177
178 pub max_access_count: Option<u32>,
179 pub access_count: u32,
180 pub disabled: bool,
181 pub hide_email: bool,
182
183 pub revision_date: DateTime<Utc>,
184 pub deletion_date: DateTime<Utc>,
185 pub expiration_date: Option<DateTime<Utc>>,
186
187 pub emails: Vec<String>,
191 pub auth_type: AuthType,
192}
193
194#[allow(missing_docs)]
195#[derive(Serialize, Deserialize, Debug)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
198pub struct SendListView {
199 pub id: Option<Uuid>,
200 pub access_id: Option<String>,
201
202 pub name: String,
203
204 pub r#type: SendType,
205 pub disabled: bool,
206
207 pub revision_date: DateTime<Utc>,
208 pub deletion_date: DateTime<Utc>,
209 pub expiration_date: Option<DateTime<Utc>>,
210
211 pub auth_type: AuthType,
212}
213
214impl Send {
215 #[allow(missing_docs)]
216 pub fn get_key(
217 ctx: &mut KeyStoreContext<KeyIds>,
218 send_key: &EncString,
219 enc_key: SymmetricKeyId,
220 ) -> Result<SymmetricKeyId, CryptoError> {
221 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
222 Self::derive_shareable_key(ctx, &key)
223 }
224
225 fn derive_shareable_key(
226 ctx: &mut KeyStoreContext<KeyIds>,
227 key: &[u8],
228 ) -> Result<SymmetricKeyId, CryptoError> {
229 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
230 ctx.derive_shareable_key(key, "send", Some("send"))
231 }
232}
233
234impl IdentifyKey<SymmetricKeyId> for Send {
235 fn key_identifier(&self) -> SymmetricKeyId {
236 SymmetricKeyId::User
237 }
238}
239
240impl IdentifyKey<SymmetricKeyId> for SendView {
241 fn key_identifier(&self) -> SymmetricKeyId {
242 SymmetricKeyId::User
243 }
244}
245
246impl Decryptable<KeyIds, SymmetricKeyId, SendTextView> for SendText {
247 fn decrypt(
248 &self,
249 ctx: &mut KeyStoreContext<KeyIds>,
250 key: SymmetricKeyId,
251 ) -> Result<SendTextView, CryptoError> {
252 Ok(SendTextView {
253 text: self.text.decrypt(ctx, key)?,
254 hidden: self.hidden,
255 })
256 }
257}
258
259impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendText> for SendTextView {
260 fn encrypt_composite(
261 &self,
262 ctx: &mut KeyStoreContext<KeyIds>,
263 key: SymmetricKeyId,
264 ) -> Result<SendText, CryptoError> {
265 Ok(SendText {
266 text: self.text.encrypt(ctx, key)?,
267 hidden: self.hidden,
268 })
269 }
270}
271
272impl Decryptable<KeyIds, SymmetricKeyId, SendFileView> for SendFile {
273 fn decrypt(
274 &self,
275 ctx: &mut KeyStoreContext<KeyIds>,
276 key: SymmetricKeyId,
277 ) -> Result<SendFileView, CryptoError> {
278 Ok(SendFileView {
279 id: self.id.clone(),
280 file_name: self.file_name.decrypt(ctx, key)?,
281 size: self.size.clone(),
282 size_name: self.size_name.clone(),
283 })
284 }
285}
286
287impl CompositeEncryptable<KeyIds, SymmetricKeyId, SendFile> for SendFileView {
288 fn encrypt_composite(
289 &self,
290 ctx: &mut KeyStoreContext<KeyIds>,
291 key: SymmetricKeyId,
292 ) -> Result<SendFile, CryptoError> {
293 Ok(SendFile {
294 id: self.id.clone(),
295 file_name: self.file_name.encrypt(ctx, key)?,
296 size: self.size.clone(),
297 size_name: self.size_name.clone(),
298 })
299 }
300}
301
302impl Decryptable<KeyIds, SymmetricKeyId, SendView> for Send {
303 fn decrypt(
304 &self,
305 ctx: &mut KeyStoreContext<KeyIds>,
306 key: SymmetricKeyId,
307 ) -> Result<SendView, CryptoError> {
308 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
312 let key = Send::derive_shareable_key(ctx, &k)?;
313
314 Ok(SendView {
315 id: self.id,
316 access_id: self.access_id.clone(),
317
318 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
319 notes: self.notes.decrypt(ctx, key).ok().flatten(),
320 key: Some(B64Url::from(k).to_string()),
321 new_password: None,
322 has_password: self.password.is_some(),
323
324 r#type: self.r#type,
325 file: self.file.decrypt(ctx, key).ok().flatten(),
326 text: self.text.decrypt(ctx, key).ok().flatten(),
327
328 max_access_count: self.max_access_count,
329 access_count: self.access_count,
330 disabled: self.disabled,
331 hide_email: self.hide_email,
332
333 revision_date: self.revision_date,
334 deletion_date: self.deletion_date,
335 expiration_date: self.expiration_date,
336
337 emails: self
338 .emails
339 .as_deref()
340 .unwrap_or_default()
341 .split(',')
342 .map(|e| e.trim())
343 .filter(|e| !e.is_empty())
344 .map(String::from)
345 .collect(),
346 auth_type: self.auth_type,
347 })
348 }
349}
350
351impl Decryptable<KeyIds, SymmetricKeyId, SendListView> for Send {
352 fn decrypt(
353 &self,
354 ctx: &mut KeyStoreContext<KeyIds>,
355 key: SymmetricKeyId,
356 ) -> Result<SendListView, CryptoError> {
357 let key = Send::get_key(ctx, &self.key, key)?;
361
362 Ok(SendListView {
363 id: self.id,
364 access_id: self.access_id.clone(),
365
366 name: self.name.decrypt(ctx, key)?,
367 r#type: self.r#type,
368
369 disabled: self.disabled,
370
371 revision_date: self.revision_date,
372 deletion_date: self.deletion_date,
373 expiration_date: self.expiration_date,
374
375 auth_type: self.auth_type,
376 })
377 }
378}
379
380impl CompositeEncryptable<KeyIds, SymmetricKeyId, Send> for SendView {
381 fn encrypt_composite(
382 &self,
383 ctx: &mut KeyStoreContext<KeyIds>,
384 key: SymmetricKeyId,
385 ) -> Result<Send, CryptoError> {
386 let k = match (&self.key, &self.id) {
390 (Some(k), _) => B64Url::try_from(k.as_str())
392 .map_err(|_| CryptoError::InvalidKey)?
393 .as_bytes()
394 .to_vec(),
395 (None, None) => {
397 let key = generate_random_bytes::<[u8; 16]>();
398 key.to_vec()
399 }
400 _ => return Err(CryptoError::InvalidKey),
402 };
403 let send_key = Send::derive_shareable_key(ctx, &k)?;
404
405 Ok(Send {
406 id: self.id,
407 access_id: self.access_id.clone(),
408
409 name: self.name.encrypt(ctx, send_key)?,
410 notes: self.notes.encrypt(ctx, send_key)?,
411 key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
412 password: self.new_password.as_ref().map(|password| {
413 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
414 B64::from(password.as_slice()).to_string()
415 }),
416
417 r#type: self.r#type,
418 file: self.file.encrypt_composite(ctx, send_key)?,
419 text: self.text.encrypt_composite(ctx, send_key)?,
420
421 max_access_count: self.max_access_count,
422 access_count: self.access_count,
423 disabled: self.disabled,
424 hide_email: self.hide_email,
425
426 revision_date: self.revision_date,
427 deletion_date: self.deletion_date,
428 expiration_date: self.expiration_date,
429
430 emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
431 auth_type: self.auth_type,
432 })
433 }
434}
435
436impl TryFrom<SendResponseModel> for Send {
437 type Error = SendParseError;
438
439 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
440 let auth_type = match send.auth_type {
441 Some(t) => t.try_into()?,
442 None => {
443 if send.password.is_some() {
444 AuthType::Password
445 } else if send.emails.is_some() {
446 AuthType::Email
447 } else {
448 AuthType::None
449 }
450 }
451 };
452 Ok(Send {
453 id: send.id,
454 access_id: send.access_id,
455 name: require!(send.name).parse()?,
456 notes: EncString::try_from_optional(send.notes)?,
457 key: require!(send.key).parse()?,
458 password: send.password,
459 r#type: require!(send.r#type).try_into()?,
460 file: send.file.map(|f| (*f).try_into()).transpose()?,
461 text: send.text.map(|t| (*t).try_into()).transpose()?,
462 max_access_count: send.max_access_count.map(|s| s as u32),
463 access_count: require!(send.access_count) as u32,
464 disabled: send.disabled.unwrap_or(false),
465 hide_email: send.hide_email.unwrap_or(false),
466 revision_date: require!(send.revision_date).parse()?,
467 deletion_date: require!(send.deletion_date).parse()?,
468 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
469 emails: send.emails,
470 auth_type,
471 })
472 }
473}
474
475impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
476 type Error = bitwarden_core::MissingFieldError;
477
478 fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
479 Ok(match t {
480 bitwarden_api_api::models::SendType::Text => SendType::Text,
481 bitwarden_api_api::models::SendType::File => SendType::File,
482 bitwarden_api_api::models::SendType::__Unknown(_) => {
483 return Err(bitwarden_core::MissingFieldError("type"));
484 }
485 })
486 }
487}
488
489impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
490 type Error = bitwarden_core::MissingFieldError;
491
492 fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
493 Ok(match value {
494 bitwarden_api_api::models::AuthType::Email => AuthType::Email,
495 bitwarden_api_api::models::AuthType::Password => AuthType::Password,
496 bitwarden_api_api::models::AuthType::None => AuthType::None,
497 bitwarden_api_api::models::AuthType::__Unknown(_) => {
498 return Err(bitwarden_core::MissingFieldError("auth_type"));
499 }
500 })
501 }
502}
503
504impl From<SendType> for bitwarden_api_api::models::SendType {
505 fn from(t: SendType) -> Self {
506 match t {
507 SendType::Text => bitwarden_api_api::models::SendType::Text,
508 SendType::File => bitwarden_api_api::models::SendType::File,
509 }
510 }
511}
512
513impl From<AuthType> for bitwarden_api_api::models::AuthType {
514 fn from(value: AuthType) -> Self {
515 match value {
516 AuthType::Email => bitwarden_api_api::models::AuthType::Email,
517 AuthType::Password => bitwarden_api_api::models::AuthType::Password,
518 AuthType::None => bitwarden_api_api::models::AuthType::None,
519 }
520 }
521}
522
523impl From<SendFile> for SendFileModel {
524 fn from(file: SendFile) -> Self {
525 SendFileModel {
526 id: file.id,
527 file_name: Some(file.file_name.to_string()),
528 size: file.size.and_then(|size| size.parse::<i64>().ok()),
529 size_name: file.size_name,
530 }
531 }
532}
533
534impl From<SendText> for SendTextModel {
535 fn from(text: SendText) -> Self {
536 SendTextModel {
537 text: text.text.map(|text| text.to_string()),
538 hidden: Some(text.hidden),
539 }
540 }
541}
542
543impl TryFrom<SendFileModel> for SendFile {
544 type Error = SendParseError;
545
546 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
547 Ok(SendFile {
548 id: file.id,
549 file_name: require!(file.file_name).parse()?,
550 size: file.size.map(|v| v.to_string()),
551 size_name: file.size_name,
552 })
553 }
554}
555
556impl TryFrom<SendTextModel> for SendText {
557 type Error = SendParseError;
558
559 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
560 Ok(SendText {
561 text: EncString::try_from_optional(text.text)?,
562 hidden: text.hidden.unwrap_or(false),
563 })
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use bitwarden_core::key_management::create_test_crypto_with_user_key;
570 use bitwarden_crypto::SymmetricCryptoKey;
571
572 use super::*;
573
574 #[test]
575 fn test_get_send_key() {
576 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
578 let crypto = create_test_crypto_with_user_key(user_key);
579 let mut ctx = crypto.context();
580
581 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
582 .parse()
583 .unwrap();
584
585 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeyId::User).unwrap();
587 #[allow(deprecated)]
588 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
589 let send_key_b64 = send_key.to_base64();
590 assert_eq!(
591 send_key_b64.to_string(),
592 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
593 );
594 }
595
596 #[test]
597 pub fn test_decrypt() {
598 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
599 let crypto = create_test_crypto_with_user_key(user_key);
600
601 let send = Send {
602 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
603 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
604 r#type: SendType::Text,
605 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
606 .unwrap(),
607 notes: None,
608 file: None,
609 text: Some(SendText {
610 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
611 hidden: false,
612 }),
613 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
614 max_access_count: None,
615 access_count: 0,
616 password: None,
617 disabled: false,
618 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
619 expiration_date: None,
620 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
621 hide_email: false,
622 emails: None,
623 auth_type: AuthType::None,
624 };
625
626 let view: SendView = crypto.decrypt(&send).unwrap();
627
628 let expected = SendView {
629 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
630 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
631 name: "Test".to_string(),
632 notes: None,
633 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
634 new_password: None,
635 has_password: false,
636 r#type: SendType::Text,
637 file: None,
638 text: Some(SendTextView {
639 text: Some("This is a test".to_owned()),
640 hidden: false,
641 }),
642 max_access_count: None,
643 access_count: 0,
644 disabled: false,
645 hide_email: false,
646 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
647 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
648 expiration_date: None,
649 emails: Vec::new(),
650 auth_type: AuthType::None,
651 };
652
653 assert_eq!(view, expected);
654 }
655
656 #[test]
657 pub fn test_encrypt() {
658 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
659 let crypto = create_test_crypto_with_user_key(user_key);
660
661 let view = SendView {
662 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
663 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
664 name: "Test".to_string(),
665 notes: None,
666 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
667 new_password: None,
668 has_password: false,
669 r#type: SendType::Text,
670 file: None,
671 text: Some(SendTextView {
672 text: Some("This is a test".to_owned()),
673 hidden: false,
674 }),
675 max_access_count: None,
676 access_count: 0,
677 disabled: false,
678 hide_email: false,
679 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
680 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
681 expiration_date: None,
682 emails: Vec::new(),
683 auth_type: AuthType::None,
684 };
685
686 let v: SendView = crypto
688 .decrypt(&crypto.encrypt(view.clone()).unwrap())
689 .unwrap();
690 assert_eq!(v, view);
691 }
692
693 #[test]
694 pub fn test_create() {
695 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
696 let crypto = create_test_crypto_with_user_key(user_key);
697
698 let view = SendView {
699 id: None,
700 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
701 name: "Test".to_string(),
702 notes: None,
703 key: None,
704 new_password: None,
705 has_password: false,
706 r#type: SendType::Text,
707 file: None,
708 text: Some(SendTextView {
709 text: Some("This is a test".to_owned()),
710 hidden: false,
711 }),
712 max_access_count: None,
713 access_count: 0,
714 disabled: false,
715 hide_email: false,
716 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
717 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
718 expiration_date: None,
719 emails: Vec::new(),
720 auth_type: AuthType::None,
721 };
722
723 let v: SendView = crypto
725 .decrypt(&crypto.encrypt(view.clone()).unwrap())
726 .unwrap();
727
728 let t = SendView { key: None, ..v };
730 assert_eq!(t, view);
731 }
732
733 #[test]
734 pub fn test_create_password() {
735 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
736 let crypto = create_test_crypto_with_user_key(user_key);
737
738 let view = SendView {
739 id: None,
740 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
741 name: "Test".to_owned(),
742 notes: None,
743 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
744 new_password: Some("abc123".to_owned()),
745 has_password: false,
746 r#type: SendType::Text,
747 file: None,
748 text: Some(SendTextView {
749 text: Some("This is a test".to_owned()),
750 hidden: false,
751 }),
752 max_access_count: None,
753 access_count: 0,
754 disabled: false,
755 hide_email: false,
756 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
757 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
758 expiration_date: None,
759 emails: Vec::new(),
760 auth_type: AuthType::Password,
761 };
762
763 let send: Send = crypto.encrypt(view).unwrap();
764
765 assert_eq!(
766 send.password,
767 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
768 );
769 assert_eq!(send.auth_type, AuthType::Password);
770
771 let v: SendView = crypto.decrypt(&send).unwrap();
772 assert_eq!(v.new_password, None);
773 assert!(v.has_password);
774 assert_eq!(v.auth_type, AuthType::Password);
775 }
776
777 #[test]
778 pub fn test_create_email_otp() {
779 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
780 let crypto = create_test_crypto_with_user_key(user_key);
781
782 let view = SendView {
783 id: None,
784 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
785 name: "Test".to_owned(),
786 notes: None,
787 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
788 new_password: None,
789 has_password: false,
790 r#type: SendType::Text,
791 file: None,
792 text: Some(SendTextView {
793 text: Some("This is a test".to_owned()),
794 hidden: false,
795 }),
796 max_access_count: None,
797 access_count: 0,
798 disabled: false,
799 hide_email: false,
800 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
801 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
802 expiration_date: None,
803 emails: vec![
804 String::from("[email protected]"),
805 String::from("[email protected]"),
806 ],
807 auth_type: AuthType::Email,
808 };
809
810 let send: Send = crypto.encrypt(view.clone()).unwrap();
811
812 let v: SendView = crypto.decrypt(&send).unwrap();
814
815 assert_eq!(v, view);
816 }
817
818 #[test]
819 fn test_send_into_send_with_id_request_model() {
820 let send_id = Uuid::parse_str("3d80dd72-2d14-4f26-812c-b0f0018aa144").unwrap();
821 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
822 .unwrap()
823 .with_timezone(&Utc);
824 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
825 .unwrap()
826 .with_timezone(&Utc);
827 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
828 .unwrap()
829 .with_timezone(&Utc);
830
831 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
832 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
833 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
834 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
835 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
836
837 let send = Send {
838 id: Some(send_id),
839 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
840 name: name.parse().unwrap(),
841 notes: Some(notes.parse().unwrap()),
842 key: key.parse().unwrap(),
843 password: Some("hash".to_string()),
844 r#type: SendType::File,
845 file: Some(SendFile {
846 id: Some("file-id".to_string()),
847 file_name: file_name.parse().unwrap(),
848 size: Some("1234".to_string()),
849 size_name: Some("1.2 KB".to_string()),
850 }),
851 text: Some(SendText {
852 text: Some(text_value.parse().unwrap()),
853 hidden: true,
854 }),
855 max_access_count: Some(42),
856 access_count: 0,
857 disabled: true,
858 hide_email: true,
859 revision_date,
860 deletion_date,
861 expiration_date: Some(expiration_date),
862 emails: Some("[email protected],[email protected]".to_string()),
863 auth_type: AuthType::Email,
864 };
865
866 let model: SendWithIdRequestModel = send.into();
867
868 assert_eq!(model.id, send_id);
869 assert_eq!(
870 model.r#type,
871 Some(bitwarden_api_api::models::SendType::File)
872 );
873 assert_eq!(
874 model.auth_type,
875 Some(bitwarden_api_api::models::AuthType::Email)
876 );
877 assert_eq!(model.file_length, Some(1234));
878 assert_eq!(model.name.as_deref(), Some(name));
879 assert_eq!(model.notes.as_deref(), Some(notes));
880 assert_eq!(model.key, key);
881 assert_eq!(model.max_access_count, Some(42));
882 assert_eq!(
883 model
884 .expiration_date
885 .unwrap()
886 .parse::<DateTime<Utc>>()
887 .unwrap(),
888 expiration_date
889 );
890 assert_eq!(
891 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
892 deletion_date
893 );
894 assert_eq!(model.password.as_deref(), Some("hash"));
895 assert_eq!(
896 model.emails.as_deref(),
897 Some("[email protected],[email protected]")
898 );
899 assert!(model.disabled);
900 assert_eq!(model.hide_email, Some(true));
901
902 let file = model.file.unwrap();
903 assert_eq!(file.id.as_deref(), Some("file-id"));
904 assert_eq!(file.file_name.as_deref(), Some(file_name));
905 assert_eq!(file.size, Some(1234));
906 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
907
908 let text = model.text.unwrap();
909 assert_eq!(text.text.as_deref(), Some(text_value));
910 assert_eq!(text.hidden, Some(true));
911 }
912}