1use bitwarden_api_api::models::{
2 CipherDetailsResponseModel, CipherMiniDetailsResponseModel, CipherMiniResponseModel,
3 CipherRequestModel, CipherResponseModel, CipherWithIdRequestModel,
4};
5use bitwarden_collections::collection::CollectionId;
6use bitwarden_core::{
7 ApiError, MissingFieldError, OrganizationId, UserId,
8 key_management::{KeySlotIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeySlotId},
9 require,
10};
11use bitwarden_crypto::{
12 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
13 PrimitiveEncryptable, SymmetricCryptoKey, SymmetricKeyAlgorithm,
14};
15use bitwarden_error::bitwarden_error;
16use bitwarden_state::repository::RepositoryError;
17use bitwarden_uuid::uuid_newtype;
18use chrono::{DateTime, SecondsFormat, Utc};
19use serde::{Deserialize, Serialize};
20use serde_repr::{Deserialize_repr, Serialize_repr};
21use thiserror::Error;
22#[cfg(feature = "wasm")]
23use tsify::Tsify;
24#[cfg(feature = "wasm")]
25use wasm_bindgen::prelude::wasm_bindgen;
26
27use super::{
28 attachment, bank_account,
29 blob::{BlobEncryptionError, decrypt_blob_cipher, is_blob_encrypted},
30 card,
31 card::CardListView,
32 cipher_permissions::CipherPermissions,
33 drivers_license, field, identity,
34 local_data::{LocalData, LocalDataView},
35 login::LoginListView,
36 passport, secure_note, ssh_key,
37};
38use crate::{
39 AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView,
40 FieldView, FolderId, Login, LoginView, VaultParseError,
41 password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
42};
43
44uuid_newtype!(pub CipherId);
45
46#[allow(missing_docs)]
47#[bitwarden_error(flat)]
48#[derive(Debug, Error)]
49pub enum CipherError {
50 #[error(transparent)]
51 MissingField(#[from] MissingFieldError),
52 #[error(transparent)]
53 Crypto(#[from] CryptoError),
54 #[error(transparent)]
55 Decrypt(#[from] DecryptError),
56 #[error(transparent)]
57 Encrypt(#[from] EncryptError),
58 #[error(
59 "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
60 )]
61 AttachmentsWithoutKeys,
62 #[error("This cipher cannot be moved to the specified organization")]
63 OrganizationAlreadySet,
64 #[error(transparent)]
65 Repository(#[from] RepositoryError),
66 #[error(transparent)]
67 Chrono(#[from] chrono::ParseError),
68 #[error(transparent)]
69 SerdeJson(#[from] serde_json::Error),
70 #[error(transparent)]
71 Api(#[from] ApiError),
72}
73
74impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherError {
75 fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
76 Self::Api(value.into())
77 }
78}
79
80pub(super) trait CipherKind {
82 fn decrypt_subtitle(
84 &self,
85 ctx: &mut KeyStoreContext<KeySlotIds>,
86 key: SymmetricKeySlotId,
87 ) -> Result<String, CryptoError>;
88
89 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
91}
92
93#[allow(missing_docs)]
94#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
95#[repr(u8)]
96#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
97#[cfg_attr(feature = "wasm", wasm_bindgen)]
98pub enum CipherType {
99 Login = 1,
100 SecureNote = 2,
101 Card = 3,
102 Identity = 4,
103 SshKey = 5,
104 BankAccount = 6,
105 DriversLicense = 7,
106 Passport = 8,
107}
108
109#[allow(missing_docs)]
110#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
111#[repr(u8)]
112#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
113#[cfg_attr(feature = "wasm", wasm_bindgen)]
114pub enum CipherRepromptType {
115 #[default]
116 None = 0,
117 Password = 1,
118}
119
120#[allow(missing_docs)]
121#[derive(Serialize, Deserialize, Debug, Clone)]
122#[serde(rename_all = "camelCase", deny_unknown_fields)]
123#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
124#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
125pub struct EncryptionContext {
126 pub encrypted_for: UserId,
129 pub cipher: Cipher,
130}
131
132impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
133 type Error = CipherError;
134 fn try_from(
135 EncryptionContext {
136 cipher,
137 encrypted_for,
138 }: EncryptionContext,
139 ) -> Result<Self, Self::Error> {
140 Ok(Self {
141 id: require!(cipher.id).into(),
142 encrypted_for: Some(encrypted_for.into()),
143 r#type: Some(cipher.r#type.into()),
144 organization_id: cipher.organization_id.map(|o| o.to_string()),
145 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
146 favorite: cipher.favorite.into(),
147 reprompt: Some(cipher.reprompt.into()),
148 key: cipher.key.map(|k| k.to_string()),
149 name: cipher.name.to_string(),
150 notes: cipher.notes.map(|n| n.to_string()),
151 fields: Some(
152 cipher
153 .fields
154 .into_iter()
155 .flatten()
156 .map(Into::into)
157 .collect(),
158 ),
159 password_history: Some(
160 cipher
161 .password_history
162 .into_iter()
163 .flatten()
164 .map(Into::into)
165 .collect(),
166 ),
167 attachments: None,
168 attachments2: Some(
169 cipher
170 .attachments
171 .into_iter()
172 .flatten()
173 .filter_map(|a| {
174 a.id.map(|id| {
175 (
176 id,
177 bitwarden_api_api::models::CipherAttachmentModel {
178 file_name: a.file_name.map(|n| n.to_string()),
179 key: a.key.map(|k| k.to_string()),
180 },
181 )
182 })
183 })
184 .collect(),
185 ),
186 login: cipher.login.map(|l| Box::new(l.into())),
187 card: cipher.card.map(|c| Box::new(c.into())),
188 identity: cipher.identity.map(|i| Box::new(i.into())),
189 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
190 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
191 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
192 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
193 passport: cipher.passport.map(|p| Box::new(p.into())),
194 data: None, last_known_revision_date: Some(
196 cipher
197 .revision_date
198 .to_rfc3339_opts(SecondsFormat::Millis, true),
199 ),
200 archived_date: cipher
201 .archived_date
202 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
203 })
204 }
205}
206
207impl From<EncryptionContext> for CipherRequestModel {
208 fn from(
209 EncryptionContext {
210 cipher,
211 encrypted_for,
212 }: EncryptionContext,
213 ) -> Self {
214 Self {
215 encrypted_for: Some(encrypted_for.into()),
216 r#type: Some(cipher.r#type.into()),
217 organization_id: cipher.organization_id.map(|o| o.to_string()),
218 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
219 favorite: cipher.favorite.into(),
220 reprompt: Some(cipher.reprompt.into()),
221 key: cipher.key.map(|k| k.to_string()),
222 name: cipher.name.to_string(),
223 notes: cipher.notes.map(|n| n.to_string()),
224 fields: Some(
225 cipher
226 .fields
227 .into_iter()
228 .flatten()
229 .map(Into::into)
230 .collect(),
231 ),
232 password_history: Some(
233 cipher
234 .password_history
235 .into_iter()
236 .flatten()
237 .map(Into::into)
238 .collect(),
239 ),
240 attachments: None,
241 attachments2: Some(
242 cipher
243 .attachments
244 .into_iter()
245 .flatten()
246 .filter_map(|a| {
247 a.id.map(|id| {
248 (
249 id,
250 bitwarden_api_api::models::CipherAttachmentModel {
251 file_name: a.file_name.map(|n| n.to_string()),
252 key: a.key.map(|k| k.to_string()),
253 },
254 )
255 })
256 })
257 .collect(),
258 ),
259 login: cipher.login.map(|l| Box::new(l.into())),
260 card: cipher.card.map(|c| Box::new(c.into())),
261 identity: cipher.identity.map(|i| Box::new(i.into())),
262 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
263 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
264 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
265 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
266 passport: cipher.passport.map(|p| Box::new(p.into())),
267 data: None, last_known_revision_date: Some(
269 cipher
270 .revision_date
271 .to_rfc3339_opts(SecondsFormat::Millis, true),
272 ),
273 archived_date: cipher
274 .archived_date
275 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
276 }
277 }
278}
279
280#[allow(missing_docs)]
281#[derive(Serialize, Deserialize, Debug, Clone)]
282#[serde(rename_all = "camelCase", deny_unknown_fields)]
283#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
284#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
285pub struct Cipher {
286 pub id: Option<CipherId>,
287 pub organization_id: Option<OrganizationId>,
288 pub folder_id: Option<FolderId>,
289 pub collection_ids: Vec<CollectionId>,
290 pub key: Option<EncString>,
293
294 pub name: EncString,
295 pub notes: Option<EncString>,
296
297 pub r#type: CipherType,
298 pub login: Option<Login>,
299 pub identity: Option<identity::Identity>,
300 pub card: Option<card::Card>,
301 pub secure_note: Option<secure_note::SecureNote>,
302 pub ssh_key: Option<ssh_key::SshKey>,
303 pub bank_account: Option<bank_account::BankAccount>,
304 pub drivers_license: Option<drivers_license::DriversLicense>,
305 pub passport: Option<passport::Passport>,
306
307 pub favorite: bool,
308 pub reprompt: CipherRepromptType,
309 pub organization_use_totp: bool,
310 pub edit: bool,
311 pub permissions: Option<CipherPermissions>,
312 pub view_password: bool,
313 pub local_data: Option<LocalData>,
314
315 pub attachments: Option<Vec<attachment::Attachment>>,
316 pub fields: Option<Vec<field::Field>>,
317 pub password_history: Option<Vec<password_history::PasswordHistory>>,
318
319 pub creation_date: DateTime<Utc>,
320 pub deleted_date: Option<DateTime<Utc>>,
321 pub revision_date: DateTime<Utc>,
322 pub archived_date: Option<DateTime<Utc>>,
323 pub data: Option<String>,
324}
325
326pub enum CipherKeyRewrapError {
329 NoCipherKey,
330 DecryptionFailure,
331 EncryptionFailure,
332}
333
334impl Cipher {
335 pub fn rewrap_cipher_key(
340 &mut self,
341 old_key: SymmetricKeySlotId,
342 new_key: SymmetricKeySlotId,
343 ctx: &mut KeyStoreContext<KeySlotIds>,
344 ) -> Result<(), CipherKeyRewrapError> {
345 let new_cipher_key = self
346 .key
347 .as_ref()
348 .ok_or(CipherKeyRewrapError::NoCipherKey)
349 .and_then(|wrapped_cipher_key| {
350 ctx.unwrap_symmetric_key(old_key, wrapped_cipher_key)
351 .map_err(|_| CipherKeyRewrapError::DecryptionFailure)
352 })
353 .and_then(|cipher_key| {
354 ctx.wrap_symmetric_key(new_key, cipher_key)
355 .map_err(|_| CipherKeyRewrapError::EncryptionFailure)
356 })?;
357 self.key = Some(new_cipher_key);
358 Ok(())
359 }
360}
361
362bitwarden_state::register_repository_item!(CipherId => Cipher, "Cipher");
363
364impl TryFrom<Cipher> for CipherRequestModel {
365 type Error = CryptoError;
366
367 fn try_from(c: Cipher) -> Result<Self, Self::Error> {
374 let attachments2 = c
375 .attachments
376 .map(|list| {
377 list.into_iter()
378 .map(|a| {
379 let id = a.id.clone().ok_or(CryptoError::MissingField("id"))?;
380 Ok::<_, CryptoError>((id, a.into()))
381 })
382 .collect::<Result<_, _>>()
383 })
384 .transpose()?;
385
386 Ok(CipherRequestModel {
387 encrypted_for: None,
388 r#type: Some(c.r#type.into()),
389 organization_id: c.organization_id.map(|id| id.to_string()),
390 folder_id: c.folder_id.map(|id| id.to_string()),
391 favorite: Some(c.favorite),
392 reprompt: Some(c.reprompt.into()),
393 key: c.key.map(|k| k.to_string()),
394 name: c.name.to_string(),
395 notes: c.notes.map(|n| n.to_string()),
396 login: c.login.map(|v| Box::new(v.into())),
397 card: c.card.map(|v| Box::new(v.into())),
398 identity: c.identity.map(|v| Box::new(v.into())),
399 secure_note: c.secure_note.map(|v| Box::new(v.into())),
400 ssh_key: c.ssh_key.map(|v| Box::new(v.into())),
401 bank_account: c.bank_account.map(|v| Box::new(v.into())),
402 drivers_license: c.drivers_license.map(|v| Box::new(v.into())),
403 passport: c.passport.map(|v| Box::new(v.into())),
404 fields: c.fields.map(|f| f.into_iter().map(Into::into).collect()),
405 password_history: c
406 .password_history
407 .map(|h| h.into_iter().map(Into::into).collect()),
408 attachments: None,
409 attachments2,
410 last_known_revision_date: Some(
411 c.revision_date.to_rfc3339_opts(SecondsFormat::Secs, true),
412 ),
413 archived_date: c.archived_date.map(|d| d.to_rfc3339()),
414 data: c.data,
415 })
416 }
417}
418
419#[allow(missing_docs)]
420#[derive(Serialize, Deserialize, Debug, Clone)]
421#[serde(rename_all = "camelCase", deny_unknown_fields)]
422#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
423#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
424pub struct CipherView {
425 pub id: Option<CipherId>,
426 pub organization_id: Option<OrganizationId>,
427 pub folder_id: Option<FolderId>,
428 pub collection_ids: Vec<CollectionId>,
429
430 pub key: Option<EncString>,
432
433 pub name: String,
434 pub notes: Option<String>,
435
436 pub r#type: CipherType,
437 pub login: Option<LoginView>,
438 pub identity: Option<identity::IdentityView>,
439 pub card: Option<card::CardView>,
440 pub secure_note: Option<secure_note::SecureNoteView>,
441 pub ssh_key: Option<ssh_key::SshKeyView>,
442 pub bank_account: Option<bank_account::BankAccountView>,
443 pub drivers_license: Option<drivers_license::DriversLicenseView>,
444 pub passport: Option<passport::PassportView>,
445
446 pub favorite: bool,
447 pub reprompt: CipherRepromptType,
448 pub organization_use_totp: bool,
449 pub edit: bool,
450 pub permissions: Option<CipherPermissions>,
451 pub view_password: bool,
452 pub local_data: Option<LocalDataView>,
453
454 pub attachments: Option<Vec<attachment::AttachmentView>>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
458 pub fields: Option<Vec<field::FieldView>>,
459 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
460 pub creation_date: DateTime<Utc>,
461 pub deleted_date: Option<DateTime<Utc>>,
462 pub revision_date: DateTime<Utc>,
463 pub archived_date: Option<DateTime<Utc>>,
464}
465
466#[allow(missing_docs)]
467#[derive(Serialize, Deserialize, Debug, PartialEq)]
468#[serde(rename_all = "camelCase", deny_unknown_fields)]
469#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
470#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
471pub enum CipherListViewType {
472 Login(LoginListView),
473 SecureNote,
474 Card(CardListView),
475 Identity,
476 SshKey,
477 BankAccount,
478 Passport,
479 DriversLicense,
480}
481
482#[derive(Serialize, Deserialize, Debug, PartialEq)]
484#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
485#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
486pub enum CopyableCipherFields {
487 LoginUsername,
488 LoginPassword,
489 LoginTotp,
490 CardNumber,
491 CardSecurityCode,
492 IdentityUsername,
493 IdentityEmail,
494 IdentityPhone,
495 IdentityAddress,
496 SshKey,
497 SecureNotes,
498 BankAccountNameOnAccount,
499 BankAccountAccountNumber,
500 BankAccountRoutingNumber,
501 BankAccountBranchNumber,
502 BankAccountPin,
503 BankAccountIban,
504 BankAccountSwift,
505 PassportGivenName,
506 PassportSurname,
507 PassportPassportNumber,
508 PassportNationalIdentificationNumber,
509 DriversLicenseFirstName,
510 DriversLicenseMiddleName,
511 DriversLicenseLastName,
512 DriversLicenseLicenseNumber,
513}
514
515#[allow(missing_docs)]
516#[derive(Serialize, Deserialize, Debug, PartialEq)]
517#[serde(rename_all = "camelCase", deny_unknown_fields)]
518#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
519#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
520pub struct CipherListView {
521 pub id: Option<CipherId>,
522 pub organization_id: Option<OrganizationId>,
523 pub folder_id: Option<FolderId>,
524 pub collection_ids: Vec<CollectionId>,
525
526 pub key: Option<EncString>,
528
529 pub name: String,
530 pub subtitle: String,
531
532 pub r#type: CipherListViewType,
533
534 pub favorite: bool,
535 pub reprompt: CipherRepromptType,
536 pub organization_use_totp: bool,
537 pub edit: bool,
538 pub permissions: Option<CipherPermissions>,
539
540 pub view_password: bool,
541
542 pub attachments: u32,
544 pub has_old_attachments: bool,
546
547 pub creation_date: DateTime<Utc>,
548 pub deleted_date: Option<DateTime<Utc>>,
549 pub revision_date: DateTime<Utc>,
550 pub archived_date: Option<DateTime<Utc>>,
551
552 pub copyable_fields: Vec<CopyableCipherFields>,
554
555 pub local_data: Option<LocalDataView>,
556
557 #[cfg(feature = "wasm")]
559 pub notes: Option<String>,
560 #[cfg(feature = "wasm")]
563 pub fields: Option<Vec<field::FieldListView>>,
564 #[cfg(feature = "wasm")]
566 pub attachment_names: Option<Vec<String>>,
567}
568
569#[derive(Serialize, Deserialize, Debug)]
575#[serde(rename_all = "camelCase", deny_unknown_fields)]
576#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
577#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
578pub struct DecryptCipherListResult {
579 pub successes: Vec<CipherListView>,
581 pub failures: Vec<Cipher>,
583}
584
585#[derive(Serialize, Deserialize, Debug)]
591#[serde(rename_all = "camelCase", deny_unknown_fields)]
592#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
593#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
594pub struct DecryptCipherResult {
595 pub successes: Vec<CipherView>,
597 pub failures: Vec<Cipher>,
599}
600
601#[derive(Serialize, Deserialize, Debug)]
605#[serde(rename_all = "camelCase", deny_unknown_fields)]
606#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
607#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
608pub struct ListOrganizationCiphersResult {
609 pub ciphers: Vec<Cipher>,
611 pub list_views: Vec<CipherListView>,
613}
614
615impl CipherListView {
616 pub(crate) fn get_totp_key(
617 self,
618 ctx: &mut KeyStoreContext<KeySlotIds>,
619 ) -> Result<Option<String>, CryptoError> {
620 let key = self.key_identifier();
621 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
622
623 let totp = match self.r#type {
624 CipherListViewType::Login(LoginListView { totp, .. }) => {
625 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
626 }
627 _ => None,
628 };
629
630 Ok(totp)
631 }
632}
633
634impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Cipher> for CipherView {
635 fn encrypt_composite(
636 &self,
637 ctx: &mut KeyStoreContext<KeySlotIds>,
638 key: SymmetricKeySlotId,
639 ) -> Result<Cipher, CryptoError> {
640 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
641
642 let mut cipher_view = self.clone();
643 cipher_view.generate_checksums();
644
645 Ok(Cipher {
646 id: cipher_view.id,
647 organization_id: cipher_view.organization_id,
648 folder_id: cipher_view.folder_id,
649 collection_ids: cipher_view.collection_ids,
650 key: cipher_view.key,
651 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
652 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
653 r#type: cipher_view.r#type,
654 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
655 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
656 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
657 secure_note: cipher_view
658 .secure_note
659 .encrypt_composite(ctx, ciphers_key)?,
660 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
661 bank_account: cipher_view
662 .bank_account
663 .encrypt_composite(ctx, ciphers_key)?,
664 drivers_license: cipher_view
665 .drivers_license
666 .encrypt_composite(ctx, ciphers_key)?,
667 passport: cipher_view.passport.encrypt_composite(ctx, ciphers_key)?,
668 favorite: cipher_view.favorite,
669 reprompt: cipher_view.reprompt,
670 organization_use_totp: cipher_view.organization_use_totp,
671 edit: cipher_view.edit,
672 view_password: cipher_view.view_password,
673 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
674 attachments: cipher_view
675 .attachments
676 .encrypt_composite(ctx, ciphers_key)?,
677 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
678 password_history: cipher_view
679 .password_history
680 .encrypt_composite(ctx, ciphers_key)?,
681 creation_date: cipher_view.creation_date,
682 deleted_date: cipher_view.deleted_date,
683 revision_date: cipher_view.revision_date,
684 permissions: cipher_view.permissions,
685 archived_date: cipher_view.archived_date,
686 data: None, })
688 }
689}
690
691pub(crate) fn lenient_decrypt_cipher_view(
697 cipher: &Cipher,
698 ctx: &mut KeyStoreContext<KeySlotIds>,
699 key: SymmetricKeySlotId,
700) -> Result<CipherView, CryptoError> {
701 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
702
703 let (attachments, attachment_decryption_failures) =
705 attachment::decrypt_attachments_with_failures(
706 cipher.attachments.as_deref().unwrap_or_default(),
707 ctx,
708 ciphers_key,
709 );
710
711 let mut view = CipherView {
712 id: cipher.id,
713 organization_id: cipher.organization_id,
714 folder_id: cipher.folder_id,
715 collection_ids: cipher.collection_ids.clone(),
716 key: cipher.key.clone(),
717 name: cipher
718 .name
719 .decrypt(ctx, ciphers_key)
720 .ok()
721 .unwrap_or_default(),
722 notes: cipher.notes.decrypt(ctx, ciphers_key).ok().flatten(),
723 r#type: cipher.r#type,
724 login: cipher.login.decrypt(ctx, ciphers_key).ok().flatten(),
725 identity: cipher.identity.decrypt(ctx, ciphers_key).ok().flatten(),
726 card: cipher.card.decrypt(ctx, ciphers_key).ok().flatten(),
727 secure_note: cipher.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
728 ssh_key: cipher.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
729 bank_account: cipher.bank_account.decrypt(ctx, ciphers_key).ok().flatten(),
730 drivers_license: cipher
731 .drivers_license
732 .decrypt(ctx, ciphers_key)
733 .ok()
734 .flatten(),
735 passport: cipher.passport.decrypt(ctx, ciphers_key).ok().flatten(),
736 favorite: cipher.favorite,
737 reprompt: cipher.reprompt,
738 organization_use_totp: cipher.organization_use_totp,
739 edit: cipher.edit,
740 permissions: cipher.permissions,
741 view_password: cipher.view_password,
742 local_data: cipher.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
743 attachments: Some(attachments),
744 attachment_decryption_failures: Some(attachment_decryption_failures),
745 fields: cipher.fields.decrypt(ctx, ciphers_key).ok().flatten(),
746 password_history: cipher
747 .password_history
748 .decrypt(ctx, ciphers_key)
749 .ok()
750 .flatten(),
751 creation_date: cipher.creation_date,
752 deleted_date: cipher.deleted_date,
753 revision_date: cipher.revision_date,
754 archived_date: cipher.archived_date,
755 };
756
757 if view.key.is_some()
760 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
761 {
762 view.remove_invalid_checksums();
763 }
764
765 Ok(view)
766}
767
768impl Cipher {
769 #[bitwarden_logging::instrument(err)]
780 pub(crate) fn decrypt_cipher_key(
781 ctx: &mut KeyStoreContext<KeySlotIds>,
782 key: SymmetricKeySlotId,
783 ciphers_key: &Option<EncString>,
784 ) -> Result<SymmetricKeySlotId, CryptoError> {
785 match ciphers_key {
786 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
787 None => Ok(key),
788 }
789 }
790
791 #[bitwarden_logging::instrument(err)]
799 pub(crate) fn make_attachment_material(
800 &self,
801 ctx: &mut KeyStoreContext<KeySlotIds>,
802 file_name: &str,
803 ) -> Result<attachment::AttachmentMaterial, CryptoError> {
804 let cipher_key = Self::decrypt_cipher_key(ctx, self.key_identifier(), &self.key)?;
805 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
806 let slot = ctx.add_local_symmetric_key(key.clone());
807 let wrapped_key = ctx.wrap_symmetric_key(cipher_key, slot)?;
808 let encrypted_file_name = file_name.encrypt(ctx, cipher_key)?;
809 Ok(attachment::AttachmentMaterial {
810 key,
811 wrapped_key,
812 encrypted_file_name,
813 })
814 }
815
816 fn get_kind(&self) -> Option<&dyn CipherKind> {
818 match self.r#type {
819 CipherType::Login => self.login.as_ref().map(|v| v as _),
820 CipherType::Card => self.card.as_ref().map(|v| v as _),
821 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
822 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
823 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
824 CipherType::BankAccount => self.bank_account.as_ref().map(|v| v as _),
825 CipherType::DriversLicense => self.drivers_license.as_ref().map(|v| v as _),
826 CipherType::Passport => self.passport.as_ref().map(|v| v as _),
827 }
828 }
829
830 fn decrypt_subtitle(
832 &self,
833 ctx: &mut KeyStoreContext<KeySlotIds>,
834 key: SymmetricKeySlotId,
835 ) -> Result<String, CryptoError> {
836 self.get_kind()
837 .map(|sub| sub.decrypt_subtitle(ctx, key))
838 .unwrap_or_else(|| Ok(String::new()))
839 }
840
841 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
844 self.get_kind()
845 .map(|kind| kind.get_copyable_fields(Some(self)))
846 .unwrap_or_default()
847 }
848
849 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
855 let data = self
856 .data
857 .as_ref()
858 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
859
860 match &self.r#type {
861 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
862 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
863 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
864 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
865 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
866 crate::CipherType::BankAccount => self.bank_account = serde_json::from_str(data)?,
867 crate::CipherType::DriversLicense => self.drivers_license = serde_json::from_str(data)?,
868 crate::CipherType::Passport => self.passport = serde_json::from_str(data)?,
869 }
870 Ok(())
871 }
872
873 pub(crate) fn soft_delete(&mut self) {
875 self.deleted_date = Some(Utc::now());
876 }
877}
878impl CipherView {
879 #[allow(missing_docs)]
880 pub fn generate_cipher_key(
881 &mut self,
882 ctx: &mut KeyStoreContext<KeySlotIds>,
883 wrapping_key: SymmetricKeySlotId,
884 ) -> Result<(), CryptoError> {
885 let old_unwrapping_key = self.key_identifier();
886 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, old_unwrapping_key, &self.key)?;
887
888 let new_key = ctx.generate_symmetric_key();
889
890 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
891 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
892
893 self.key = Some(ctx.wrap_symmetric_key(wrapping_key, new_key)?);
894 Ok(())
895 }
896
897 #[allow(missing_docs)]
898 pub fn generate_checksums(&mut self) {
899 if let Some(l) = self.login.as_mut() {
900 l.generate_checksums();
901 }
902 }
903
904 #[allow(missing_docs)]
905 pub fn remove_invalid_checksums(&mut self) {
906 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
907 uris.retain(|u| u.is_checksum_valid());
908 }
909 }
910
911 fn reencrypt_attachment_keys(
912 &mut self,
913 ctx: &mut KeyStoreContext<KeySlotIds>,
914 old_key: SymmetricKeySlotId,
915 new_key: SymmetricKeySlotId,
916 ) -> Result<(), CryptoError> {
917 if let Some(attachments) = &mut self.attachments {
918 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
919 }
920 Ok(())
921 }
922
923 #[allow(missing_docs)]
924 pub fn decrypt_fido2_credentials(
925 &self,
926 ctx: &mut KeyStoreContext<KeySlotIds>,
927 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
928 let key = self.key_identifier();
929 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
930
931 Ok(self
932 .login
933 .as_ref()
934 .and_then(|l| l.fido2_credentials.as_ref())
935 .map(|f| f.decrypt(ctx, ciphers_key))
936 .transpose()?
937 .unwrap_or_default())
938 }
939
940 fn reencrypt_fido2_credentials(
941 &mut self,
942 ctx: &mut KeyStoreContext<KeySlotIds>,
943 old_key: SymmetricKeySlotId,
944 new_key: SymmetricKeySlotId,
945 ) -> Result<(), CryptoError> {
946 if let Some(login) = self.login.as_mut() {
947 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
948 }
949 Ok(())
950 }
951
952 pub fn move_to_organization(
959 &mut self,
960 ctx: &mut KeyStoreContext<KeySlotIds>,
961 organization_id: OrganizationId,
962 ) -> Result<(), CipherError> {
963 let new_key = SymmetricKeySlotId::Organization(organization_id);
964
965 self.reencrypt_cipher_keys(ctx, new_key)?;
966 self.organization_id = Some(organization_id);
967
968 Ok(())
969 }
970
971 pub fn reencrypt_cipher_keys(
976 &mut self,
977 ctx: &mut KeyStoreContext<KeySlotIds>,
978 new_wrapping_key: SymmetricKeySlotId,
979 ) -> Result<(), CipherError> {
980 let old_key = self.key_identifier();
981
982 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
984 return Err(CipherError::AttachmentsWithoutKeys);
985 }
986
987 if self.key.is_some() {
989 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
991
992 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
994 } else {
995 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
998 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
999 }
1000
1001 Ok(())
1002 }
1003
1004 #[allow(missing_docs)]
1005 pub fn set_new_fido2_credentials(
1006 &mut self,
1007 ctx: &mut KeyStoreContext<KeySlotIds>,
1008 creds: Vec<Fido2CredentialFullView>,
1009 ) -> Result<(), CipherError> {
1010 let key = self.key_identifier();
1011
1012 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1013
1014 require!(self.login.as_mut()).fido2_credentials =
1015 Some(creds.encrypt_composite(ctx, ciphers_key)?);
1016
1017 Ok(())
1018 }
1019
1020 #[allow(missing_docs)]
1021 pub fn get_fido2_credentials(
1022 &self,
1023 ctx: &mut KeyStoreContext<KeySlotIds>,
1024 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
1025 let key = self.key_identifier();
1026
1027 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1028
1029 let login = require!(self.login.as_ref());
1030 let creds = require!(login.fido2_credentials.as_ref());
1031 let res = creds.decrypt(ctx, ciphers_key)?;
1032 Ok(res)
1033 }
1034
1035 #[allow(missing_docs)]
1036 pub fn decrypt_fido2_private_key(
1037 &self,
1038 ctx: &mut KeyStoreContext<KeySlotIds>,
1039 ) -> Result<String, CipherError> {
1040 let fido2_credential = self.get_fido2_credentials(ctx)?;
1041
1042 Ok(fido2_credential[0].key_value.clone())
1043 }
1044
1045 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
1046 let changes = self
1047 .login
1048 .as_mut()
1049 .map_or(vec![], |login| {
1050 login.detect_password_change(&original_cipher.login)
1051 })
1052 .into_iter()
1053 .chain(self.fields.as_deref().map_or(vec![], |fields| {
1054 FieldView::detect_hidden_field_changes(
1055 fields,
1056 original_cipher.fields.as_deref().unwrap_or(&[]),
1057 )
1058 }))
1059 .rev()
1060 .chain(original_cipher.password_history.iter().flatten().cloned())
1061 .take(MAX_PASSWORD_HISTORY_ENTRIES)
1062 .collect();
1063 self.password_history = Some(changes)
1064 }
1065
1066 pub(crate) fn to_list_view(
1077 &self,
1078 ctx: &mut KeyStoreContext<KeySlotIds>,
1079 key: SymmetricKeySlotId,
1080 ) -> Result<CipherListView, CryptoError> {
1081 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1082
1083 let all_attachments = || {
1084 self.attachments
1085 .iter()
1086 .flatten()
1087 .chain(self.attachment_decryption_failures.iter().flatten())
1088 };
1089 let attachments_count = all_attachments().count() as u32;
1090 let has_old_attachments = all_attachments().any(|att| att.key.is_none());
1091
1092 let list_type = match self.r#type {
1093 CipherType::Login => {
1094 let login = self
1095 .login
1096 .as_ref()
1097 .ok_or(CryptoError::MissingField("login"))?;
1098 CipherListViewType::Login(login.to_list_view(ctx, ciphers_key)?)
1099 }
1100 CipherType::SecureNote => CipherListViewType::SecureNote,
1101 CipherType::Card => {
1102 let card = self
1103 .card
1104 .as_ref()
1105 .ok_or(CryptoError::MissingField("card"))?;
1106 CipherListViewType::Card(CardListView {
1107 brand: card.brand.clone(),
1108 })
1109 }
1110 CipherType::Identity => CipherListViewType::Identity,
1111 CipherType::SshKey => CipherListViewType::SshKey,
1112 CipherType::BankAccount => CipherListViewType::BankAccount,
1113 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1114 CipherType::Passport => CipherListViewType::Passport,
1115 };
1116
1117 Ok(CipherListView {
1118 id: self.id,
1119 organization_id: self.organization_id,
1120 folder_id: self.folder_id,
1121 collection_ids: self.collection_ids.clone(),
1122 key: self.key.clone(),
1123 name: self.name.clone(),
1124 subtitle: self.subtitle(),
1125 r#type: list_type,
1126 favorite: self.favorite,
1127 reprompt: self.reprompt,
1128 organization_use_totp: self.organization_use_totp,
1129 edit: self.edit,
1130 permissions: self.permissions,
1131 view_password: self.view_password,
1132 attachments: attachments_count,
1133 has_old_attachments,
1134 creation_date: self.creation_date,
1135 deleted_date: self.deleted_date,
1136 revision_date: self.revision_date,
1137 archived_date: self.archived_date,
1138 copyable_fields: self.get_copyable_fields(),
1139 local_data: self.local_data.clone(),
1140 #[cfg(feature = "wasm")]
1141 notes: self.notes.clone(),
1142 #[cfg(feature = "wasm")]
1143 fields: self.fields.as_ref().map(|fields| {
1144 fields
1145 .iter()
1146 .cloned()
1147 .map(field::FieldListView::from)
1148 .collect()
1149 }),
1150 #[cfg(feature = "wasm")]
1151 attachment_names: self.attachments.as_ref().map(|attachments| {
1152 attachments
1153 .iter()
1154 .filter_map(|a| a.file_name.clone())
1155 .collect()
1156 }),
1157 })
1158 }
1159
1160 fn subtitle(&self) -> String {
1165 match self.r#type {
1166 CipherType::Login => self
1167 .login
1168 .as_ref()
1169 .and_then(|l| l.username.clone())
1170 .unwrap_or_default(),
1171 CipherType::Card => self
1172 .card
1173 .as_ref()
1174 .map(|c| card::build_subtitle_card(c.brand.clone(), c.number.clone()))
1175 .unwrap_or_default(),
1176 CipherType::Identity => self
1177 .identity
1178 .as_ref()
1179 .map(|i| {
1180 identity::build_subtitle_identity(i.first_name.clone(), i.last_name.clone())
1181 })
1182 .unwrap_or_default(),
1183 CipherType::SshKey => self
1184 .ssh_key
1185 .as_ref()
1186 .map(|s| s.fingerprint.clone())
1187 .unwrap_or_default(),
1188 CipherType::SecureNote => String::new(),
1189 CipherType::BankAccount => self
1190 .bank_account
1191 .as_ref()
1192 .map(|b| b.bank_name.clone().unwrap_or_default())
1193 .unwrap_or_default(),
1194 CipherType::DriversLicense => self
1195 .drivers_license
1196 .as_ref()
1197 .map(|d| {
1198 drivers_license::build_subtitle_drivers_license(
1199 d.first_name.clone(),
1200 d.last_name.clone(),
1201 )
1202 })
1203 .unwrap_or_default(),
1204 CipherType::Passport => self
1205 .passport
1206 .as_ref()
1207 .map(|p| passport::build_subtitle_passport(p.given_name.clone(), p.surname.clone()))
1208 .unwrap_or_default(),
1209 }
1210 }
1211
1212 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
1217 match self.r#type {
1218 CipherType::Login => self
1219 .login
1220 .as_ref()
1221 .map(|l| {
1222 [
1223 l.username
1224 .as_ref()
1225 .map(|_| CopyableCipherFields::LoginUsername),
1226 l.password
1227 .as_ref()
1228 .map(|_| CopyableCipherFields::LoginPassword),
1229 l.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
1230 ]
1231 .into_iter()
1232 .flatten()
1233 .collect()
1234 })
1235 .unwrap_or_default(),
1236 CipherType::Card => self
1237 .card
1238 .as_ref()
1239 .map(|c| {
1240 [
1241 c.number.as_ref().map(|_| CopyableCipherFields::CardNumber),
1242 c.code
1243 .as_ref()
1244 .map(|_| CopyableCipherFields::CardSecurityCode),
1245 ]
1246 .into_iter()
1247 .flatten()
1248 .collect()
1249 })
1250 .unwrap_or_default(),
1251 CipherType::Identity => self
1252 .identity
1253 .as_ref()
1254 .map(|i| {
1255 [
1256 i.username
1257 .as_ref()
1258 .map(|_| CopyableCipherFields::IdentityUsername),
1259 i.email
1260 .as_ref()
1261 .map(|_| CopyableCipherFields::IdentityEmail),
1262 i.phone
1263 .as_ref()
1264 .map(|_| CopyableCipherFields::IdentityPhone),
1265 i.address1
1266 .as_ref()
1267 .or(i.address2.as_ref())
1268 .or(i.address3.as_ref())
1269 .or(i.city.as_ref())
1270 .or(i.state.as_ref())
1271 .or(i.postal_code.as_ref())
1272 .map(|_| CopyableCipherFields::IdentityAddress),
1273 ]
1274 .into_iter()
1275 .flatten()
1276 .collect()
1277 })
1278 .unwrap_or_default(),
1279 CipherType::SshKey => vec![CopyableCipherFields::SshKey],
1280 CipherType::SecureNote => self
1281 .notes
1282 .as_ref()
1283 .map(|_| vec![CopyableCipherFields::SecureNotes])
1284 .unwrap_or_default(),
1285 CipherType::BankAccount => self
1286 .bank_account
1287 .as_ref()
1288 .map(|b| {
1289 [
1290 b.name_on_account
1291 .as_ref()
1292 .map(|_| CopyableCipherFields::BankAccountNameOnAccount),
1293 b.account_number
1294 .as_ref()
1295 .map(|_| CopyableCipherFields::BankAccountAccountNumber),
1296 b.routing_number
1297 .as_ref()
1298 .map(|_| CopyableCipherFields::BankAccountRoutingNumber),
1299 b.branch_number
1300 .as_ref()
1301 .map(|_| CopyableCipherFields::BankAccountBranchNumber),
1302 b.pin.as_ref().map(|_| CopyableCipherFields::BankAccountPin),
1303 b.iban
1304 .as_ref()
1305 .map(|_| CopyableCipherFields::BankAccountIban),
1306 b.swift_code
1307 .as_ref()
1308 .map(|_| CopyableCipherFields::BankAccountSwift),
1309 ]
1310 .into_iter()
1311 .flatten()
1312 .collect()
1313 })
1314 .unwrap_or_default(),
1315 CipherType::DriversLicense => self
1316 .drivers_license
1317 .as_ref()
1318 .map(|d| {
1319 [
1320 d.first_name
1321 .as_ref()
1322 .map(|_| CopyableCipherFields::DriversLicenseFirstName),
1323 d.middle_name
1324 .as_ref()
1325 .map(|_| CopyableCipherFields::DriversLicenseMiddleName),
1326 d.last_name
1327 .as_ref()
1328 .map(|_| CopyableCipherFields::DriversLicenseLastName),
1329 d.license_number
1330 .as_ref()
1331 .map(|_| CopyableCipherFields::DriversLicenseLicenseNumber),
1332 ]
1333 .into_iter()
1334 .flatten()
1335 .collect()
1336 })
1337 .unwrap_or_default(),
1338 CipherType::Passport => self
1339 .passport
1340 .as_ref()
1341 .map(|p| {
1342 [
1343 p.given_name
1344 .as_ref()
1345 .map(|_| CopyableCipherFields::PassportGivenName),
1346 p.surname
1347 .as_ref()
1348 .map(|_| CopyableCipherFields::PassportSurname),
1349 p.passport_number
1350 .as_ref()
1351 .map(|_| CopyableCipherFields::PassportPassportNumber),
1352 p.national_identification_number
1353 .as_ref()
1354 .map(|_| CopyableCipherFields::PassportNationalIdentificationNumber),
1355 ]
1356 .into_iter()
1357 .flatten()
1358 .collect()
1359 })
1360 .unwrap_or_default(),
1361 }
1362 }
1363}
1364
1365pub(crate) fn lenient_decrypt_cipher_list_view(
1368 cipher: &Cipher,
1369 ctx: &mut KeyStoreContext<KeySlotIds>,
1370 key: SymmetricKeySlotId,
1371) -> Result<CipherListView, CryptoError> {
1372 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1373
1374 Ok(CipherListView {
1375 id: cipher.id,
1376 organization_id: cipher.organization_id,
1377 folder_id: cipher.folder_id,
1378 collection_ids: cipher.collection_ids.clone(),
1379 key: cipher.key.clone(),
1380 name: cipher
1381 .name
1382 .decrypt(ctx, ciphers_key)
1383 .ok()
1384 .unwrap_or_default(),
1385 subtitle: cipher
1386 .decrypt_subtitle(ctx, ciphers_key)
1387 .ok()
1388 .unwrap_or_default(),
1389 r#type: match cipher.r#type {
1390 CipherType::Login => {
1391 let login = cipher
1392 .login
1393 .as_ref()
1394 .ok_or(CryptoError::MissingField("login"))?;
1395 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
1396 }
1397 CipherType::SecureNote => CipherListViewType::SecureNote,
1398 CipherType::Card => {
1399 let card = cipher
1400 .card
1401 .as_ref()
1402 .ok_or(CryptoError::MissingField("card"))?;
1403 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
1404 }
1405 CipherType::Identity => CipherListViewType::Identity,
1406 CipherType::SshKey => CipherListViewType::SshKey,
1407 CipherType::BankAccount => CipherListViewType::BankAccount,
1408 CipherType::Passport => CipherListViewType::Passport,
1409 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1410 },
1411 favorite: cipher.favorite,
1412 reprompt: cipher.reprompt,
1413 organization_use_totp: cipher.organization_use_totp,
1414 edit: cipher.edit,
1415 permissions: cipher.permissions,
1416 view_password: cipher.view_password,
1417 attachments: cipher
1418 .attachments
1419 .as_ref()
1420 .map(|a| a.len() as u32)
1421 .unwrap_or(0),
1422 has_old_attachments: cipher
1423 .attachments
1424 .as_ref()
1425 .map(|a| a.iter().any(|att| att.key.is_none()))
1426 .unwrap_or(false),
1427 creation_date: cipher.creation_date,
1428 deleted_date: cipher.deleted_date,
1429 revision_date: cipher.revision_date,
1430 copyable_fields: cipher.get_copyable_fields(),
1431 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1432 archived_date: cipher.archived_date,
1433 #[cfg(feature = "wasm")]
1434 notes: cipher.notes.decrypt(ctx, ciphers_key).ok().flatten(),
1435 #[cfg(feature = "wasm")]
1436 fields: cipher.fields.as_ref().map(|fields| {
1437 fields
1438 .iter()
1439 .filter_map(|f| {
1440 f.decrypt(ctx, ciphers_key)
1441 .ok()
1442 .map(field::FieldListView::from)
1443 })
1444 .collect()
1445 }),
1446 #[cfg(feature = "wasm")]
1447 attachment_names: cipher.attachments.as_ref().map(|attachments| {
1448 attachments
1449 .iter()
1450 .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
1451 .collect()
1452 }),
1453 })
1454}
1455
1456impl IdentifyKey<SymmetricKeySlotId> for Cipher {
1457 fn key_identifier(&self) -> SymmetricKeySlotId {
1458 match self.organization_id {
1459 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1460 None => SymmetricKeySlotId::User,
1461 }
1462 }
1463}
1464
1465impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for Cipher {
1466 #[bitwarden_logging::instrument(err, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
1467 fn decrypt(
1468 &self,
1469 ctx: &mut KeyStoreContext<KeySlotIds>,
1470 key: SymmetricKeySlotId,
1471 ) -> Result<CipherView, CryptoError> {
1472 if is_blob_encrypted(self) {
1473 decrypt_blob_cipher(self, ctx).map_err(blob_err_to_crypto)
1474 } else {
1475 lenient_decrypt_cipher_view(self, ctx, key)
1476 }
1477 }
1478}
1479
1480impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for Cipher {
1481 fn decrypt(
1482 &self,
1483 ctx: &mut KeyStoreContext<KeySlotIds>,
1484 key: SymmetricKeySlotId,
1485 ) -> Result<CipherListView, CryptoError> {
1486 if is_blob_encrypted(self) {
1487 let view = decrypt_blob_cipher(self, ctx).map_err(blob_err_to_crypto)?;
1488 view.to_list_view(ctx, key)
1489 } else {
1490 lenient_decrypt_cipher_list_view(self, ctx, key)
1491 }
1492 }
1493}
1494
1495impl IdentifyKey<SymmetricKeySlotId> for CipherView {
1496 fn key_identifier(&self) -> SymmetricKeySlotId {
1497 match self.organization_id {
1498 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1499 None => SymmetricKeySlotId::User,
1500 }
1501 }
1502}
1503
1504impl IdentifyKey<SymmetricKeySlotId> for CipherListView {
1505 fn key_identifier(&self) -> SymmetricKeySlotId {
1506 match self.organization_id {
1507 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1508 None => SymmetricKeySlotId::User,
1509 }
1510 }
1511}
1512
1513pub(crate) struct StrictDecrypt<T>(pub(crate) T);
1522
1523impl IdentifyKey<SymmetricKeySlotId> for StrictDecrypt<Cipher> {
1524 fn key_identifier(&self) -> SymmetricKeySlotId {
1525 self.0.key_identifier()
1526 }
1527}
1528
1529impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for StrictDecrypt<Cipher> {
1530 #[bitwarden_logging::instrument(err, fields(cipher_id = ?self.0.id, org_id = ?self.0.organization_id, kind = ?self.0.r#type))]
1531 fn decrypt(
1532 &self,
1533 ctx: &mut KeyStoreContext<KeySlotIds>,
1534 key: SymmetricKeySlotId,
1535 ) -> Result<CipherView, CryptoError> {
1536 if is_blob_encrypted(&self.0) {
1537 decrypt_blob_cipher(&self.0, ctx).map_err(blob_err_to_crypto)
1538 } else {
1539 strict_decrypt_cipher_view(&self.0, ctx, key)
1540 }
1541 }
1542}
1543
1544fn strict_decrypt_cipher_view(
1547 cipher: &Cipher,
1548 ctx: &mut KeyStoreContext<KeySlotIds>,
1549 key: SymmetricKeySlotId,
1550) -> Result<CipherView, CryptoError> {
1551 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1552
1553 let (attachments, attachment_decryption_failures) =
1555 attachment::decrypt_attachments_with_failures(
1556 cipher.attachments.as_deref().unwrap_or_default(),
1557 ctx,
1558 ciphers_key,
1559 );
1560
1561 let mut view = CipherView {
1562 id: cipher.id,
1563 organization_id: cipher.organization_id,
1564 folder_id: cipher.folder_id,
1565 collection_ids: cipher.collection_ids.clone(),
1566 key: cipher.key.clone(),
1567 name: cipher.name.decrypt(ctx, ciphers_key)?,
1568 notes: cipher.notes.decrypt(ctx, ciphers_key)?,
1569 r#type: cipher.r#type,
1570 login: cipher
1571 .login
1572 .as_ref()
1573 .map(|l| StrictDecrypt(l).decrypt(ctx, ciphers_key))
1574 .transpose()?,
1575 identity: cipher
1576 .identity
1577 .as_ref()
1578 .map(|i| StrictDecrypt(i).decrypt(ctx, ciphers_key))
1579 .transpose()?,
1580 card: cipher
1581 .card
1582 .as_ref()
1583 .map(|c| StrictDecrypt(c).decrypt(ctx, ciphers_key))
1584 .transpose()?,
1585 secure_note: cipher.secure_note.decrypt(ctx, ciphers_key)?,
1586 ssh_key: cipher.ssh_key.decrypt(ctx, ciphers_key)?,
1587 bank_account: cipher.bank_account.decrypt(ctx, ciphers_key)?,
1588 drivers_license: cipher.drivers_license.decrypt(ctx, ciphers_key)?,
1589 passport: cipher.passport.decrypt(ctx, ciphers_key)?,
1590 favorite: cipher.favorite,
1591 reprompt: cipher.reprompt,
1592 organization_use_totp: cipher.organization_use_totp,
1593 edit: cipher.edit,
1594 permissions: cipher.permissions,
1595 view_password: cipher.view_password,
1596 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1597 attachments: Some(attachments),
1598 attachment_decryption_failures: Some(attachment_decryption_failures),
1599 fields: cipher
1600 .fields
1601 .as_ref()
1602 .map(|fields| {
1603 fields
1604 .iter()
1605 .map(|f| StrictDecrypt(f).decrypt(ctx, ciphers_key))
1606 .collect::<Result<Vec<_>, _>>()
1607 })
1608 .transpose()?,
1609 password_history: cipher.password_history.decrypt(ctx, ciphers_key)?,
1610 creation_date: cipher.creation_date,
1611 deleted_date: cipher.deleted_date,
1612 revision_date: cipher.revision_date,
1613 archived_date: cipher.archived_date,
1614 };
1615
1616 if view.key.is_some()
1619 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1620 {
1621 view.remove_invalid_checksums();
1622 }
1623
1624 Ok(view)
1625}
1626
1627impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for StrictDecrypt<Cipher> {
1628 fn decrypt(
1629 &self,
1630 ctx: &mut KeyStoreContext<KeySlotIds>,
1631 key: SymmetricKeySlotId,
1632 ) -> Result<CipherListView, CryptoError> {
1633 if is_blob_encrypted(&self.0) {
1634 let view = decrypt_blob_cipher(&self.0, ctx).map_err(blob_err_to_crypto)?;
1635 view.to_list_view(ctx, key)
1636 } else {
1637 strict_decrypt_cipher_list_view(&self.0, ctx, key)
1638 }
1639 }
1640}
1641
1642fn strict_decrypt_cipher_list_view(
1645 cipher: &Cipher,
1646 ctx: &mut KeyStoreContext<KeySlotIds>,
1647 key: SymmetricKeySlotId,
1648) -> Result<CipherListView, CryptoError> {
1649 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1650
1651 Ok(CipherListView {
1652 id: cipher.id,
1653 organization_id: cipher.organization_id,
1654 folder_id: cipher.folder_id,
1655 collection_ids: cipher.collection_ids.clone(),
1656 key: cipher.key.clone(),
1657 name: cipher.name.decrypt(ctx, ciphers_key)?,
1658 subtitle: cipher.decrypt_subtitle(ctx, ciphers_key)?,
1659 r#type: match cipher.r#type {
1660 CipherType::Login => {
1661 let login = cipher
1662 .login
1663 .as_ref()
1664 .ok_or(CryptoError::MissingField("login"))?;
1665 CipherListViewType::Login(StrictDecrypt(login).decrypt(ctx, ciphers_key)?)
1666 }
1667 CipherType::SecureNote => CipherListViewType::SecureNote,
1668 CipherType::Card => {
1669 let card = cipher
1670 .card
1671 .as_ref()
1672 .ok_or(CryptoError::MissingField("card"))?;
1673 CipherListViewType::Card(StrictDecrypt(card).decrypt(ctx, ciphers_key)?)
1674 }
1675 CipherType::Identity => CipherListViewType::Identity,
1676 CipherType::SshKey => CipherListViewType::SshKey,
1677 CipherType::BankAccount => CipherListViewType::BankAccount,
1678 CipherType::Passport => CipherListViewType::Passport,
1679 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1680 },
1681 favorite: cipher.favorite,
1682 reprompt: cipher.reprompt,
1683 organization_use_totp: cipher.organization_use_totp,
1684 edit: cipher.edit,
1685 permissions: cipher.permissions,
1686 view_password: cipher.view_password,
1687 attachments: cipher
1688 .attachments
1689 .as_ref()
1690 .map(|a| a.len() as u32)
1691 .unwrap_or(0),
1692 has_old_attachments: cipher
1693 .attachments
1694 .as_ref()
1695 .map(|a| a.iter().any(|att| att.key.is_none()))
1696 .unwrap_or(false),
1697 creation_date: cipher.creation_date,
1698 deleted_date: cipher.deleted_date,
1699 revision_date: cipher.revision_date,
1700 copyable_fields: cipher.get_copyable_fields(),
1701 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1702 archived_date: cipher.archived_date,
1703 #[cfg(feature = "wasm")]
1704 notes: cipher.notes.decrypt(ctx, ciphers_key)?,
1705 #[cfg(feature = "wasm")]
1706 fields: cipher
1707 .fields
1708 .as_ref()
1709 .map(|fields| {
1710 fields
1711 .iter()
1712 .map(|f| {
1713 StrictDecrypt(f)
1714 .decrypt(ctx, ciphers_key)
1715 .map(field::FieldListView::from)
1716 })
1717 .collect::<Result<Vec<_>, _>>()
1718 })
1719 .transpose()?,
1720 #[cfg(feature = "wasm")]
1721 attachment_names: cipher
1722 .attachments
1723 .as_ref()
1724 .map(|attachments| {
1725 attachments
1726 .iter()
1727 .map(|a| a.file_name.decrypt(ctx, ciphers_key))
1728 .collect::<Result<Vec<_>, _>>()
1729 })
1730 .transpose()?
1731 .map(|names| names.into_iter().flatten().collect()),
1732 })
1733}
1734
1735fn blob_err_to_crypto(err: BlobEncryptionError) -> CryptoError {
1740 match err {
1741 BlobEncryptionError::Crypto(c) => c,
1742 BlobEncryptionError::SealedBlob(_) | BlobEncryptionError::NoBlobData => {
1743 CryptoError::Decrypt
1744 }
1745 }
1746}
1747
1748impl TryFrom<CipherDetailsResponseModel> for Cipher {
1749 type Error = VaultParseError;
1750
1751 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
1752 Ok(Self {
1753 id: cipher.id.map(CipherId::new),
1754 organization_id: cipher.organization_id.map(OrganizationId::new),
1755 folder_id: cipher.folder_id.map(FolderId::new),
1756 collection_ids: cipher
1757 .collection_ids
1758 .unwrap_or_default()
1759 .into_iter()
1760 .map(CollectionId::new)
1761 .collect(),
1762 name: require!(EncString::try_from_optional(cipher.name)?),
1763 notes: EncString::try_from_optional(cipher.notes)?,
1764 r#type: require!(cipher.r#type).try_into()?,
1765 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1766 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1767 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1768 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1769 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1770 bank_account: cipher.bank_account.map(|b| (*b).try_into()).transpose()?,
1771 drivers_license: cipher
1772 .drivers_license
1773 .map(|d| (*d).try_into())
1774 .transpose()?,
1775 passport: cipher.passport.map(|p| (*p).try_into()).transpose()?,
1776 favorite: cipher.favorite.unwrap_or(false),
1777 reprompt: cipher
1778 .reprompt
1779 .map(|r| r.try_into())
1780 .transpose()?
1781 .unwrap_or(CipherRepromptType::None),
1782 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
1783 edit: cipher.edit.unwrap_or(true),
1784 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1785 view_password: cipher.view_password.unwrap_or(true),
1786 local_data: None, attachments: cipher
1788 .attachments
1789 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1790 .transpose()?,
1791 fields: cipher
1792 .fields
1793 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1794 .transpose()?,
1795 password_history: cipher
1796 .password_history
1797 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1798 .transpose()?,
1799 creation_date: require!(cipher.creation_date).parse()?,
1800 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1801 revision_date: require!(cipher.revision_date).parse()?,
1802 key: EncString::try_from_optional(cipher.key)?,
1803 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1804 data: cipher.data,
1805 })
1806 }
1807}
1808
1809impl PartialCipher for CipherDetailsResponseModel {
1810 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1811 Ok(Cipher {
1812 local_data: cipher.and_then(|c| c.local_data),
1813 ..self.try_into()?
1814 })
1815 }
1816}
1817
1818impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
1819 type Error = MissingFieldError;
1820
1821 fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
1822 Ok(match t {
1823 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1824 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1825 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1826 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1827 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1828 bitwarden_api_api::models::CipherType::BankAccount => CipherType::BankAccount,
1829 bitwarden_api_api::models::CipherType::Passport => CipherType::Passport,
1830 bitwarden_api_api::models::CipherType::DriversLicense => CipherType::DriversLicense,
1831 bitwarden_api_api::models::CipherType::__Unknown(_) => {
1832 return Err(MissingFieldError("type"));
1833 }
1834 })
1835 }
1836}
1837
1838impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1839 type Error = MissingFieldError;
1840
1841 fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
1842 Ok(match t {
1843 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1844 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1845 bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
1846 return Err(MissingFieldError("reprompt"));
1847 }
1848 })
1849 }
1850}
1851
1852pub(crate) trait PartialCipher {
1856 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1857}
1858
1859impl From<CipherType> for bitwarden_api_api::models::CipherType {
1860 fn from(t: CipherType) -> Self {
1861 match t {
1862 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1863 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1864 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1865 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1866 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1867 CipherType::BankAccount => bitwarden_api_api::models::CipherType::BankAccount,
1868 CipherType::Passport => bitwarden_api_api::models::CipherType::Passport,
1869 CipherType::DriversLicense => bitwarden_api_api::models::CipherType::DriversLicense,
1870 }
1871 }
1872}
1873
1874impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1875 fn from(t: CipherRepromptType) -> Self {
1876 match t {
1877 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1878 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1879 }
1880 }
1881}
1882
1883impl PartialCipher for CipherResponseModel {
1884 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1885 Ok(Cipher {
1886 collection_ids: cipher
1887 .as_ref()
1888 .map(|c| c.collection_ids.clone())
1889 .unwrap_or_default(),
1890 local_data: cipher.and_then(|c| c.local_data),
1891 id: self.id.map(CipherId::new),
1892 organization_id: self.organization_id.map(OrganizationId::new),
1893 folder_id: self.folder_id.map(FolderId::new),
1894 name: require!(self.name).parse()?,
1895 notes: EncString::try_from_optional(self.notes)?,
1896 r#type: require!(self.r#type).try_into()?,
1897 login: self.login.map(|l| (*l).try_into()).transpose()?,
1898 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1899 card: self.card.map(|c| (*c).try_into()).transpose()?,
1900 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1901 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1902 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1903 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1904 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1905 favorite: self.favorite.unwrap_or(false),
1906 reprompt: self
1907 .reprompt
1908 .map(|r| r.try_into())
1909 .transpose()?
1910 .unwrap_or(CipherRepromptType::None),
1911 organization_use_totp: self.organization_use_totp.unwrap_or(false),
1912 edit: self.edit.unwrap_or(false),
1913 permissions: self.permissions.map(|p| (*p).try_into()).transpose()?,
1914 view_password: self.view_password.unwrap_or(true),
1915 attachments: self
1916 .attachments
1917 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1918 .transpose()?,
1919 fields: self
1920 .fields
1921 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1922 .transpose()?,
1923 password_history: self
1924 .password_history
1925 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1926 .transpose()?,
1927 creation_date: require!(self.creation_date).parse()?,
1928 deleted_date: self.deleted_date.map(|d| d.parse()).transpose()?,
1929 revision_date: require!(self.revision_date).parse()?,
1930 key: EncString::try_from_optional(self.key)?,
1931 archived_date: self.archived_date.map(|d| d.parse()).transpose()?,
1932 data: self.data,
1933 })
1934 }
1935}
1936
1937impl PartialCipher for CipherMiniResponseModel {
1938 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1939 let cipher = cipher.as_ref();
1940 Ok(Cipher {
1941 id: self.id.map(CipherId::new),
1942 organization_id: self.organization_id.map(OrganizationId::new),
1943 key: EncString::try_from_optional(self.key)?,
1944 name: require!(EncString::try_from_optional(self.name)?),
1945 notes: EncString::try_from_optional(self.notes)?,
1946 r#type: require!(self.r#type).try_into()?,
1947 login: self.login.map(|l| (*l).try_into()).transpose()?,
1948 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1949 card: self.card.map(|c| (*c).try_into()).transpose()?,
1950 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1951 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1952 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1953 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1954 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1955 reprompt: self
1956 .reprompt
1957 .map(|r| r.try_into())
1958 .transpose()?
1959 .unwrap_or(CipherRepromptType::None),
1960 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1961 attachments: self
1962 .attachments
1963 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1964 .transpose()?,
1965 fields: self
1966 .fields
1967 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1968 .transpose()?,
1969 password_history: self
1970 .password_history
1971 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1972 .transpose()?,
1973 creation_date: require!(self.creation_date)
1974 .parse()
1975 .map_err(Into::<VaultParseError>::into)?,
1976 deleted_date: self
1977 .deleted_date
1978 .map(|d| d.parse())
1979 .transpose()
1980 .map_err(Into::<VaultParseError>::into)?,
1981 revision_date: require!(self.revision_date)
1982 .parse()
1983 .map_err(Into::<VaultParseError>::into)?,
1984 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1985 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1986 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1987 edit: cipher.map_or(Default::default(), |c| c.edit),
1988 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1989 view_password: cipher.is_none_or(|c| c.view_password),
1990 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1991 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1992 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1993 })
1994 }
1995}
1996
1997impl PartialCipher for CipherMiniDetailsResponseModel {
1998 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1999 let cipher = cipher.as_ref();
2000 Ok(Cipher {
2001 id: self.id.map(CipherId::new),
2002 organization_id: self.organization_id.map(OrganizationId::new),
2003 key: EncString::try_from_optional(self.key)?,
2004 name: require!(EncString::try_from_optional(self.name)?),
2005 notes: EncString::try_from_optional(self.notes)?,
2006 r#type: require!(self.r#type).try_into()?,
2007 login: self.login.map(|l| (*l).try_into()).transpose()?,
2008 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2009 card: self.card.map(|c| (*c).try_into()).transpose()?,
2010 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2011 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2012 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2013 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2014 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2015 reprompt: self
2016 .reprompt
2017 .map(|r| r.try_into())
2018 .transpose()?
2019 .unwrap_or(CipherRepromptType::None),
2020 organization_use_totp: self.organization_use_totp.unwrap_or(true),
2021 attachments: self
2022 .attachments
2023 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2024 .transpose()?,
2025 fields: self
2026 .fields
2027 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2028 .transpose()?,
2029 password_history: self
2030 .password_history
2031 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2032 .transpose()?,
2033 creation_date: require!(self.creation_date)
2034 .parse()
2035 .map_err(Into::<VaultParseError>::into)?,
2036 deleted_date: self
2037 .deleted_date
2038 .map(|d| d.parse())
2039 .transpose()
2040 .map_err(Into::<VaultParseError>::into)?,
2041 revision_date: require!(self.revision_date)
2042 .parse()
2043 .map_err(Into::<VaultParseError>::into)?,
2044 collection_ids: self
2045 .collection_ids
2046 .into_iter()
2047 .flatten()
2048 .map(CollectionId::new)
2049 .collect(),
2050 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
2051 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
2052 favorite: cipher.map_or(Default::default(), |c| c.favorite),
2053 edit: cipher.map_or(Default::default(), |c| c.edit),
2054 permissions: cipher.map_or(Default::default(), |c| c.permissions),
2055 view_password: cipher.is_none_or(|c: &Cipher| c.view_password),
2056 data: cipher.map_or(Default::default(), |c| c.data.clone()),
2057 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
2058 })
2059 }
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064
2065 use attachment::AttachmentView;
2066 use bitwarden_core::key_management::{
2067 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
2068 };
2069 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
2070
2071 use super::*;
2072 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
2073
2074 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
2076 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
2077 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2078 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
2079 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
2080 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2081 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
2082
2083 fn generate_cipher() -> CipherView {
2084 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
2085 CipherView {
2086 r#type: CipherType::Login,
2087 login: Some(LoginView {
2088 username: Some("test_username".to_string()),
2089 password: Some("test_password".to_string()),
2090 password_revision_date: None,
2091 uris: None,
2092 totp: None,
2093 autofill_on_page_load: None,
2094 fido2_credentials: None,
2095 }),
2096 id: Some(test_id),
2097 organization_id: None,
2098 folder_id: None,
2099 collection_ids: vec![],
2100 key: None,
2101 name: "My test login".to_string(),
2102 notes: None,
2103 identity: None,
2104 card: None,
2105 secure_note: None,
2106 ssh_key: None,
2107 bank_account: None,
2108 drivers_license: None,
2109 passport: None,
2110 favorite: false,
2111 reprompt: CipherRepromptType::None,
2112 organization_use_totp: true,
2113 edit: true,
2114 permissions: None,
2115 view_password: true,
2116 local_data: None,
2117 attachments: None,
2118 attachment_decryption_failures: None,
2119 fields: None,
2120 password_history: None,
2121 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2122 deleted_date: None,
2123 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2124 archived_date: None,
2125 }
2126 }
2127
2128 fn generate_fido2(
2129 ctx: &mut KeyStoreContext<KeySlotIds>,
2130 key: SymmetricKeySlotId,
2131 ) -> Fido2Credential {
2132 Fido2Credential {
2133 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
2134 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
2135 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
2136 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
2137 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
2138 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
2139 user_handle: None,
2140 user_name: None,
2141 counter: "123".to_string().encrypt(ctx, key).unwrap(),
2142 rp_name: None,
2143 user_display_name: None,
2144 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
2145 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
2146 }
2147 }
2148
2149 #[test]
2150 fn test_decrypt_cipher_list_view() {
2151 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2152 let key_store = create_test_crypto_with_user_key(key);
2153
2154 let cipher = Cipher {
2155 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2156 organization_id: None,
2157 folder_id: None,
2158 collection_ids: vec![],
2159 key: None,
2160 name: TEST_CIPHER_NAME.parse().unwrap(),
2161 notes: None,
2162 r#type: CipherType::Login,
2163 login: Some(Login {
2164 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
2165 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
2166 password_revision_date: None,
2167 uris: None,
2168 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
2169 autofill_on_page_load: None,
2170 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
2171 }),
2172 identity: None,
2173 card: None,
2174 secure_note: None,
2175 ssh_key: None,
2176 bank_account: None,
2177 drivers_license: None,
2178 passport: None,
2179 favorite: false,
2180 reprompt: CipherRepromptType::None,
2181 organization_use_totp: false,
2182 edit: true,
2183 permissions: Some(CipherPermissions {
2184 delete: false,
2185 restore: false
2186 }),
2187 view_password: true,
2188 local_data: None,
2189 attachments: None,
2190 fields: None,
2191 password_history: None,
2192 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2193 deleted_date: None,
2194 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2195 archived_date: None,
2196 data: None,
2197 };
2198
2199 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
2200
2201 assert_eq!(
2202 view,
2203 CipherListView {
2204 id: cipher.id,
2205 organization_id: cipher.organization_id,
2206 folder_id: cipher.folder_id,
2207 collection_ids: cipher.collection_ids,
2208 key: cipher.key,
2209 name: "My test login".to_string(),
2210 subtitle: "test_username".to_string(),
2211 r#type: CipherListViewType::Login(LoginListView {
2212 fido2_credentials: Some(vec![Fido2CredentialListView {
2213 credential_id: "123".to_string(),
2214 rp_id: "123".to_string(),
2215 user_handle: None,
2216 user_name: None,
2217 user_display_name: None,
2218 counter: "123".to_string(),
2219 }]),
2220 has_fido2: true,
2221 username: Some("test_username".to_string()),
2222 totp: cipher.login.as_ref().unwrap().totp.clone(),
2223 uris: None,
2224 }),
2225 favorite: cipher.favorite,
2226 reprompt: cipher.reprompt,
2227 organization_use_totp: cipher.organization_use_totp,
2228 edit: cipher.edit,
2229 permissions: cipher.permissions,
2230 view_password: cipher.view_password,
2231 attachments: 0,
2232 has_old_attachments: false,
2233 creation_date: cipher.creation_date,
2234 deleted_date: cipher.deleted_date,
2235 revision_date: cipher.revision_date,
2236 copyable_fields: vec![
2237 CopyableCipherFields::LoginUsername,
2238 CopyableCipherFields::LoginPassword,
2239 CopyableCipherFields::LoginTotp
2240 ],
2241 local_data: None,
2242 archived_date: cipher.archived_date,
2243 #[cfg(feature = "wasm")]
2244 notes: None,
2245 #[cfg(feature = "wasm")]
2246 fields: None,
2247 #[cfg(feature = "wasm")]
2248 attachment_names: None,
2249 }
2250 )
2251 }
2252
2253 #[test]
2254 fn test_decrypt_cipher_fails_with_invalid_name() {
2255 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2256 SymmetricKeyAlgorithm::Aes256CbcHmac,
2257 ));
2258
2259 let cipher = key_store.encrypt(generate_cipher()).unwrap();
2261 let cipher = Cipher {
2262 name: TEST_CIPHER_NAME.parse().unwrap(), ..cipher
2264 };
2265
2266 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2268 assert!(
2269 lenient_result.is_ok(),
2270 "Lenient decryption should succeed even when name is encrypted with a different key"
2271 );
2272 assert_eq!(
2273 lenient_result.unwrap().name,
2274 String::new(),
2275 "Lenient decryption should yield an empty name on error"
2276 );
2277
2278 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
2280 assert!(
2281 strict_result.is_err(),
2282 "Strict decryption should fail when name is encrypted with a different key"
2283 );
2284 }
2285
2286 #[test]
2287 fn test_decrypt_cipher_fails_with_invalid_login() {
2288 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2289 SymmetricKeyAlgorithm::Aes256CbcHmac,
2290 ));
2291
2292 let cipher = key_store.encrypt(generate_cipher()).unwrap();
2294 let cipher = Cipher {
2295 login: Some(Login {
2296 username: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher.login.unwrap()
2298 }),
2299 ..cipher
2300 };
2301
2302 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2304 assert!(
2305 lenient_result.is_ok(),
2306 "Lenient decryption should succeed even when login username is encrypted with a different key"
2307 );
2308 let lenient_view = lenient_result.unwrap();
2309 assert!(
2310 lenient_view.login.is_some(),
2311 "Lenient decryption should still return the login object"
2312 );
2313 assert!(
2314 lenient_view.login.unwrap().username.is_none(),
2315 "Lenient decryption should null out the failing username field"
2316 );
2317
2318 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
2320 assert!(
2321 strict_result.is_err(),
2322 "Strict decryption should fail when login username is encrypted with a different key"
2323 );
2324 }
2325
2326 #[test]
2327 fn test_generate_cipher_key() {
2328 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2329 let key_store = create_test_crypto_with_user_key(key);
2330
2331 let original_cipher = generate_cipher();
2332
2333 let cipher = generate_cipher();
2335 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
2336 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
2337 assert!(no_key_cipher_dec.key.is_none());
2338 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
2339
2340 let mut cipher = generate_cipher();
2341 cipher
2342 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2343 .unwrap();
2344
2345 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
2347 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
2348 assert!(key_cipher_dec.key.is_some());
2349 assert_eq!(key_cipher_dec.name, original_cipher.name);
2350 }
2351
2352 #[test]
2353 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
2354 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2355 let key_store = create_test_crypto_with_user_key(key);
2356
2357 let mut original_cipher = generate_cipher();
2358 {
2359 let mut ctx = key_store.context();
2360 let cipher_key = ctx.generate_symmetric_key();
2361
2362 original_cipher.key = Some(
2363 ctx.wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
2364 .unwrap(),
2365 );
2366 }
2367
2368 original_cipher
2369 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
2370 .unwrap();
2371
2372 let wrapped_key = original_cipher.key.unwrap();
2374 let mut ctx = key_store.context();
2375 let _ = ctx
2376 .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_key)
2377 .unwrap();
2378 }
2379
2380 #[test]
2381 fn test_generate_cipher_key_ignores_attachments_without_key() {
2382 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2383 let key_store = create_test_crypto_with_user_key(key);
2384
2385 let mut cipher = generate_cipher();
2386 let attachment = AttachmentView {
2387 id: None,
2388 url: None,
2389 size: None,
2390 size_name: None,
2391 file_name: Some("Attachment test name".into()),
2392 key: None,
2393 #[cfg(feature = "wasm")]
2394 decrypted_key: None,
2395 };
2396 cipher.attachments = Some(vec![attachment]);
2397
2398 cipher
2399 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2400 .unwrap();
2401 assert!(cipher.attachments.unwrap()[0].key.is_none());
2402 }
2403
2404 #[test]
2405 fn test_reencrypt_cipher_key() {
2406 let old_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2407 let new_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2408 let key_store = create_test_crypto_with_user_key(old_key);
2409 let mut ctx = key_store.context_mut();
2410
2411 let mut cipher = generate_cipher();
2412 cipher
2413 .generate_cipher_key(&mut ctx, cipher.key_identifier())
2414 .unwrap();
2415
2416 let new_key_id = ctx.add_local_symmetric_key(new_key);
2418
2419 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
2420
2421 assert!(cipher.key.is_some());
2423 assert!(
2424 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
2425 .is_ok()
2426 );
2427 }
2428
2429 #[test]
2430 fn test_reencrypt_cipher_key_ignores_missing_key() {
2431 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2432 let key_store = create_test_crypto_with_user_key(key);
2433 let mut ctx = key_store.context_mut();
2434 let mut cipher = generate_cipher();
2435
2436 let new_cipher_key = ctx.generate_symmetric_key();
2438 cipher
2439 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
2440 .unwrap();
2441
2442 assert!(cipher.key.is_none());
2444 }
2445
2446 #[test]
2447 fn test_move_user_cipher_to_org() {
2448 let org = OrganizationId::new_v4();
2449 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2450 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2451 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2452
2453 let mut cipher = generate_cipher();
2455 cipher
2456 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2457 .unwrap();
2458
2459 cipher
2460 .move_to_organization(&mut key_store.context(), org)
2461 .unwrap();
2462 assert_eq!(cipher.organization_id, Some(org));
2463
2464 let cipher_enc = key_store.encrypt(cipher).unwrap();
2466 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
2467
2468 assert_eq!(cipher_dec.name, "My test login");
2469 }
2470
2471 #[test]
2472 fn test_move_user_cipher_to_org_manually() {
2473 let org = OrganizationId::new_v4();
2474 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2475 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2476 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2477
2478 let mut cipher = generate_cipher();
2480 cipher
2481 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2482 .unwrap();
2483
2484 cipher.organization_id = Some(org);
2485
2486 assert!(key_store.encrypt(cipher).is_err());
2489 }
2490
2491 #[test]
2492 fn test_move_user_cipher_with_attachment_without_key_to_org() {
2493 let org = OrganizationId::new_v4();
2494 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2495 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2496 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2497
2498 let mut cipher = generate_cipher();
2499 let attachment = AttachmentView {
2500 id: None,
2501 url: None,
2502 size: None,
2503 size_name: None,
2504 file_name: Some("Attachment test name".into()),
2505 key: None,
2506 #[cfg(feature = "wasm")]
2507 decrypted_key: None,
2508 };
2509 cipher.attachments = Some(vec![attachment]);
2510
2511 assert!(
2513 cipher
2514 .move_to_organization(&mut key_store.context(), org)
2515 .is_err()
2516 );
2517 }
2518
2519 #[test]
2520 fn test_move_user_cipher_with_attachment_with_key_to_org() {
2521 let org = OrganizationId::new_v4();
2522 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2523 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2524 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2525 let org_key = SymmetricKeySlotId::Organization(org);
2526
2527 let (attachment_key_enc, attachment_key_val) = {
2529 let mut ctx = key_store.context();
2530 let attachment_key = ctx.generate_symmetric_key();
2531 let attachment_key_enc = ctx
2532 .wrap_symmetric_key(SymmetricKeySlotId::User, attachment_key)
2533 .unwrap();
2534 #[allow(deprecated)]
2535 let attachment_key_val = ctx
2536 .dangerous_get_symmetric_key(attachment_key)
2537 .unwrap()
2538 .clone();
2539
2540 (attachment_key_enc, attachment_key_val)
2541 };
2542
2543 let mut cipher = generate_cipher();
2544 let attachment = AttachmentView {
2545 id: None,
2546 url: None,
2547 size: None,
2548 size_name: None,
2549 file_name: Some("Attachment test name".into()),
2550 key: Some(attachment_key_enc),
2551 #[cfg(feature = "wasm")]
2552 decrypted_key: None,
2553 };
2554 cipher.attachments = Some(vec![attachment]);
2555 let cred = generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User);
2556 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
2557
2558 cipher
2559 .move_to_organization(&mut key_store.context(), org)
2560 .unwrap();
2561
2562 assert!(cipher.key.is_none());
2563
2564 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
2567 let mut ctx = key_store.context();
2568 let new_attachment_key_id = ctx
2569 .unwrap_symmetric_key(org_key, &new_attachment_key)
2570 .unwrap();
2571 #[allow(deprecated)]
2572 let new_attachment_key_dec = ctx
2573 .dangerous_get_symmetric_key(new_attachment_key_id)
2574 .unwrap();
2575
2576 assert_eq!(*new_attachment_key_dec, attachment_key_val);
2577
2578 let cred2: Fido2CredentialFullView = cipher
2579 .login
2580 .unwrap()
2581 .fido2_credentials
2582 .unwrap()
2583 .first()
2584 .unwrap()
2585 .decrypt(&mut key_store.context(), org_key)
2586 .unwrap();
2587
2588 assert_eq!(cred2.credential_id, "123");
2589 }
2590
2591 #[test]
2592 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
2593 let org = OrganizationId::new_v4();
2594 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2595 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2596 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2597 let org_key = SymmetricKeySlotId::Organization(org);
2598
2599 let mut ctx = key_store.context();
2600
2601 let cipher_key = ctx.generate_symmetric_key();
2602 let cipher_key_enc = ctx
2603 .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
2604 .unwrap();
2605
2606 let attachment_key = ctx.generate_symmetric_key();
2608 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
2609
2610 let mut cipher = generate_cipher();
2611 cipher.key = Some(cipher_key_enc);
2612
2613 let attachment = AttachmentView {
2614 id: None,
2615 url: None,
2616 size: None,
2617 size_name: None,
2618 file_name: Some("Attachment test name".into()),
2619 key: Some(attachment_key_enc.clone()),
2620 #[cfg(feature = "wasm")]
2621 decrypted_key: None,
2622 };
2623 cipher.attachments = Some(vec![attachment]);
2624
2625 let cred = generate_fido2(&mut ctx, cipher_key);
2626 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
2627
2628 cipher.move_to_organization(&mut ctx, org).unwrap();
2629
2630 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
2632 let new_cipher_key_dec = ctx
2633 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
2634 .unwrap();
2635 #[allow(deprecated)]
2636 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
2637 #[allow(deprecated)]
2638 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
2639
2640 assert_eq!(new_cipher_key_dec, cipher_key_val);
2641
2642 assert_eq!(
2644 cipher.attachments.unwrap()[0]
2645 .key
2646 .as_ref()
2647 .unwrap()
2648 .to_string(),
2649 attachment_key_enc.to_string()
2650 );
2651
2652 let cred2: Fido2Credential = cipher
2653 .login
2654 .unwrap()
2655 .fido2_credentials
2656 .unwrap()
2657 .first()
2658 .unwrap()
2659 .clone();
2660
2661 assert_eq!(
2662 cred2.credential_id.to_string(),
2663 cred.credential_id.to_string()
2664 );
2665 }
2666
2667 #[test]
2668 fn test_decrypt_fido2_private_key() {
2669 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2670 SymmetricKeyAlgorithm::Aes256CbcHmac,
2671 ));
2672 let mut ctx = key_store.context();
2673
2674 let mut cipher_view = generate_cipher();
2675 cipher_view
2676 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
2677 .unwrap();
2678
2679 let key_id = cipher_view.key_identifier();
2680 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
2681
2682 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
2683
2684 cipher_view.login.as_mut().unwrap().fido2_credentials =
2685 Some(vec![fido2_credential.clone()]);
2686
2687 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
2688 assert_eq!(decrypted_key_value, "123");
2689 }
2690
2691 #[test]
2692 fn test_password_history_on_password_change() {
2693 use chrono::Utc;
2694
2695 let original_cipher = generate_cipher();
2696 let mut new_cipher = generate_cipher();
2697
2698 if let Some(ref mut login) = new_cipher.login {
2700 login.password = Some("new_password123".to_string());
2701 }
2702
2703 let start = Utc::now();
2704 new_cipher.update_password_history(&original_cipher);
2705 let end = Utc::now();
2706
2707 assert!(new_cipher.password_history.is_some());
2708 let history = new_cipher.password_history.unwrap();
2709 assert_eq!(history.len(), 1);
2710 assert_eq!(history[0].password, "test_password");
2711 assert!(
2712 history[0].last_used_date >= start && history[0].last_used_date <= end,
2713 "last_used_date was not set properly"
2714 );
2715 }
2716
2717 #[test]
2718 fn test_password_history_on_unchanged_password() {
2719 let original_cipher = generate_cipher();
2720 let mut new_cipher = generate_cipher();
2721
2722 new_cipher.update_password_history(&original_cipher);
2723
2724 assert!(
2726 new_cipher.password_history.is_none()
2727 || new_cipher.password_history.as_ref().unwrap().is_empty()
2728 );
2729 }
2730
2731 #[test]
2732 fn test_password_history_is_preserved() {
2733 use chrono::TimeZone;
2734
2735 let mut original_cipher = generate_cipher();
2736 original_cipher.password_history = Some(
2737 (0..4)
2738 .map(|i| PasswordHistoryView {
2739 password: format!("old_password_{}", i),
2740 last_used_date: chrono::Utc
2741 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
2742 .unwrap(),
2743 })
2744 .collect(),
2745 );
2746
2747 let mut new_cipher = generate_cipher();
2748
2749 new_cipher.update_password_history(&original_cipher);
2750
2751 assert!(new_cipher.password_history.is_some());
2752 let history = new_cipher.password_history.unwrap();
2753 assert_eq!(history.len(), 4);
2754
2755 assert_eq!(history[0].password, "old_password_0");
2756 assert_eq!(
2757 history[0].last_used_date,
2758 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
2759 );
2760 assert_eq!(history[1].password, "old_password_1");
2761 assert_eq!(
2762 history[1].last_used_date,
2763 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
2764 );
2765 assert_eq!(history[2].password, "old_password_2");
2766 assert_eq!(
2767 history[2].last_used_date,
2768 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
2769 );
2770 assert_eq!(history[3].password, "old_password_3");
2771 assert_eq!(
2772 history[3].last_used_date,
2773 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
2774 );
2775 }
2776
2777 #[test]
2778 fn test_populate_cipher_types_login_with_valid_data() {
2779 let mut cipher = Cipher {
2780 id: Some(TEST_UUID.parse().unwrap()),
2781 organization_id: None,
2782 folder_id: None,
2783 collection_ids: vec![],
2784 key: None,
2785 name: TEST_CIPHER_NAME.parse().unwrap(),
2786 notes: None,
2787 r#type: CipherType::Login,
2788 login: None,
2789 identity: None,
2790 card: None,
2791 secure_note: None,
2792 ssh_key: None,
2793 bank_account: None,
2794 drivers_license: None,
2795 passport: None,
2796 favorite: false,
2797 reprompt: CipherRepromptType::None,
2798 organization_use_totp: false,
2799 edit: true,
2800 view_password: true,
2801 permissions: None,
2802 local_data: None,
2803 attachments: None,
2804 fields: None,
2805 password_history: None,
2806 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2807 deleted_date: None,
2808 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2809 archived_date: None,
2810 data: Some(format!(
2811 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2812 TEST_ENC_STRING_1, TEST_ENC_STRING_2
2813 )),
2814 };
2815
2816 cipher
2817 .populate_cipher_types()
2818 .expect("populate_cipher_types failed");
2819
2820 assert!(cipher.login.is_some());
2821 let login = cipher.login.unwrap();
2822 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
2823 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
2824 }
2825
2826 #[test]
2827 fn test_populate_cipher_types_secure_note() {
2828 let mut cipher = Cipher {
2829 id: Some(TEST_UUID.parse().unwrap()),
2830 organization_id: None,
2831 folder_id: None,
2832 collection_ids: vec![],
2833 key: None,
2834 name: TEST_CIPHER_NAME.parse().unwrap(),
2835 notes: None,
2836 r#type: CipherType::SecureNote,
2837 login: None,
2838 identity: None,
2839 card: None,
2840 secure_note: None,
2841 ssh_key: None,
2842 bank_account: None,
2843 drivers_license: None,
2844 passport: None,
2845 favorite: false,
2846 reprompt: CipherRepromptType::None,
2847 organization_use_totp: false,
2848 edit: true,
2849 view_password: true,
2850 permissions: None,
2851 local_data: None,
2852 attachments: None,
2853 fields: None,
2854 password_history: None,
2855 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2856 deleted_date: None,
2857 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2858 archived_date: None,
2859 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
2860 };
2861
2862 cipher
2863 .populate_cipher_types()
2864 .expect("populate_cipher_types failed");
2865
2866 assert!(cipher.secure_note.is_some());
2867 }
2868
2869 #[test]
2870 fn test_populate_cipher_types_card() {
2871 let mut cipher = Cipher {
2872 id: Some(TEST_UUID.parse().unwrap()),
2873 organization_id: None,
2874 folder_id: None,
2875 collection_ids: vec![],
2876 key: None,
2877 name: TEST_CIPHER_NAME.parse().unwrap(),
2878 notes: None,
2879 r#type: CipherType::Card,
2880 login: None,
2881 identity: None,
2882 card: None,
2883 secure_note: None,
2884 ssh_key: None,
2885 bank_account: None,
2886 drivers_license: None,
2887 passport: None,
2888 favorite: false,
2889 reprompt: CipherRepromptType::None,
2890 organization_use_totp: false,
2891 edit: true,
2892 view_password: true,
2893 permissions: None,
2894 local_data: None,
2895 attachments: None,
2896 fields: None,
2897 password_history: None,
2898 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2899 deleted_date: None,
2900 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2901 archived_date: None,
2902 data: Some(format!(
2903 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2904 TEST_ENC_STRING_1,
2905 TEST_ENC_STRING_2,
2906 TEST_ENC_STRING_3,
2907 TEST_ENC_STRING_4,
2908 TEST_ENC_STRING_5,
2909 TEST_ENC_STRING_1
2910 )),
2911 };
2912
2913 cipher
2914 .populate_cipher_types()
2915 .expect("populate_cipher_types failed");
2916
2917 assert!(cipher.card.is_some());
2918 let card = cipher.card.unwrap();
2919 assert_eq!(
2920 card.cardholder_name.as_ref().unwrap().to_string(),
2921 TEST_ENC_STRING_1
2922 );
2923 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
2924 assert_eq!(
2925 card.exp_month.as_ref().unwrap().to_string(),
2926 TEST_ENC_STRING_3
2927 );
2928 assert_eq!(
2929 card.exp_year.as_ref().unwrap().to_string(),
2930 TEST_ENC_STRING_4
2931 );
2932 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
2933 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
2934 }
2935
2936 #[test]
2937 fn test_populate_cipher_types_identity() {
2938 let mut cipher = Cipher {
2939 id: Some(TEST_UUID.parse().unwrap()),
2940 organization_id: None,
2941 folder_id: None,
2942 collection_ids: vec![],
2943 key: None,
2944 name: TEST_CIPHER_NAME.parse().unwrap(),
2945 notes: None,
2946 r#type: CipherType::Identity,
2947 login: None,
2948 identity: None,
2949 card: None,
2950 secure_note: None,
2951 ssh_key: None,
2952 bank_account: None,
2953 drivers_license: None,
2954 passport: None,
2955 favorite: false,
2956 reprompt: CipherRepromptType::None,
2957 organization_use_totp: false,
2958 edit: true,
2959 view_password: true,
2960 permissions: None,
2961 local_data: None,
2962 attachments: None,
2963 fields: None,
2964 password_history: None,
2965 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2966 deleted_date: None,
2967 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2968 archived_date: None,
2969 data: Some(format!(
2970 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2971 TEST_ENC_STRING_1,
2972 TEST_ENC_STRING_2,
2973 TEST_ENC_STRING_3,
2974 TEST_ENC_STRING_4,
2975 TEST_ENC_STRING_5,
2976 TEST_ENC_STRING_1,
2977 TEST_ENC_STRING_2,
2978 TEST_ENC_STRING_3,
2979 TEST_ENC_STRING_4,
2980 TEST_ENC_STRING_5
2981 )),
2982 };
2983
2984 cipher
2985 .populate_cipher_types()
2986 .expect("populate_cipher_types failed");
2987
2988 assert!(cipher.identity.is_some());
2989 let identity = cipher.identity.unwrap();
2990 assert_eq!(
2991 identity.first_name.as_ref().unwrap().to_string(),
2992 TEST_ENC_STRING_1
2993 );
2994 assert_eq!(
2995 identity.last_name.as_ref().unwrap().to_string(),
2996 TEST_ENC_STRING_2
2997 );
2998 assert_eq!(
2999 identity.email.as_ref().unwrap().to_string(),
3000 TEST_ENC_STRING_3
3001 );
3002 assert_eq!(
3003 identity.phone.as_ref().unwrap().to_string(),
3004 TEST_ENC_STRING_4
3005 );
3006 assert_eq!(
3007 identity.company.as_ref().unwrap().to_string(),
3008 TEST_ENC_STRING_5
3009 );
3010 assert_eq!(
3011 identity.address1.as_ref().unwrap().to_string(),
3012 TEST_ENC_STRING_1
3013 );
3014 assert_eq!(
3015 identity.city.as_ref().unwrap().to_string(),
3016 TEST_ENC_STRING_2
3017 );
3018 assert_eq!(
3019 identity.state.as_ref().unwrap().to_string(),
3020 TEST_ENC_STRING_3
3021 );
3022 assert_eq!(
3023 identity.postal_code.as_ref().unwrap().to_string(),
3024 TEST_ENC_STRING_4
3025 );
3026 assert_eq!(
3027 identity.country.as_ref().unwrap().to_string(),
3028 TEST_ENC_STRING_5
3029 );
3030 }
3031
3032 #[test]
3033
3034 fn test_password_history_with_hidden_fields() {
3035 let mut original_cipher = generate_cipher();
3036 original_cipher.fields = Some(vec![FieldView {
3037 name: Some("Secret Key".to_string()),
3038 value: Some("old_secret_value".to_string()),
3039 r#type: crate::FieldType::Hidden,
3040 linked_id: None,
3041 }]);
3042
3043 let mut new_cipher = generate_cipher();
3044 new_cipher.fields = Some(vec![FieldView {
3045 name: Some("Secret Key".to_string()),
3046 value: Some("new_secret_value".to_string()),
3047 r#type: crate::FieldType::Hidden,
3048 linked_id: None,
3049 }]);
3050
3051 new_cipher.update_password_history(&original_cipher);
3052
3053 assert!(new_cipher.password_history.is_some());
3054 let history = new_cipher.password_history.unwrap();
3055 assert_eq!(history.len(), 1);
3056 assert_eq!(history[0].password, "Secret Key: old_secret_value");
3057 }
3058
3059 #[test]
3060 fn test_password_history_length_limit() {
3061 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
3062
3063 let mut original_cipher = generate_cipher();
3064 original_cipher.password_history = Some(
3065 (0..10)
3066 .map(|i| PasswordHistoryView {
3067 password: format!("old_password_{}", i),
3068 last_used_date: chrono::Utc::now(),
3069 })
3070 .collect(),
3071 );
3072
3073 let mut new_cipher = original_cipher.clone();
3074 if let Some(ref mut login) = new_cipher.login {
3076 login.password = Some("brand_new_password".to_string());
3077 }
3078
3079 new_cipher.update_password_history(&original_cipher);
3080
3081 assert!(new_cipher.password_history.is_some());
3082 let history = new_cipher.password_history.unwrap();
3083
3084 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
3086
3087 assert_eq!(history[0].password, "test_password");
3089 assert_eq!(history[1].password, "old_password_0");
3091 assert_eq!(history[2].password, "old_password_1");
3092 assert_eq!(history[3].password, "old_password_2");
3093 assert_eq!(history[4].password, "old_password_3");
3094 }
3095
3096 #[test]
3097 fn test_populate_cipher_types_ssh_key() {
3098 let mut cipher = Cipher {
3099 id: Some(TEST_UUID.parse().unwrap()),
3100 organization_id: None,
3101 folder_id: None,
3102 collection_ids: vec![],
3103 key: None,
3104 name: TEST_CIPHER_NAME.parse().unwrap(),
3105 notes: None,
3106 r#type: CipherType::SshKey,
3107 login: None,
3108 identity: None,
3109 card: None,
3110 secure_note: None,
3111 ssh_key: None,
3112 bank_account: None,
3113 drivers_license: None,
3114 passport: None,
3115 favorite: false,
3116 reprompt: CipherRepromptType::None,
3117 organization_use_totp: false,
3118 edit: true,
3119 view_password: true,
3120 permissions: None,
3121 local_data: None,
3122 attachments: None,
3123 fields: None,
3124 password_history: None,
3125 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3126 deleted_date: None,
3127 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3128 archived_date: None,
3129 data: Some(format!(
3130 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3131 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
3132 )),
3133 };
3134
3135 cipher
3136 .populate_cipher_types()
3137 .expect("populate_cipher_types failed");
3138
3139 assert!(cipher.ssh_key.is_some());
3140 let ssh_key = cipher.ssh_key.unwrap();
3141 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
3142 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
3143 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
3144 }
3145
3146 #[test]
3147 fn test_populate_cipher_types_with_null_data() {
3148 let mut cipher = Cipher {
3149 id: Some(TEST_UUID.parse().unwrap()),
3150 organization_id: None,
3151 folder_id: None,
3152 collection_ids: vec![],
3153 key: None,
3154 name: TEST_CIPHER_NAME.parse().unwrap(),
3155 notes: None,
3156 r#type: CipherType::Login,
3157 login: None,
3158 identity: None,
3159 card: None,
3160 secure_note: None,
3161 ssh_key: None,
3162 bank_account: None,
3163 drivers_license: None,
3164 passport: None,
3165 favorite: false,
3166 reprompt: CipherRepromptType::None,
3167 organization_use_totp: false,
3168 edit: true,
3169 view_password: true,
3170 permissions: None,
3171 local_data: None,
3172 attachments: None,
3173 fields: None,
3174 password_history: None,
3175 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3176 deleted_date: None,
3177 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3178 archived_date: None,
3179 data: None,
3180 };
3181
3182 let result = cipher.populate_cipher_types();
3183 assert!(matches!(
3184 result,
3185 Err(VaultParseError::MissingField(MissingFieldError("data")))
3186 ));
3187 }
3188
3189 #[test]
3190 fn test_populate_cipher_types_with_invalid_json() {
3191 let mut cipher = Cipher {
3192 id: Some(TEST_UUID.parse().unwrap()),
3193 organization_id: None,
3194 folder_id: None,
3195 collection_ids: vec![],
3196 key: None,
3197 name: TEST_CIPHER_NAME.parse().unwrap(),
3198 notes: None,
3199 r#type: CipherType::Login,
3200 login: None,
3201 identity: None,
3202 card: None,
3203 secure_note: None,
3204 ssh_key: None,
3205 bank_account: None,
3206 drivers_license: None,
3207 passport: None,
3208 favorite: false,
3209 reprompt: CipherRepromptType::None,
3210 organization_use_totp: false,
3211 edit: true,
3212 view_password: true,
3213 permissions: None,
3214 local_data: None,
3215 attachments: None,
3216 fields: None,
3217 password_history: None,
3218 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3219 deleted_date: None,
3220 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3221 archived_date: None,
3222 data: Some("invalid json".to_string()),
3223 };
3224
3225 let result = cipher.populate_cipher_types();
3226
3227 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
3228 }
3229
3230 #[test]
3231 fn test_decrypt_cipher_with_mixed_attachments() {
3232 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
3233 let key_store = create_test_crypto_with_user_key(user_key);
3234
3235 let mut ctx = key_store.context();
3237 let valid1 = "valid_file_1.txt"
3238 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3239 .unwrap();
3240 let valid2 = "valid_file_2.txt"
3241 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3242 .unwrap();
3243
3244 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
3246 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
3247 let mut wrong_ctx = wrong_key_store.context();
3248 let corrupted = "corrupted_file.txt"
3249 .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
3250 .unwrap();
3251
3252 let cipher = Cipher {
3253 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
3254 organization_id: None,
3255 folder_id: None,
3256 collection_ids: vec![],
3257 key: None,
3258 name: TEST_CIPHER_NAME.parse().unwrap(),
3259 notes: None,
3260 r#type: CipherType::Login,
3261 login: None,
3262 identity: None,
3263 card: None,
3264 secure_note: None,
3265 ssh_key: None,
3266 bank_account: None,
3267 drivers_license: None,
3268 passport: None,
3269 favorite: false,
3270 reprompt: CipherRepromptType::None,
3271 organization_use_totp: false,
3272 edit: true,
3273 permissions: None,
3274 view_password: true,
3275 local_data: None,
3276 attachments: Some(vec![
3277 attachment::Attachment {
3279 id: Some("valid-attachment".to_string()),
3280 url: Some("https://example.com/valid".to_string()),
3281 size: Some("100".to_string()),
3282 size_name: Some("100 Bytes".to_string()),
3283 file_name: Some(valid1),
3284 key: None,
3285 },
3286 attachment::Attachment {
3288 id: Some("corrupted-attachment".to_string()),
3289 url: Some("https://example.com/corrupted".to_string()),
3290 size: Some("200".to_string()),
3291 size_name: Some("200 Bytes".to_string()),
3292 file_name: Some(corrupted),
3293 key: None,
3294 },
3295 attachment::Attachment {
3297 id: Some("valid-attachment-2".to_string()),
3298 url: Some("https://example.com/valid2".to_string()),
3299 size: Some("150".to_string()),
3300 size_name: Some("150 Bytes".to_string()),
3301 file_name: Some(valid2),
3302 key: None,
3303 },
3304 ]),
3305 fields: None,
3306 password_history: None,
3307 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3308 deleted_date: None,
3309 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3310 archived_date: None,
3311 data: None,
3312 };
3313
3314 let view: CipherView = key_store.decrypt(&cipher).unwrap();
3315
3316 assert!(view.attachments.is_some());
3318 let successes = view.attachments.as_ref().unwrap();
3319 assert_eq!(successes.len(), 2);
3320 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
3321 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
3322
3323 assert!(view.attachment_decryption_failures.is_some());
3325 let failures = view.attachment_decryption_failures.as_ref().unwrap();
3326 assert_eq!(failures.len(), 1);
3327 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
3328 assert_eq!(failures[0].file_name, None);
3329 }
3330
3331 #[test]
3332 fn test_decrypt_cipher_list_view_passport() {
3333 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3334 SymmetricKeyAlgorithm::Aes256CbcHmac,
3335 ));
3336
3337 let cipher_view = CipherView {
3338 r#type: CipherType::Passport,
3339 passport: Some(passport::PassportView {
3340 given_name: Some("Jane".to_string()),
3341 surname: Some("Doe".to_string()),
3342 passport_number: Some("P12345678".to_string()),
3343 ..Default::default()
3344 }),
3345 login: None,
3346 ..generate_cipher()
3347 };
3348
3349 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
3350 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3351
3352 assert_eq!(list_view.r#type, CipherListViewType::Passport);
3353 assert_eq!(list_view.subtitle, "Jane Doe");
3354 assert_eq!(
3355 list_view.copyable_fields,
3356 vec![
3357 CopyableCipherFields::PassportGivenName,
3358 CopyableCipherFields::PassportSurname,
3359 CopyableCipherFields::PassportPassportNumber,
3360 ]
3361 );
3362 }
3363
3364 #[test]
3365 fn test_decrypt_cipher_list_view_drivers_license() {
3366 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3367 SymmetricKeyAlgorithm::Aes256CbcHmac,
3368 ));
3369
3370 let cipher_view = CipherView {
3371 r#type: CipherType::DriversLicense,
3372 drivers_license: Some(drivers_license::DriversLicenseView {
3373 first_name: Some("John".to_string()),
3374 last_name: Some("Doe".to_string()),
3375 license_number: Some("DL-987654".to_string()),
3376 ..Default::default()
3377 }),
3378 login: None,
3379 ..generate_cipher()
3380 };
3381
3382 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
3383 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3384
3385 assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
3386 assert_eq!(list_view.subtitle, "John Doe");
3387 assert_eq!(
3388 list_view.copyable_fields,
3389 vec![
3390 CopyableCipherFields::DriversLicenseFirstName,
3391 CopyableCipherFields::DriversLicenseLastName,
3392 CopyableCipherFields::DriversLicenseLicenseNumber,
3393 ]
3394 );
3395 }
3396
3397 #[test]
3398 fn test_cipher_view_encrypt_decrypt_passport() {
3399 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3400 SymmetricKeyAlgorithm::Aes256CbcHmac,
3401 ));
3402
3403 let passport = passport::PassportView {
3404 given_name: Some("Jane".to_string()),
3405 surname: Some("Doe".to_string()),
3406 date_of_birth: Some("1990-01-01".to_string()),
3407 sex: Some("F".to_string()),
3408 birth_place: Some("New York".to_string()),
3409 nationality: Some("American".to_string()),
3410 issuing_country: Some("US".to_string()),
3411 passport_number: Some("P12345678".to_string()),
3412 passport_type: Some("P".to_string()),
3413 national_identification_number: Some("123-45-6789".to_string()),
3414 issuing_authority: Some("US State Department".to_string()),
3415 issue_date: Some("2020-01-01".to_string()),
3416 expiration_date: Some("2030-01-01".to_string()),
3417 };
3418
3419 let cipher_view = CipherView {
3420 r#type: CipherType::Passport,
3421 passport: Some(passport.clone()),
3422 login: None,
3423 ..generate_cipher()
3424 };
3425
3426 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3427 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3428
3429 assert_eq!(decrypted.r#type, CipherType::Passport);
3430 assert_eq!(decrypted.passport, Some(passport));
3431 assert!(decrypted.login.is_none());
3432 }
3433
3434 #[test]
3435 fn test_cipher_view_encrypt_decrypt_drivers_license() {
3436 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3437 SymmetricKeyAlgorithm::Aes256CbcHmac,
3438 ));
3439
3440 let dl = drivers_license::DriversLicenseView {
3441 first_name: Some("John".to_string()),
3442 middle_name: Some("Michael".to_string()),
3443 last_name: Some("Doe".to_string()),
3444 date_of_birth: Some("1985-06-15".to_string()),
3445 license_number: Some("DL-987654".to_string()),
3446 issuing_country: Some("US".to_string()),
3447 issuing_state: Some("NY".to_string()),
3448 issue_date: Some("2020-01-01".to_string()),
3449 expiration_date: Some("2028-01-01".to_string()),
3450 issuing_authority: Some("NY DMV".to_string()),
3451 license_class: Some("D".to_string()),
3452 };
3453
3454 let cipher_view = CipherView {
3455 r#type: CipherType::DriversLicense,
3456 drivers_license: Some(dl.clone()),
3457 login: None,
3458 ..generate_cipher()
3459 };
3460
3461 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3462 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3463
3464 assert_eq!(decrypted.r#type, CipherType::DriversLicense);
3465 assert_eq!(decrypted.drivers_license, Some(dl));
3466 assert!(decrypted.login.is_none());
3467 }
3468
3469 #[test]
3470 fn test_mini_response_model_view_password_defaults_to_true() {
3471 use chrono::Utc;
3472
3473 let mini_response = CipherMiniResponseModel {
3476 id: Some(TEST_UUID.parse().unwrap()),
3477 name: Some(TEST_CIPHER_NAME.to_string()),
3478 r#type: Some(bitwarden_api_api::models::CipherType::Login),
3479 creation_date: Some(Utc::now().to_rfc3339()),
3480 revision_date: Some(Utc::now().to_rfc3339()),
3481 ..Default::default()
3482 };
3483
3484 let cipher = mini_response.merge_with_cipher(None).unwrap();
3485 assert!(
3486 cipher.view_password,
3487 "view_password should default to true for CipherMiniResponseModel"
3488 );
3489
3490 let mini_details_response = CipherMiniDetailsResponseModel {
3492 id: Some(TEST_UUID.parse().unwrap()),
3493 name: Some(TEST_CIPHER_NAME.to_string()),
3494 r#type: Some(bitwarden_api_api::models::CipherType::Login),
3495 creation_date: Some(Utc::now().to_rfc3339()),
3496 revision_date: Some(Utc::now().to_rfc3339()),
3497 ..Default::default()
3498 };
3499
3500 let cipher = mini_details_response.merge_with_cipher(None).unwrap();
3501 assert!(
3502 cipher.view_password,
3503 "view_password should default to true for CipherMiniDetailsResponseModel"
3504 );
3505 }
3506
3507 mod cipher_decrypt_dispatch {
3510 use bitwarden_crypto::KeyStore;
3511
3512 use super::*;
3513 use crate::{
3514 BankAccountView, CardView, DriversLicenseView, IdentityView, PassportView,
3515 SecureNoteType, SecureNoteView, SshKeyView, cipher::blob::encrypt_blob_cipher,
3516 };
3517
3518 fn make_key_store() -> KeyStore<KeySlotIds> {
3519 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3520 SymmetricKeyAlgorithm::Aes256CbcHmac,
3521 ))
3522 }
3523
3524 fn encrypt_legacy(view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
3526 key_store.encrypt(view).unwrap()
3527 }
3528
3529 fn encrypt_blob(mut view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
3531 let mut ctx = key_store.context_mut();
3532 encrypt_blob_cipher(&mut view, &mut ctx).unwrap()
3533 }
3534
3535 fn base_login_view() -> CipherView {
3536 let mut view = generate_cipher();
3537 view.name = "Test Login".to_string();
3538 view.login = Some(LoginView {
3539 username: Some("[email protected]".to_string()),
3540 password: Some("hunter2".to_string()),
3541 password_revision_date: None,
3542 uris: None,
3543 totp: Some("otpauth://totp/test?secret=SECRET".to_string()),
3544 autofill_on_page_load: None,
3545 fido2_credentials: None,
3546 });
3547 view
3548 }
3549
3550 #[test]
3552 fn dispatches_blob_to_cipher_view() {
3553 let key_store = make_key_store();
3554 let cipher = encrypt_blob(base_login_view(), &key_store);
3555
3556 let view: CipherView = key_store.decrypt(&cipher).unwrap();
3557
3558 assert_eq!(view.name, "Test Login");
3559 let login = view.login.expect("blob decrypt should restore login");
3560 assert_eq!(login.username.as_deref(), Some("[email protected]"));
3561 assert_eq!(login.password.as_deref(), Some("hunter2"));
3562 }
3563
3564 #[test]
3567 fn dispatches_legacy_to_cipher_view() {
3568 let key_store = make_key_store();
3569
3570 let cipher = encrypt_legacy(base_login_view(), &key_store);
3572 let view: CipherView = key_store.decrypt(&cipher).unwrap();
3573 assert_eq!(view.name, "Test Login");
3574 assert_eq!(
3575 view.login.unwrap().username.as_deref(),
3576 Some("[email protected]"),
3577 );
3578
3579 let cipher = encrypt_legacy(base_login_view(), &key_store);
3581 let view: CipherView = key_store.decrypt(&StrictDecrypt(cipher)).unwrap();
3582 assert_eq!(view.name, "Test Login");
3583 assert_eq!(
3584 view.login.unwrap().username.as_deref(),
3585 Some("[email protected]"),
3586 );
3587 }
3588
3589 #[test]
3594 fn blob_to_list_view_per_type() {
3595 let key_store = make_key_store();
3596
3597 {
3599 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
3600 assert_eq!(list_view.name, "Test Login");
3601 assert_eq!(list_view.subtitle, "[email protected]");
3602 assert!(matches!(list_view.r#type, CipherListViewType::Login(_)));
3603 assert!(
3604 list_view
3605 .copyable_fields
3606 .contains(&CopyableCipherFields::LoginUsername)
3607 );
3608 assert!(
3609 list_view
3610 .copyable_fields
3611 .contains(&CopyableCipherFields::LoginPassword)
3612 );
3613 assert!(
3614 list_view
3615 .copyable_fields
3616 .contains(&CopyableCipherFields::LoginTotp)
3617 );
3618 }
3619
3620 {
3622 let mut view = generate_cipher();
3623 view.r#type = CipherType::Card;
3624 view.login = None;
3625 view.name = "My Card".to_string();
3626 view.card = Some(CardView {
3627 cardholder_name: Some("John Doe".to_string()),
3628 exp_month: Some("12".to_string()),
3629 exp_year: Some("2030".to_string()),
3630 code: Some("123".to_string()),
3631 brand: Some("Visa".to_string()),
3632 number: Some("4111111111111111".to_string()),
3633 });
3634 let list_view = decrypt_blob_list_view(&key_store, view);
3635 assert_eq!(list_view.name, "My Card");
3636 assert!(list_view.subtitle.contains("Visa"));
3637 assert!(list_view.subtitle.contains("1111"));
3638 match &list_view.r#type {
3639 CipherListViewType::Card(card) => {
3640 assert_eq!(card.brand.as_deref(), Some("Visa"))
3641 }
3642 other => panic!("expected Card, got {other:?}"),
3643 }
3644 assert!(
3645 list_view
3646 .copyable_fields
3647 .contains(&CopyableCipherFields::CardNumber)
3648 );
3649 assert!(
3650 list_view
3651 .copyable_fields
3652 .contains(&CopyableCipherFields::CardSecurityCode)
3653 );
3654 }
3655
3656 {
3658 let mut view = generate_cipher();
3659 view.r#type = CipherType::Identity;
3660 view.login = None;
3661 view.name = "My Identity".to_string();
3662 view.identity = Some(IdentityView {
3663 title: None,
3664 first_name: Some("Jane".to_string()),
3665 middle_name: None,
3666 last_name: Some("Doe".to_string()),
3667 address1: Some("123 Main St".to_string()),
3668 address2: None,
3669 address3: None,
3670 city: None,
3671 state: None,
3672 postal_code: None,
3673 country: None,
3674 company: None,
3675 email: Some("[email protected]".to_string()),
3676 phone: None,
3677 ssn: None,
3678 username: None,
3679 passport_number: None,
3680 license_number: None,
3681 });
3682 let list_view = decrypt_blob_list_view(&key_store, view);
3683 assert_eq!(list_view.name, "My Identity");
3684 assert!(list_view.subtitle.contains("Jane"));
3685 assert!(list_view.subtitle.contains("Doe"));
3686 assert!(matches!(list_view.r#type, CipherListViewType::Identity));
3687 assert!(
3688 list_view
3689 .copyable_fields
3690 .contains(&CopyableCipherFields::IdentityEmail)
3691 );
3692 assert!(
3693 list_view
3694 .copyable_fields
3695 .contains(&CopyableCipherFields::IdentityAddress)
3696 );
3697 }
3698
3699 {
3701 let mut view = generate_cipher();
3702 view.r#type = CipherType::SecureNote;
3703 view.login = None;
3704 view.name = "My Note".to_string();
3705 view.notes = Some("secret".to_string());
3706 view.secure_note = Some(SecureNoteView {
3707 r#type: SecureNoteType::Generic,
3708 });
3709 let list_view = decrypt_blob_list_view(&key_store, view);
3710 assert_eq!(list_view.name, "My Note");
3711 assert_eq!(list_view.subtitle, "");
3712 assert!(matches!(list_view.r#type, CipherListViewType::SecureNote));
3713 assert!(
3714 list_view
3715 .copyable_fields
3716 .contains(&CopyableCipherFields::SecureNotes)
3717 );
3718 }
3719
3720 {
3722 let mut view = generate_cipher();
3723 view.r#type = CipherType::SshKey;
3724 view.login = None;
3725 view.name = "My SSH".to_string();
3726 view.ssh_key = Some(SshKeyView {
3727 private_key: "-----BEGIN PRIVATE KEY-----".to_string(),
3728 public_key: "ssh-ed25519 AAAA".to_string(),
3729 fingerprint: "SHA256:abcdef".to_string(),
3730 });
3731 let list_view = decrypt_blob_list_view(&key_store, view);
3732 assert_eq!(list_view.name, "My SSH");
3733 assert_eq!(list_view.subtitle, "SHA256:abcdef");
3734 assert!(matches!(list_view.r#type, CipherListViewType::SshKey));
3735 assert!(
3736 list_view
3737 .copyable_fields
3738 .contains(&CopyableCipherFields::SshKey)
3739 );
3740 }
3741
3742 {
3744 let mut view = generate_cipher();
3745 view.r#type = CipherType::BankAccount;
3746 view.login = None;
3747 view.name = "My Bank Account".to_string();
3748 view.bank_account = Some(BankAccountView {
3749 bank_name: Some("Some Bank".to_string()),
3750 name_on_account: Some("Jane Doe".to_string()),
3751 account_number: Some("123456".to_string()),
3752 routing_number: Some("111000025".to_string()),
3753 branch_number: Some("001".to_string()),
3754 pin: Some("4321".to_string()),
3755 swift_code: Some("ABCDEF12".to_string()),
3756 iban: Some("DE89370400440532013000".to_string()),
3757 ..Default::default()
3758 });
3759 let list_view = decrypt_blob_list_view(&key_store, view);
3760 assert_eq!(list_view.name, "My Bank Account");
3761 assert_eq!(list_view.subtitle, "Some Bank");
3762 assert!(matches!(list_view.r#type, CipherListViewType::BankAccount));
3763 assert_eq!(
3764 list_view.copyable_fields,
3765 vec![
3766 CopyableCipherFields::BankAccountNameOnAccount,
3767 CopyableCipherFields::BankAccountAccountNumber,
3768 CopyableCipherFields::BankAccountRoutingNumber,
3769 CopyableCipherFields::BankAccountBranchNumber,
3770 CopyableCipherFields::BankAccountPin,
3771 CopyableCipherFields::BankAccountIban,
3772 CopyableCipherFields::BankAccountSwift,
3773 ]
3774 );
3775 }
3776 }
3777
3778 fn fully_populated_views() -> Vec<(&'static str, CipherView)> {
3785 let with_type = |r#type: CipherType, f: &dyn Fn(&mut CipherView)| {
3786 let mut view = generate_cipher();
3787 view.r#type = r#type;
3788 view.login = None;
3789 f(&mut view);
3790 view
3791 };
3792
3793 vec![
3794 ("Login", base_login_view()),
3795 (
3796 "Card",
3797 with_type(CipherType::Card, &|v| {
3798 v.card = Some(CardView {
3799 cardholder_name: Some("Jane Doe".to_string()),
3800 exp_month: Some("12".to_string()),
3801 exp_year: Some("2030".to_string()),
3802 code: Some("123".to_string()),
3803 brand: Some("Visa".to_string()),
3804 number: Some("4111111111111111".to_string()),
3805 });
3806 }),
3807 ),
3808 (
3809 "Identity",
3810 with_type(CipherType::Identity, &|v| {
3811 v.identity = Some(IdentityView {
3812 title: Some("Mx".to_string()),
3813 first_name: Some("Jane".to_string()),
3814 middle_name: Some("Q".to_string()),
3815 last_name: Some("Doe".to_string()),
3816 address1: Some("1 Main St".to_string()),
3817 address2: Some("Apt 2".to_string()),
3818 address3: Some("Floor 3".to_string()),
3819 city: Some("Anytown".to_string()),
3820 state: Some("CA".to_string()),
3821 postal_code: Some("90210".to_string()),
3822 country: Some("US".to_string()),
3823 company: Some("Acme".to_string()),
3824 email: Some("[email protected]".to_string()),
3825 phone: Some("555-0100".to_string()),
3826 ssn: Some("000-00-0000".to_string()),
3827 username: Some("jane".to_string()),
3828 passport_number: Some("X1234567".to_string()),
3829 license_number: Some("D1234567".to_string()),
3830 });
3831 }),
3832 ),
3833 (
3834 "SecureNote",
3835 with_type(CipherType::SecureNote, &|v| {
3836 v.notes = Some("a secret note".to_string());
3837 v.secure_note = Some(SecureNoteView {
3838 r#type: SecureNoteType::Generic,
3839 });
3840 }),
3841 ),
3842 (
3843 "SshKey",
3844 with_type(CipherType::SshKey, &|v| {
3845 v.ssh_key = Some(SshKeyView {
3846 private_key: "private".to_string(),
3847 public_key: "public".to_string(),
3848 fingerprint: "SHA256:abc".to_string(),
3849 });
3850 }),
3851 ),
3852 (
3853 "BankAccount",
3854 with_type(CipherType::BankAccount, &|v| {
3855 v.bank_account = Some(BankAccountView {
3856 bank_name: Some("Some Bank".to_string()),
3857 name_on_account: Some("Jane Doe".to_string()),
3858 account_type: Some("Checking".to_string()),
3859 account_number: Some("123456".to_string()),
3860 routing_number: Some("111000025".to_string()),
3861 branch_number: Some("001".to_string()),
3862 pin: Some("4321".to_string()),
3863 swift_code: Some("ABCDEF12".to_string()),
3864 iban: Some("DE89370400440532013000".to_string()),
3865 bank_contact_phone: Some("555-0199".to_string()),
3866 });
3867 }),
3868 ),
3869 (
3870 "DriversLicense",
3871 with_type(CipherType::DriversLicense, &|v| {
3872 v.drivers_license = Some(DriversLicenseView {
3873 first_name: Some("Jane".to_string()),
3874 middle_name: Some("Q".to_string()),
3875 last_name: Some("Doe".to_string()),
3876 date_of_birth: Some("1990-01-01".to_string()),
3877 license_number: Some("D1234567".to_string()),
3878 issuing_country: Some("US".to_string()),
3879 issuing_state: Some("CA".to_string()),
3880 issue_date: Some("2020-01-01".to_string()),
3881 expiration_date: Some("2030-01-01".to_string()),
3882 issuing_authority: Some("DMV".to_string()),
3883 license_class: Some("C".to_string()),
3884 });
3885 }),
3886 ),
3887 (
3888 "Passport",
3889 with_type(CipherType::Passport, &|v| {
3890 v.passport = Some(PassportView {
3891 surname: Some("Doe".to_string()),
3892 given_name: Some("Jane".to_string()),
3893 date_of_birth: Some("1990-01-01".to_string()),
3894 sex: Some("F".to_string()),
3895 birth_place: Some("Anytown".to_string()),
3896 nationality: Some("US".to_string()),
3897 issuing_country: Some("US".to_string()),
3898 passport_number: Some("X1234567".to_string()),
3899 passport_type: Some("P".to_string()),
3900 national_identification_number: Some("000-00-0000".to_string()),
3901 issuing_authority: Some("State Dept".to_string()),
3902 issue_date: Some("2020-01-01".to_string()),
3903 expiration_date: Some("2030-01-01".to_string()),
3904 });
3905 }),
3906 ),
3907 ]
3908 }
3909
3910 #[test]
3917 fn copyable_fields_parity_between_legacy_and_blob() {
3918 let key_store = make_key_store();
3919
3920 for (label, view) in fully_populated_views() {
3921 let legacy: CipherListView = key_store
3922 .decrypt(&encrypt_legacy(view.clone(), &key_store))
3923 .unwrap();
3924 let blob = decrypt_blob_list_view(&key_store, view);
3925
3926 assert_eq!(
3927 legacy.copyable_fields, blob.copyable_fields,
3928 "copyable_fields diverged between legacy and blob paths for {label}",
3929 );
3930 }
3931 }
3932
3933 #[test]
3937 fn login_list_view_preserves_totp_round_trip() {
3938 let key_store = make_key_store();
3939 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
3940
3941 match &list_view.r#type {
3942 CipherListViewType::Login(login) => assert!(login.totp.is_some()),
3943 other => panic!("expected Login, got {other:?}"),
3944 }
3945 let totp = list_view.get_totp_key(&mut key_store.context()).unwrap();
3946 assert_eq!(totp.as_deref(), Some("otpauth://totp/test?secret=SECRET"));
3947 }
3948
3949 #[test]
3951 fn mixed_batch_decrypt_list() {
3952 let key_store = make_key_store();
3953 let blob = encrypt_blob(base_login_view(), &key_store);
3954 let legacy = encrypt_legacy(base_login_view(), &key_store);
3955
3956 let ciphers = vec![blob, legacy];
3957 let views: Vec<CipherListView> = key_store.decrypt_list(&ciphers).unwrap();
3958
3959 assert_eq!(views.len(), 2);
3960 for v in &views {
3961 assert_eq!(v.name, "Test Login");
3962 assert_eq!(v.subtitle, "[email protected]");
3963 }
3964 }
3965
3966 fn decrypt_blob_list_view(
3967 key_store: &KeyStore<KeySlotIds>,
3968 view: CipherView,
3969 ) -> CipherListView {
3970 let cipher = encrypt_blob(view, key_store);
3971 key_store.decrypt(&cipher).unwrap()
3972 }
3973
3974 fn failing_attachments() -> Vec<attachment::Attachment> {
3978 let wrong = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3979 SymmetricKeyAlgorithm::Aes256CbcHmac,
3980 ));
3981 let mut ctx = wrong.context();
3982 let mut enc = |s: &str| s.encrypt(&mut ctx, SymmetricKeySlotId::User).unwrap();
3983 vec![
3984 attachment::Attachment {
3985 id: Some("a1".to_string()),
3986 url: None,
3987 size: None,
3988 size_name: None,
3989 file_name: Some(enc("a1.txt")),
3990 key: Some(enc("k1")),
3991 },
3992 attachment::Attachment {
3993 id: Some("a2-old".to_string()),
3994 url: None,
3995 size: None,
3996 size_name: None,
3997 file_name: Some(enc("a2.txt")),
3998 key: None,
3999 },
4000 attachment::Attachment {
4001 id: Some("a3".to_string()),
4002 url: None,
4003 size: None,
4004 size_name: None,
4005 file_name: Some(enc("a3.txt")),
4006 key: Some(enc("k3")),
4007 },
4008 ]
4009 }
4010
4011 #[test]
4018 fn attachment_metrics_parity_with_failing_attachments() {
4019 let key_store = make_key_store();
4020
4021 let mut legacy = encrypt_legacy(base_login_view(), &key_store);
4022 legacy.attachments = Some(failing_attachments());
4023 let legacy_list: CipherListView = key_store.decrypt(&legacy).unwrap();
4024
4025 let mut blob = encrypt_blob(base_login_view(), &key_store);
4026 blob.attachments = Some(failing_attachments());
4027 let blob_list: CipherListView = key_store.decrypt(&blob).unwrap();
4028
4029 assert_eq!(legacy_list.attachments, 3);
4030 assert!(legacy_list.has_old_attachments);
4031 assert_eq!(blob_list.attachments, legacy_list.attachments);
4032 assert_eq!(
4033 blob_list.has_old_attachments,
4034 legacy_list.has_old_attachments,
4035 );
4036 }
4037 }
4038}