1use bitwarden_api_api::models::{
2 SendFileModel, SendResponseModel, SendTextModel, SendWithIdRequestModel,
3};
4use bitwarden_core::{
5 key_management::{KeySlotIds, SymmetricKeySlotId},
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 bitwarden_uuid::uuid_newtype;
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use serde_repr::{Deserialize_repr, Serialize_repr};
17use thiserror::Error;
18use zeroize::Zeroizing;
19#[cfg(feature = "wasm")]
20use {tsify::Tsify, wasm_bindgen::prelude::*};
21
22use crate::SendParseError;
23pub const SEND_ITERATIONS: u32 = 100_000;
24
25uuid_newtype!(pub SendId);
26
27#[derive(Debug, Error)]
29#[error("Email authentication requires at least one email address")]
30pub struct EmptyEmailListError;
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
34#[serde(rename_all = "camelCase", deny_unknown_fields)]
35#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
36#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
37pub struct SendFile {
38 pub id: Option<String>,
40 pub file_name: EncString,
42 pub size: Option<String>,
44 pub size_name: Option<String>,
46}
47
48#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53pub struct SendFileView {
54 pub id: Option<String>,
56 pub file_name: String,
58 pub size: Option<String>,
60 pub size_name: Option<String>,
62}
63
64#[derive(Serialize, Deserialize, Debug, Clone)]
66#[serde(rename_all = "camelCase", deny_unknown_fields)]
67#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
68#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
69pub struct SendText {
70 pub text: Option<EncString>,
71 pub hidden: bool,
72}
73
74#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
78#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
79pub struct SendTextView {
80 pub text: Option<String>,
82 pub hidden: bool,
84}
85
86#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
88#[repr(u8)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
90#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
91pub enum SendType {
92 Text = 0,
94 File = 1,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
100#[repr(u8)]
101#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
102#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
103pub enum AuthType {
104 Email = 0,
106
107 Password = 1,
109
110 None = 2,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "camelCase")]
118#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
119#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
120pub enum SendAuthType {
121 None,
123 Password {
125 password: String,
127 },
128 Emails {
130 emails: Vec<String>,
132 },
133}
134
135impl SendAuthType {
136 pub fn auth_type(&self) -> AuthType {
138 match self {
139 SendAuthType::None => AuthType::None,
140 SendAuthType::Password { .. } => AuthType::Password,
141 SendAuthType::Emails { .. } => AuthType::Email,
142 }
143 }
144
145 pub(crate) fn validate(&self) -> Result<(), EmptyEmailListError> {
148 if let SendAuthType::Emails { emails } = self
149 && emails.is_empty()
150 {
151 return Err(EmptyEmailListError);
152 }
153 Ok(())
154 }
155
156 pub(crate) fn auth_data(&self, k: &[u8]) -> (Option<String>, Option<String>) {
159 match self {
160 SendAuthType::Password { password } => {
161 let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), k, SEND_ITERATIONS);
162 (Some(B64::from(hashed.as_slice()).to_string()), None)
163 }
164 SendAuthType::Emails { emails } => {
165 let emails_str = if emails.is_empty() {
166 None
167 } else {
168 Some(emails.join(","))
169 };
170 (None, emails_str)
171 }
172 SendAuthType::None => (None, None),
173 }
174 }
175}
176
177#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
179#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
180pub enum SendViewType {
181 File(SendFileView),
183 Text(SendTextView),
185}
186
187type SendApiModels = (
189 bitwarden_api_api::models::SendType,
190 Option<Box<bitwarden_api_api::models::SendFileModel>>,
191 Option<Box<bitwarden_api_api::models::SendTextModel>>,
192);
193
194impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendApiModels> for SendViewType {
195 fn encrypt_composite(
196 &self,
197 ctx: &mut KeyStoreContext<KeySlotIds>,
198 key: SymmetricKeySlotId,
199 ) -> Result<SendApiModels, CryptoError> {
200 match self {
201 SendViewType::File(f) => Ok((
202 bitwarden_api_api::models::SendType::File,
203 Some(Box::new(bitwarden_api_api::models::SendFileModel {
204 id: f.id.clone(),
205 file_name: Some(f.file_name.encrypt(ctx, key)?.to_string()),
206 size: f.size.clone(),
207 size_name: f.size_name.clone(),
208 })),
209 None,
210 )),
211 SendViewType::Text(t) => Ok((
212 bitwarden_api_api::models::SendType::Text,
213 None,
214 Some(Box::new(bitwarden_api_api::models::SendTextModel {
215 text: t
216 .text
217 .as_ref()
218 .map(|txt| txt.encrypt(ctx, key))
219 .transpose()?
220 .map(|e| e.to_string()),
221 hidden: Some(t.hidden),
222 })),
223 )),
224 }
225 }
226}
227
228#[allow(missing_docs)]
229#[derive(Serialize, Deserialize, Debug, Clone)]
230#[serde(rename_all = "camelCase", deny_unknown_fields)]
231#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
232#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
233pub struct Send {
234 pub id: Option<SendId>,
235 pub access_id: Option<String>,
236
237 pub name: EncString,
238 pub notes: Option<EncString>,
239 pub key: EncString,
240 pub password: Option<String>,
241
242 pub r#type: SendType,
243 pub file: Option<SendFile>,
244 pub text: Option<SendText>,
245
246 pub max_access_count: Option<u32>,
247 pub access_count: u32,
248 pub disabled: bool,
249 pub hide_email: bool,
250
251 pub revision_date: DateTime<Utc>,
252 pub deletion_date: DateTime<Utc>,
253 pub expiration_date: Option<DateTime<Utc>>,
254
255 pub emails: Option<String>,
260 pub auth_type: AuthType,
261}
262
263bitwarden_state::register_repository_item!(SendId => Send, "Send");
264
265impl From<Send> for SendWithIdRequestModel {
266 fn from(send: Send) -> Self {
267 let file_length = send.file.as_ref().and_then(|file| {
268 file.size
269 .as_deref()
270 .and_then(|size| size.parse::<i64>().ok())
271 });
272
273 SendWithIdRequestModel {
274 r#type: Some(send.r#type.into()),
275 auth_type: Some(send.auth_type.into()),
276 file_length,
277 name: Some(send.name.to_string()),
278 notes: send.notes.map(|notes| notes.to_string()),
279 key: send.key.to_string(),
280 max_access_count: send.max_access_count.map(|count| count as i32),
281 expiration_date: send.expiration_date.map(|date| date.to_rfc3339()),
282 deletion_date: send.deletion_date.to_rfc3339(),
283 file: send.file.map(|file| Box::new(file.into())),
284 text: send.text.map(|text| Box::new(text.into())),
285 password: send.password,
286 emails: send.emails,
287 disabled: send.disabled,
288 hide_email: Some(send.hide_email),
289 id: send
290 .id
291 .expect("SendWithIdRequestModel conversion requires send id")
292 .into(),
293 }
294 }
295}
296
297#[allow(missing_docs)]
298#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
299#[serde(rename_all = "camelCase", deny_unknown_fields)]
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
302pub struct SendView {
303 pub id: Option<SendId>,
304 pub access_id: Option<String>,
305
306 pub name: String,
307 pub notes: Option<String>,
308 pub key: Option<String>,
310 pub new_password: Option<String>,
314 pub has_password: bool,
317
318 pub r#type: SendType,
319 pub file: Option<SendFileView>,
320 pub text: Option<SendTextView>,
321
322 pub max_access_count: Option<u32>,
323 pub access_count: u32,
324 pub disabled: bool,
325 pub hide_email: bool,
326
327 pub revision_date: DateTime<Utc>,
328 pub deletion_date: DateTime<Utc>,
329 pub expiration_date: Option<DateTime<Utc>>,
330
331 pub emails: Vec<String>,
336 pub auth_type: AuthType,
337}
338
339#[allow(missing_docs)]
340#[derive(Serialize, Deserialize, Debug)]
341#[serde(rename_all = "camelCase", deny_unknown_fields)]
342#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
343#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
344pub struct SendListView {
345 pub id: Option<SendId>,
346 pub access_id: Option<String>,
347
348 pub name: String,
349
350 pub r#type: SendType,
351 pub disabled: bool,
352
353 pub revision_date: DateTime<Utc>,
354 pub deletion_date: DateTime<Utc>,
355 pub expiration_date: Option<DateTime<Utc>>,
356
357 pub auth_type: AuthType,
358}
359
360impl Send {
361 #[allow(missing_docs)]
362 pub fn get_key(
363 ctx: &mut KeyStoreContext<KeySlotIds>,
364 send_key: &EncString,
365 enc_key: SymmetricKeySlotId,
366 ) -> Result<SymmetricKeySlotId, CryptoError> {
367 let key: Vec<u8> = send_key.decrypt(ctx, enc_key)?;
368 Self::derive_shareable_key(ctx, &key)
369 }
370
371 pub(crate) fn derive_shareable_key(
372 ctx: &mut KeyStoreContext<KeySlotIds>,
373 key: &[u8],
374 ) -> Result<SymmetricKeySlotId, CryptoError> {
375 let key = Zeroizing::new(key.try_into().map_err(|_| CryptoError::InvalidKeyLen)?);
376 ctx.derive_shareable_key(key, "send", Some("send"))
377 }
378}
379
380impl IdentifyKey<SymmetricKeySlotId> for Send {
381 fn key_identifier(&self) -> SymmetricKeySlotId {
382 SymmetricKeySlotId::User
383 }
384}
385
386impl IdentifyKey<SymmetricKeySlotId> for SendView {
387 fn key_identifier(&self) -> SymmetricKeySlotId {
388 SymmetricKeySlotId::User
389 }
390}
391
392impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendTextView> for SendText {
393 fn decrypt(
394 &self,
395 ctx: &mut KeyStoreContext<KeySlotIds>,
396 key: SymmetricKeySlotId,
397 ) -> Result<SendTextView, CryptoError> {
398 Ok(SendTextView {
399 text: self.text.decrypt(ctx, key)?,
400 hidden: self.hidden,
401 })
402 }
403}
404
405impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendText> for SendTextView {
406 fn encrypt_composite(
407 &self,
408 ctx: &mut KeyStoreContext<KeySlotIds>,
409 key: SymmetricKeySlotId,
410 ) -> Result<SendText, CryptoError> {
411 Ok(SendText {
412 text: self.text.encrypt(ctx, key)?,
413 hidden: self.hidden,
414 })
415 }
416}
417
418impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendFileView> for SendFile {
419 fn decrypt(
420 &self,
421 ctx: &mut KeyStoreContext<KeySlotIds>,
422 key: SymmetricKeySlotId,
423 ) -> Result<SendFileView, CryptoError> {
424 Ok(SendFileView {
425 id: self.id.clone(),
426 file_name: self.file_name.decrypt(ctx, key)?,
427 size: self.size.clone(),
428 size_name: self.size_name.clone(),
429 })
430 }
431}
432
433impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, SendFile> for SendFileView {
434 fn encrypt_composite(
435 &self,
436 ctx: &mut KeyStoreContext<KeySlotIds>,
437 key: SymmetricKeySlotId,
438 ) -> Result<SendFile, CryptoError> {
439 Ok(SendFile {
440 id: self.id.clone(),
441 file_name: self.file_name.encrypt(ctx, key)?,
442 size: self.size.clone(),
443 size_name: self.size_name.clone(),
444 })
445 }
446}
447
448impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendView> for Send {
449 fn decrypt(
450 &self,
451 ctx: &mut KeyStoreContext<KeySlotIds>,
452 key: SymmetricKeySlotId,
453 ) -> Result<SendView, CryptoError> {
454 let k: Vec<u8> = self.key.decrypt(ctx, key)?;
458 let key = Send::derive_shareable_key(ctx, &k)?;
459
460 Ok(SendView {
461 id: self.id,
462 access_id: self.access_id.clone(),
463
464 name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
465 notes: self.notes.decrypt(ctx, key).ok().flatten(),
466 key: Some(B64Url::from(k).to_string()),
467 new_password: None,
468 has_password: self.password.is_some(),
469
470 r#type: self.r#type,
471 file: self.file.decrypt(ctx, key).ok().flatten(),
472 text: self.text.decrypt(ctx, key).ok().flatten(),
473
474 max_access_count: self.max_access_count,
475 access_count: self.access_count,
476 disabled: self.disabled,
477 hide_email: self.hide_email,
478
479 revision_date: self.revision_date,
480 deletion_date: self.deletion_date,
481 expiration_date: self.expiration_date,
482
483 emails: self
484 .emails
485 .as_deref()
486 .unwrap_or_default()
487 .split(',')
488 .map(|e| e.trim())
489 .filter(|e| !e.is_empty())
490 .map(String::from)
491 .collect(),
492 auth_type: self.auth_type,
493 })
494 }
495}
496
497impl Decryptable<KeySlotIds, SymmetricKeySlotId, SendListView> for Send {
498 fn decrypt(
499 &self,
500 ctx: &mut KeyStoreContext<KeySlotIds>,
501 key: SymmetricKeySlotId,
502 ) -> Result<SendListView, CryptoError> {
503 let key = Send::get_key(ctx, &self.key, key)?;
507
508 Ok(SendListView {
509 id: self.id,
510 access_id: self.access_id.clone(),
511
512 name: self.name.decrypt(ctx, key)?,
513 r#type: self.r#type,
514
515 disabled: self.disabled,
516
517 revision_date: self.revision_date,
518 deletion_date: self.deletion_date,
519 expiration_date: self.expiration_date,
520
521 auth_type: self.auth_type,
522 })
523 }
524}
525
526impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Send> for SendView {
527 fn encrypt_composite(
528 &self,
529 ctx: &mut KeyStoreContext<KeySlotIds>,
530 key: SymmetricKeySlotId,
531 ) -> Result<Send, CryptoError> {
532 let k = match (&self.key, &self.id) {
536 (Some(k), _) => B64Url::try_from(k.as_str())
538 .map_err(|_| CryptoError::InvalidKey)?
539 .as_bytes()
540 .to_vec(),
541 (None, None) => {
543 let key = generate_random_bytes::<[u8; 16]>();
544 key.to_vec()
545 }
546 _ => return Err(CryptoError::InvalidKey),
548 };
549 let send_key = Send::derive_shareable_key(ctx, &k)?;
550
551 Ok(Send {
552 id: self.id,
553 access_id: self.access_id.clone(),
554
555 name: self.name.encrypt(ctx, send_key)?,
556 notes: self.notes.encrypt(ctx, send_key)?,
557 key: OctetStreamBytes::from(k.clone()).encrypt(ctx, key)?,
558 password: self.new_password.as_ref().map(|password| {
559 let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS);
560 B64::from(password.as_slice()).to_string()
561 }),
562
563 r#type: self.r#type,
564 file: self.file.encrypt_composite(ctx, send_key)?,
565 text: self.text.encrypt_composite(ctx, send_key)?,
566
567 max_access_count: self.max_access_count,
568 access_count: self.access_count,
569 disabled: self.disabled,
570 hide_email: self.hide_email,
571
572 revision_date: self.revision_date,
573 deletion_date: self.deletion_date,
574 expiration_date: self.expiration_date,
575
576 emails: (!self.emails.is_empty()).then(|| self.emails.join(",")),
577 auth_type: self.auth_type,
578 })
579 }
580}
581
582impl TryFrom<SendResponseModel> for Send {
583 type Error = SendParseError;
584
585 fn try_from(send: SendResponseModel) -> Result<Self, Self::Error> {
586 let auth_type = match send.auth_type {
587 Some(t) => t.try_into()?,
588 None => {
589 if send.password.is_some() {
590 AuthType::Password
591 } else if send.emails.is_some() {
592 AuthType::Email
593 } else {
594 AuthType::None
595 }
596 }
597 };
598 Ok(Send {
599 id: send.id.map(SendId::new),
600 access_id: send.access_id,
601 name: require!(send.name).parse()?,
602 notes: EncString::try_from_optional(send.notes)?,
603 key: require!(send.key).parse()?,
604 password: send.password,
605 r#type: require!(send.r#type).try_into()?,
606 file: send.file.map(|f| (*f).try_into()).transpose()?,
607 text: send.text.map(|t| (*t).try_into()).transpose()?,
608 max_access_count: send.max_access_count.map(|s| s as u32),
609 access_count: require!(send.access_count) as u32,
610 disabled: send.disabled.unwrap_or(false),
611 hide_email: send.hide_email.unwrap_or(false),
612 revision_date: require!(send.revision_date).parse()?,
613 deletion_date: require!(send.deletion_date).parse()?,
614 expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?,
615 emails: send.emails,
616 auth_type,
617 })
618 }
619}
620
621impl TryFrom<bitwarden_api_api::models::SendType> for SendType {
622 type Error = bitwarden_core::MissingFieldError;
623
624 fn try_from(t: bitwarden_api_api::models::SendType) -> Result<Self, Self::Error> {
625 Ok(match t {
626 bitwarden_api_api::models::SendType::Text => SendType::Text,
627 bitwarden_api_api::models::SendType::File => SendType::File,
628 bitwarden_api_api::models::SendType::__Unknown(_) => {
629 return Err(bitwarden_core::MissingFieldError("type"));
630 }
631 })
632 }
633}
634
635impl From<SendType> for bitwarden_api_api::models::SendType {
636 fn from(t: SendType) -> Self {
637 match t {
638 SendType::Text => bitwarden_api_api::models::SendType::Text,
639 SendType::File => bitwarden_api_api::models::SendType::File,
640 }
641 }
642}
643
644impl TryFrom<bitwarden_api_api::models::AuthType> for AuthType {
645 type Error = bitwarden_core::MissingFieldError;
646
647 fn try_from(value: bitwarden_api_api::models::AuthType) -> Result<Self, Self::Error> {
648 Ok(match value {
649 bitwarden_api_api::models::AuthType::Email => AuthType::Email,
650 bitwarden_api_api::models::AuthType::Password => AuthType::Password,
651 bitwarden_api_api::models::AuthType::None => AuthType::None,
652 bitwarden_api_api::models::AuthType::__Unknown(_) => {
653 return Err(bitwarden_core::MissingFieldError("auth_type"));
654 }
655 })
656 }
657}
658
659impl From<AuthType> for bitwarden_api_api::models::AuthType {
660 fn from(value: AuthType) -> Self {
661 match value {
662 AuthType::Email => bitwarden_api_api::models::AuthType::Email,
663 AuthType::Password => bitwarden_api_api::models::AuthType::Password,
664 AuthType::None => bitwarden_api_api::models::AuthType::None,
665 }
666 }
667}
668
669impl From<SendFile> for SendFileModel {
670 fn from(file: SendFile) -> Self {
671 SendFileModel {
672 id: file.id,
673 file_name: Some(file.file_name.to_string()),
674 size: file.size,
675 size_name: file.size_name,
676 }
677 }
678}
679
680impl From<SendText> for SendTextModel {
681 fn from(text: SendText) -> Self {
682 SendTextModel {
683 text: text.text.map(|text| text.to_string()),
684 hidden: Some(text.hidden),
685 }
686 }
687}
688
689impl TryFrom<SendFileModel> for SendFile {
690 type Error = SendParseError;
691
692 fn try_from(file: SendFileModel) -> Result<Self, Self::Error> {
693 Ok(SendFile {
694 id: file.id,
695 file_name: require!(file.file_name).parse()?,
696 size: file.size.map(|v| v.to_string()),
697 size_name: file.size_name,
698 })
699 }
700}
701
702impl TryFrom<SendTextModel> for SendText {
703 type Error = SendParseError;
704
705 fn try_from(text: SendTextModel) -> Result<Self, Self::Error> {
706 Ok(SendText {
707 text: EncString::try_from_optional(text.text)?,
708 hidden: text.hidden.unwrap_or(false),
709 })
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use bitwarden_core::key_management::create_test_crypto_with_user_key;
716 use bitwarden_crypto::SymmetricCryptoKey;
717
718 use super::*;
719
720 #[test]
721 fn test_get_send_key() {
722 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
724 let crypto = create_test_crypto_with_user_key(user_key);
725 let mut ctx = crypto.context();
726
727 let send_key = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g="
728 .parse()
729 .unwrap();
730
731 let send_key = Send::get_key(&mut ctx, &send_key, SymmetricKeySlotId::User).unwrap();
733 #[allow(deprecated)]
734 let send_key = ctx.dangerous_get_symmetric_key(send_key).unwrap();
735 let send_key_b64 = send_key.to_base64();
736 assert_eq!(
737 send_key_b64.to_string(),
738 "IR9ImHGm6rRuIjiN7csj94bcZR5WYTJj5GtNfx33zm6tJCHUl+QZlpNPba8g2yn70KnOHsAODLcR0um6E3MAlg=="
739 );
740 }
741
742 #[test]
743 pub fn test_decrypt() {
744 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
745 let crypto = create_test_crypto_with_user_key(user_key);
746
747 let send = Send {
748 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
749 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
750 r#type: SendType::Text,
751 name: "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=".parse()
752 .unwrap(),
753 notes: None,
754 file: None,
755 text: Some(SendText {
756 text: "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=".parse().ok(),
757 hidden: false,
758 }),
759 key: "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=".parse().unwrap(),
760 max_access_count: None,
761 access_count: 0,
762 password: None,
763 disabled: false,
764 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
765 expiration_date: None,
766 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
767 hide_email: false,
768 emails: None,
769 auth_type: AuthType::None,
770 };
771
772 let view: SendView = crypto.decrypt(&send).unwrap();
773
774 let expected = SendView {
775 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
776 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
777 name: "Test".to_string(),
778 notes: None,
779 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
780 new_password: None,
781 has_password: false,
782 r#type: SendType::Text,
783 file: None,
784 text: Some(SendTextView {
785 text: Some("This is a test".to_owned()),
786 hidden: false,
787 }),
788 max_access_count: None,
789 access_count: 0,
790 disabled: false,
791 hide_email: false,
792 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
793 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
794 expiration_date: None,
795 emails: Vec::new(),
796 auth_type: AuthType::None,
797 };
798
799 assert_eq!(view, expected);
800 }
801
802 #[test]
803 pub fn test_encrypt() {
804 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
805 let crypto = create_test_crypto_with_user_key(user_key);
806
807 let view = SendView {
808 id: "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().ok(),
809 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
810 name: "Test".to_string(),
811 notes: None,
812 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
813 new_password: None,
814 has_password: false,
815 r#type: SendType::Text,
816 file: None,
817 text: Some(SendTextView {
818 text: Some("This is a test".to_owned()),
819 hidden: false,
820 }),
821 max_access_count: None,
822 access_count: 0,
823 disabled: false,
824 hide_email: false,
825 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
826 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
827 expiration_date: None,
828 emails: Vec::new(),
829 auth_type: AuthType::None,
830 };
831
832 let v: SendView = crypto
834 .decrypt(&crypto.encrypt(view.clone()).unwrap())
835 .unwrap();
836 assert_eq!(v, view);
837 }
838
839 #[test]
840 pub fn test_create() {
841 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
842 let crypto = create_test_crypto_with_user_key(user_key);
843
844 let view = SendView {
845 id: None,
846 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
847 name: "Test".to_string(),
848 notes: None,
849 key: None,
850 new_password: None,
851 has_password: false,
852 r#type: SendType::Text,
853 file: None,
854 text: Some(SendTextView {
855 text: Some("This is a test".to_owned()),
856 hidden: false,
857 }),
858 max_access_count: None,
859 access_count: 0,
860 disabled: false,
861 hide_email: false,
862 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
863 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
864 expiration_date: None,
865 emails: Vec::new(),
866 auth_type: AuthType::None,
867 };
868
869 let v: SendView = crypto
871 .decrypt(&crypto.encrypt(view.clone()).unwrap())
872 .unwrap();
873
874 let t = SendView { key: None, ..v };
876 assert_eq!(t, view);
877 }
878
879 #[test]
880 pub fn test_create_password() {
881 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
882 let crypto = create_test_crypto_with_user_key(user_key);
883
884 let view = SendView {
885 id: None,
886 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
887 name: "Test".to_owned(),
888 notes: None,
889 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
890 new_password: Some("abc123".to_owned()),
891 has_password: false,
892 r#type: SendType::Text,
893 file: None,
894 text: Some(SendTextView {
895 text: Some("This is a test".to_owned()),
896 hidden: false,
897 }),
898 max_access_count: None,
899 access_count: 0,
900 disabled: false,
901 hide_email: false,
902 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
903 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
904 expiration_date: None,
905 emails: Vec::new(),
906 auth_type: AuthType::Password,
907 };
908
909 let send: Send = crypto.encrypt(view).unwrap();
910
911 assert_eq!(
912 send.password,
913 Some("vTIDfdj3FTDbejmMf+mJWpYdMXsxfeSd1Sma3sjCtiQ=".to_owned())
914 );
915 assert_eq!(send.auth_type, AuthType::Password);
916
917 let v: SendView = crypto.decrypt(&send).unwrap();
918 assert_eq!(v.new_password, None);
919 assert!(v.has_password);
920 assert_eq!(v.auth_type, AuthType::Password);
921 }
922
923 #[test]
924 pub fn test_create_email_otp() {
925 let user_key: SymmetricCryptoKey = "bYCsk857hl8QJJtxyRK65tjUrbxKC4aDifJpsml+NIv4W9cVgFvi3qVD+yJTUU2T4UwNKWYtt9pqWf7Q+2WCCg==".to_string().try_into().unwrap();
926 let crypto = create_test_crypto_with_user_key(user_key);
927
928 let view = SendView {
929 id: None,
930 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_owned()),
931 name: "Test".to_owned(),
932 notes: None,
933 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
934 new_password: None,
935 has_password: false,
936 r#type: SendType::Text,
937 file: None,
938 text: Some(SendTextView {
939 text: Some("This is a test".to_owned()),
940 hidden: false,
941 }),
942 max_access_count: None,
943 access_count: 0,
944 disabled: false,
945 hide_email: false,
946 revision_date: "2024-01-07T23:56:48.207363Z".parse().unwrap(),
947 deletion_date: "2024-01-14T23:56:48Z".parse().unwrap(),
948 expiration_date: None,
949 emails: vec![
950 String::from("[email protected]"),
951 String::from("[email protected]"),
952 ],
953 auth_type: AuthType::Email,
954 };
955
956 let send: Send = crypto.encrypt(view.clone()).unwrap();
957
958 let v: SendView = crypto.decrypt(&send).unwrap();
960
961 assert_eq!(v, view);
962 }
963
964 #[test]
965 fn test_send_into_send_with_id_request_model() {
966 let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap();
967 let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z")
968 .unwrap()
969 .with_timezone(&Utc);
970 let deletion_date = DateTime::parse_from_rfc3339("2024-01-14T23:56:48Z")
971 .unwrap()
972 .with_timezone(&Utc);
973 let expiration_date = DateTime::parse_from_rfc3339("2024-01-20T23:56:48Z")
974 .unwrap()
975 .with_timezone(&Utc);
976
977 let name = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
978 let notes = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
979 let key = "2.KLv/j0V4Ebs0dwyPdtt4vw==|jcrFuNYN1Qb3onBlwvtxUV/KpdnR1LPRL4EsCoXNAt4=|gHSywGy4Rj/RsCIZFwze4s2AACYKBtqDXTrQXjkgtIE=";
980 let file_name = "2.+1KUfOX8A83Xkwk1bumo/w==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
981 let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0=";
982
983 let send = Send {
984 id: Some(SendId::new(send_id)),
985 access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()),
986 name: name.parse().unwrap(),
987 notes: Some(notes.parse().unwrap()),
988 key: key.parse().unwrap(),
989 password: Some("hash".to_string()),
990 r#type: SendType::File,
991 file: Some(SendFile {
992 id: Some("file-id".to_string()),
993 file_name: file_name.parse().unwrap(),
994 size: Some("1234".to_string()),
995 size_name: Some("1.2 KB".to_string()),
996 }),
997 text: Some(SendText {
998 text: Some(text_value.parse().unwrap()),
999 hidden: true,
1000 }),
1001 max_access_count: Some(42),
1002 access_count: 0,
1003 disabled: true,
1004 hide_email: true,
1005 revision_date,
1006 deletion_date,
1007 expiration_date: Some(expiration_date),
1008 emails: Some("[email protected],[email protected]".to_string()),
1009 auth_type: AuthType::Email,
1010 };
1011
1012 let model: SendWithIdRequestModel = send.into();
1013
1014 assert_eq!(model.id, send_id);
1015 assert_eq!(
1016 model.r#type,
1017 Some(bitwarden_api_api::models::SendType::File)
1018 );
1019 assert_eq!(
1020 model.auth_type,
1021 Some(bitwarden_api_api::models::AuthType::Email)
1022 );
1023 assert_eq!(model.file_length, Some(1234));
1024 assert_eq!(model.name.as_deref(), Some(name));
1025 assert_eq!(model.notes.as_deref(), Some(notes));
1026 assert_eq!(model.key, key);
1027 assert_eq!(model.max_access_count, Some(42));
1028 assert_eq!(
1029 model
1030 .expiration_date
1031 .unwrap()
1032 .parse::<DateTime<Utc>>()
1033 .unwrap(),
1034 expiration_date
1035 );
1036 assert_eq!(
1037 model.deletion_date.parse::<DateTime<Utc>>().unwrap(),
1038 deletion_date
1039 );
1040 assert_eq!(model.password.as_deref(), Some("hash"));
1041 assert_eq!(
1042 model.emails.as_deref(),
1043 Some("[email protected],[email protected]")
1044 );
1045 assert!(model.disabled);
1046 assert_eq!(model.hide_email, Some(true));
1047
1048 let file = model.file.unwrap();
1049 assert_eq!(file.id.as_deref(), Some("file-id"));
1050 assert_eq!(file.file_name.as_deref(), Some(file_name));
1051 assert_eq!(file.size.as_deref(), Some("1234"));
1052 assert_eq!(file.size_name.as_deref(), Some("1.2 KB"));
1053
1054 let text = model.text.unwrap();
1055 assert_eq!(text.text.as_deref(), Some(text_value));
1056 assert_eq!(text.hidden, Some(true));
1057 }
1058}