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