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