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