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