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