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,
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;
22use tracing::instrument;
23#[cfg(feature = "wasm")]
24use tsify::Tsify;
25#[cfg(feature = "wasm")]
26use wasm_bindgen::prelude::wasm_bindgen;
27
28use super::{
29 attachment, bank_account, card,
30 card::CardListView,
31 cipher_permissions::CipherPermissions,
32 drivers_license, field, identity,
33 local_data::{LocalData, LocalDataView},
34 login::LoginListView,
35 passport, secure_note, ssh_key,
36};
37use crate::{
38 AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView,
39 FieldView, FolderId, Login, LoginView, VaultParseError,
40 password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
41};
42
43uuid_newtype!(pub CipherId);
44
45#[allow(missing_docs)]
46#[bitwarden_error(flat)]
47#[derive(Debug, Error)]
48pub enum CipherError {
49 #[error(transparent)]
50 MissingField(#[from] MissingFieldError),
51 #[error(transparent)]
52 Crypto(#[from] CryptoError),
53 #[error(transparent)]
54 Decrypt(#[from] DecryptError),
55 #[error(transparent)]
56 Encrypt(#[from] EncryptError),
57 #[error(
58 "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
59 )]
60 AttachmentsWithoutKeys,
61 #[error("This cipher cannot be moved to the specified organization")]
62 OrganizationAlreadySet,
63 #[error(transparent)]
64 Repository(#[from] RepositoryError),
65 #[error(transparent)]
66 Chrono(#[from] chrono::ParseError),
67 #[error(transparent)]
68 SerdeJson(#[from] serde_json::Error),
69 #[error(transparent)]
70 Api(#[from] ApiError),
71}
72
73impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherError {
74 fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
75 Self::Api(value.into())
76 }
77}
78
79pub(super) trait CipherKind {
81 fn decrypt_subtitle(
83 &self,
84 ctx: &mut KeyStoreContext<KeySlotIds>,
85 key: SymmetricKeySlotId,
86 ) -> Result<String, CryptoError>;
87
88 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
90}
91
92#[allow(missing_docs)]
93#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
94#[repr(u8)]
95#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
96#[cfg_attr(feature = "wasm", wasm_bindgen)]
97pub enum CipherType {
98 Login = 1,
99 SecureNote = 2,
100 Card = 3,
101 Identity = 4,
102 SshKey = 5,
103 BankAccount = 6,
104 DriversLicense = 7,
105 Passport = 8,
106}
107
108#[allow(missing_docs)]
109#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
110#[repr(u8)]
111#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
112#[cfg_attr(feature = "wasm", wasm_bindgen)]
113pub enum CipherRepromptType {
114 #[default]
115 None = 0,
116 Password = 1,
117}
118
119#[allow(missing_docs)]
120#[derive(Serialize, Deserialize, Debug, Clone)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
123#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
124pub struct EncryptionContext {
125 pub encrypted_for: UserId,
128 pub cipher: Cipher,
129}
130
131impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
132 type Error = CipherError;
133 fn try_from(
134 EncryptionContext {
135 cipher,
136 encrypted_for,
137 }: EncryptionContext,
138 ) -> Result<Self, Self::Error> {
139 Ok(Self {
140 id: require!(cipher.id).into(),
141 encrypted_for: Some(encrypted_for.into()),
142 r#type: Some(cipher.r#type.into()),
143 organization_id: cipher.organization_id.map(|o| o.to_string()),
144 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
145 favorite: cipher.favorite.into(),
146 reprompt: Some(cipher.reprompt.into()),
147 key: cipher.key.map(|k| k.to_string()),
148 name: cipher.name.to_string(),
149 notes: cipher.notes.map(|n| n.to_string()),
150 fields: Some(
151 cipher
152 .fields
153 .into_iter()
154 .flatten()
155 .map(Into::into)
156 .collect(),
157 ),
158 password_history: Some(
159 cipher
160 .password_history
161 .into_iter()
162 .flatten()
163 .map(Into::into)
164 .collect(),
165 ),
166 attachments: None,
167 attachments2: Some(
168 cipher
169 .attachments
170 .into_iter()
171 .flatten()
172 .filter_map(|a| {
173 a.id.map(|id| {
174 (
175 id,
176 bitwarden_api_api::models::CipherAttachmentModel {
177 file_name: a.file_name.map(|n| n.to_string()),
178 key: a.key.map(|k| k.to_string()),
179 },
180 )
181 })
182 })
183 .collect(),
184 ),
185 login: cipher.login.map(|l| Box::new(l.into())),
186 card: cipher.card.map(|c| Box::new(c.into())),
187 identity: cipher.identity.map(|i| Box::new(i.into())),
188 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
189 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
190 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
191 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
192 passport: cipher.passport.map(|p| Box::new(p.into())),
193 data: None, last_known_revision_date: Some(
195 cipher
196 .revision_date
197 .to_rfc3339_opts(SecondsFormat::Millis, true),
198 ),
199 archived_date: cipher
200 .archived_date
201 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
202 })
203 }
204}
205
206impl From<EncryptionContext> for CipherRequestModel {
207 fn from(
208 EncryptionContext {
209 cipher,
210 encrypted_for,
211 }: EncryptionContext,
212 ) -> Self {
213 Self {
214 encrypted_for: Some(encrypted_for.into()),
215 r#type: Some(cipher.r#type.into()),
216 organization_id: cipher.organization_id.map(|o| o.to_string()),
217 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
218 favorite: cipher.favorite.into(),
219 reprompt: Some(cipher.reprompt.into()),
220 key: cipher.key.map(|k| k.to_string()),
221 name: cipher.name.to_string(),
222 notes: cipher.notes.map(|n| n.to_string()),
223 fields: Some(
224 cipher
225 .fields
226 .into_iter()
227 .flatten()
228 .map(Into::into)
229 .collect(),
230 ),
231 password_history: Some(
232 cipher
233 .password_history
234 .into_iter()
235 .flatten()
236 .map(Into::into)
237 .collect(),
238 ),
239 attachments: None,
240 attachments2: Some(
241 cipher
242 .attachments
243 .into_iter()
244 .flatten()
245 .filter_map(|a| {
246 a.id.map(|id| {
247 (
248 id,
249 bitwarden_api_api::models::CipherAttachmentModel {
250 file_name: a.file_name.map(|n| n.to_string()),
251 key: a.key.map(|k| k.to_string()),
252 },
253 )
254 })
255 })
256 .collect(),
257 ),
258 login: cipher.login.map(|l| Box::new(l.into())),
259 card: cipher.card.map(|c| Box::new(c.into())),
260 identity: cipher.identity.map(|i| Box::new(i.into())),
261 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
262 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
263 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
264 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
265 passport: cipher.passport.map(|p| Box::new(p.into())),
266 data: None, last_known_revision_date: Some(
268 cipher
269 .revision_date
270 .to_rfc3339_opts(SecondsFormat::Millis, true),
271 ),
272 archived_date: cipher
273 .archived_date
274 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
275 }
276 }
277}
278
279#[allow(missing_docs)]
280#[derive(Serialize, Deserialize, Debug, Clone)]
281#[serde(rename_all = "camelCase", deny_unknown_fields)]
282#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
283#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
284pub struct Cipher {
285 pub id: Option<CipherId>,
286 pub organization_id: Option<OrganizationId>,
287 pub folder_id: Option<FolderId>,
288 pub collection_ids: Vec<CollectionId>,
289 pub key: Option<EncString>,
292
293 pub name: EncString,
294 pub notes: Option<EncString>,
295
296 pub r#type: CipherType,
297 pub login: Option<Login>,
298 pub identity: Option<identity::Identity>,
299 pub card: Option<card::Card>,
300 pub secure_note: Option<secure_note::SecureNote>,
301 pub ssh_key: Option<ssh_key::SshKey>,
302 pub bank_account: Option<bank_account::BankAccount>,
303 pub drivers_license: Option<drivers_license::DriversLicense>,
304 pub passport: Option<passport::Passport>,
305
306 pub favorite: bool,
307 pub reprompt: CipherRepromptType,
308 pub organization_use_totp: bool,
309 pub edit: bool,
310 pub permissions: Option<CipherPermissions>,
311 pub view_password: bool,
312 pub local_data: Option<LocalData>,
313
314 pub attachments: Option<Vec<attachment::Attachment>>,
315 pub fields: Option<Vec<field::Field>>,
316 pub password_history: Option<Vec<password_history::PasswordHistory>>,
317
318 pub creation_date: DateTime<Utc>,
319 pub deleted_date: Option<DateTime<Utc>>,
320 pub revision_date: DateTime<Utc>,
321 pub archived_date: Option<DateTime<Utc>>,
322 pub data: Option<String>,
323}
324
325pub enum CipherKeyRewrapError {
328 NoCipherKey,
329 DecryptionFailure,
330 EncryptionFailure,
331}
332
333impl Cipher {
334 pub fn rewrap_cipher_key(
339 &mut self,
340 old_key: SymmetricKeySlotId,
341 new_key: SymmetricKeySlotId,
342 ctx: &mut KeyStoreContext<KeySlotIds>,
343 ) -> Result<(), CipherKeyRewrapError> {
344 let new_cipher_key = self
345 .key
346 .as_ref()
347 .ok_or(CipherKeyRewrapError::NoCipherKey)
348 .and_then(|wrapped_cipher_key| {
349 ctx.unwrap_symmetric_key(old_key, wrapped_cipher_key)
350 .map_err(|_| CipherKeyRewrapError::DecryptionFailure)
351 })
352 .and_then(|cipher_key| {
353 ctx.wrap_symmetric_key(new_key, cipher_key)
354 .map_err(|_| CipherKeyRewrapError::EncryptionFailure)
355 })?;
356 self.key = Some(new_cipher_key);
357 Ok(())
358 }
359}
360
361bitwarden_state::register_repository_item!(CipherId => Cipher, "Cipher");
362
363impl TryFrom<Cipher> for CipherRequestModel {
364 type Error = CryptoError;
365
366 fn try_from(c: Cipher) -> Result<Self, Self::Error> {
373 let attachments2 = c
374 .attachments
375 .map(|list| {
376 list.into_iter()
377 .map(|a| {
378 let id = a.id.clone().ok_or(CryptoError::MissingField("id"))?;
379 Ok::<_, CryptoError>((id, a.into()))
380 })
381 .collect::<Result<_, _>>()
382 })
383 .transpose()?;
384
385 Ok(CipherRequestModel {
386 encrypted_for: None,
387 r#type: Some(c.r#type.into()),
388 organization_id: c.organization_id.map(|id| id.to_string()),
389 folder_id: c.folder_id.map(|id| id.to_string()),
390 favorite: Some(c.favorite),
391 reprompt: Some(c.reprompt.into()),
392 key: c.key.map(|k| k.to_string()),
393 name: c.name.to_string(),
394 notes: c.notes.map(|n| n.to_string()),
395 login: c.login.map(|v| Box::new(v.into())),
396 card: c.card.map(|v| Box::new(v.into())),
397 identity: c.identity.map(|v| Box::new(v.into())),
398 secure_note: c.secure_note.map(|v| Box::new(v.into())),
399 ssh_key: c.ssh_key.map(|v| Box::new(v.into())),
400 bank_account: c.bank_account.map(|v| Box::new(v.into())),
401 drivers_license: c.drivers_license.map(|v| Box::new(v.into())),
402 passport: c.passport.map(|v| Box::new(v.into())),
403 fields: c.fields.map(|f| f.into_iter().map(Into::into).collect()),
404 password_history: c
405 .password_history
406 .map(|h| h.into_iter().map(Into::into).collect()),
407 attachments: None,
408 attachments2,
409 last_known_revision_date: Some(
410 c.revision_date.to_rfc3339_opts(SecondsFormat::Secs, true),
411 ),
412 archived_date: c.archived_date.map(|d| d.to_rfc3339()),
413 data: c.data,
414 })
415 }
416}
417
418#[allow(missing_docs)]
419#[derive(Serialize, Deserialize, Debug, Clone)]
420#[serde(rename_all = "camelCase", deny_unknown_fields)]
421#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
422#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
423pub struct CipherView {
424 pub id: Option<CipherId>,
425 pub organization_id: Option<OrganizationId>,
426 pub folder_id: Option<FolderId>,
427 pub collection_ids: Vec<CollectionId>,
428
429 pub key: Option<EncString>,
431
432 pub name: String,
433 pub notes: Option<String>,
434
435 pub r#type: CipherType,
436 pub login: Option<LoginView>,
437 pub identity: Option<identity::IdentityView>,
438 pub card: Option<card::CardView>,
439 pub secure_note: Option<secure_note::SecureNoteView>,
440 pub ssh_key: Option<ssh_key::SshKeyView>,
441 pub bank_account: Option<bank_account::BankAccountView>,
442 pub drivers_license: Option<drivers_license::DriversLicenseView>,
443 pub passport: Option<passport::PassportView>,
444
445 pub favorite: bool,
446 pub reprompt: CipherRepromptType,
447 pub organization_use_totp: bool,
448 pub edit: bool,
449 pub permissions: Option<CipherPermissions>,
450 pub view_password: bool,
451 pub local_data: Option<LocalDataView>,
452
453 pub attachments: Option<Vec<attachment::AttachmentView>>,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
457 pub fields: Option<Vec<field::FieldView>>,
458 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
459 pub creation_date: DateTime<Utc>,
460 pub deleted_date: Option<DateTime<Utc>>,
461 pub revision_date: DateTime<Utc>,
462 pub archived_date: Option<DateTime<Utc>>,
463}
464
465#[allow(missing_docs)]
466#[derive(Serialize, Deserialize, Debug, PartialEq)]
467#[serde(rename_all = "camelCase", deny_unknown_fields)]
468#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
469#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
470pub enum CipherListViewType {
471 Login(LoginListView),
472 SecureNote,
473 Card(CardListView),
474 Identity,
475 SshKey,
476 BankAccount,
477 Passport,
478 DriversLicense,
479}
480
481#[derive(Serialize, Deserialize, Debug, PartialEq)]
483#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
484#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
485pub enum CopyableCipherFields {
486 LoginUsername,
487 LoginPassword,
488 LoginTotp,
489 CardNumber,
490 CardSecurityCode,
491 IdentityUsername,
492 IdentityEmail,
493 IdentityPhone,
494 IdentityAddress,
495 SshKey,
496 SecureNotes,
497 BankAccountNameOnAccount,
498 BankAccountAccountNumber,
499 BankAccountRoutingNumber,
500 BankAccountBranchNumber,
501 BankAccountPin,
502 BankAccountIban,
503 BankAccountSwift,
504 PassportGivenName,
505 PassportSurname,
506 PassportPassportNumber,
507 PassportNationalIdentificationNumber,
508 DriversLicenseFirstName,
509 DriversLicenseMiddleName,
510 DriversLicenseLastName,
511 DriversLicenseLicenseNumber,
512}
513
514#[allow(missing_docs)]
515#[derive(Serialize, Deserialize, Debug, PartialEq)]
516#[serde(rename_all = "camelCase", deny_unknown_fields)]
517#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
518#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
519pub struct CipherListView {
520 pub id: Option<CipherId>,
521 pub organization_id: Option<OrganizationId>,
522 pub folder_id: Option<FolderId>,
523 pub collection_ids: Vec<CollectionId>,
524
525 pub key: Option<EncString>,
527
528 pub name: String,
529 pub subtitle: String,
530
531 pub r#type: CipherListViewType,
532
533 pub favorite: bool,
534 pub reprompt: CipherRepromptType,
535 pub organization_use_totp: bool,
536 pub edit: bool,
537 pub permissions: Option<CipherPermissions>,
538
539 pub view_password: bool,
540
541 pub attachments: u32,
543 pub has_old_attachments: bool,
545
546 pub creation_date: DateTime<Utc>,
547 pub deleted_date: Option<DateTime<Utc>>,
548 pub revision_date: DateTime<Utc>,
549 pub archived_date: Option<DateTime<Utc>>,
550
551 pub copyable_fields: Vec<CopyableCipherFields>,
553
554 pub local_data: Option<LocalDataView>,
555
556 #[cfg(feature = "wasm")]
558 pub notes: Option<String>,
559 #[cfg(feature = "wasm")]
562 pub fields: Option<Vec<field::FieldListView>>,
563 #[cfg(feature = "wasm")]
565 pub attachment_names: Option<Vec<String>>,
566}
567
568#[derive(Serialize, Deserialize, Debug)]
574#[serde(rename_all = "camelCase", deny_unknown_fields)]
575#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
576#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
577pub struct DecryptCipherListResult {
578 pub successes: Vec<CipherListView>,
580 pub failures: Vec<Cipher>,
582}
583
584#[derive(Serialize, Deserialize, Debug)]
590#[serde(rename_all = "camelCase", deny_unknown_fields)]
591#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
592#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
593pub struct DecryptCipherResult {
594 pub successes: Vec<CipherView>,
596 pub failures: Vec<Cipher>,
598}
599
600#[derive(Serialize, Deserialize, Debug)]
604#[serde(rename_all = "camelCase", deny_unknown_fields)]
605#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
606#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
607pub struct ListOrganizationCiphersResult {
608 pub ciphers: Vec<Cipher>,
610 pub list_views: Vec<CipherListView>,
612}
613
614impl CipherListView {
615 pub(crate) fn get_totp_key(
616 self,
617 ctx: &mut KeyStoreContext<KeySlotIds>,
618 ) -> Result<Option<String>, CryptoError> {
619 let key = self.key_identifier();
620 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
621
622 let totp = match self.r#type {
623 CipherListViewType::Login(LoginListView { totp, .. }) => {
624 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
625 }
626 _ => None,
627 };
628
629 Ok(totp)
630 }
631}
632
633impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Cipher> for CipherView {
634 fn encrypt_composite(
635 &self,
636 ctx: &mut KeyStoreContext<KeySlotIds>,
637 key: SymmetricKeySlotId,
638 ) -> Result<Cipher, CryptoError> {
639 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
640
641 let mut cipher_view = self.clone();
642 cipher_view.generate_checksums();
643
644 Ok(Cipher {
645 id: cipher_view.id,
646 organization_id: cipher_view.organization_id,
647 folder_id: cipher_view.folder_id,
648 collection_ids: cipher_view.collection_ids,
649 key: cipher_view.key,
650 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
651 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
652 r#type: cipher_view.r#type,
653 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
654 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
655 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
656 secure_note: cipher_view
657 .secure_note
658 .encrypt_composite(ctx, ciphers_key)?,
659 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
660 bank_account: cipher_view
661 .bank_account
662 .encrypt_composite(ctx, ciphers_key)?,
663 drivers_license: cipher_view
664 .drivers_license
665 .encrypt_composite(ctx, ciphers_key)?,
666 passport: cipher_view.passport.encrypt_composite(ctx, ciphers_key)?,
667 favorite: cipher_view.favorite,
668 reprompt: cipher_view.reprompt,
669 organization_use_totp: cipher_view.organization_use_totp,
670 edit: cipher_view.edit,
671 view_password: cipher_view.view_password,
672 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
673 attachments: cipher_view
674 .attachments
675 .encrypt_composite(ctx, ciphers_key)?,
676 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
677 password_history: cipher_view
678 .password_history
679 .encrypt_composite(ctx, ciphers_key)?,
680 creation_date: cipher_view.creation_date,
681 deleted_date: cipher_view.deleted_date,
682 revision_date: cipher_view.revision_date,
683 permissions: cipher_view.permissions,
684 archived_date: cipher_view.archived_date,
685 data: None, })
687 }
688}
689
690impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for Cipher {
691 #[instrument(err, skip_all, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
692 fn decrypt(
693 &self,
694 ctx: &mut KeyStoreContext<KeySlotIds>,
695 key: SymmetricKeySlotId,
696 ) -> Result<CipherView, CryptoError> {
697 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
698
699 let (attachments, attachment_decryption_failures) =
701 attachment::decrypt_attachments_with_failures(
702 self.attachments.as_deref().unwrap_or_default(),
703 ctx,
704 ciphers_key,
705 );
706
707 let mut cipher = CipherView {
708 id: self.id,
709 organization_id: self.organization_id,
710 folder_id: self.folder_id,
711 collection_ids: self.collection_ids.clone(),
712 key: self.key.clone(),
713 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
714 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
715 r#type: self.r#type,
716 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
717 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
718 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
719 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
720 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
721 bank_account: self.bank_account.decrypt(ctx, ciphers_key).ok().flatten(),
722 drivers_license: self
723 .drivers_license
724 .decrypt(ctx, ciphers_key)
725 .ok()
726 .flatten(),
727 passport: self.passport.decrypt(ctx, ciphers_key).ok().flatten(),
728 favorite: self.favorite,
729 reprompt: self.reprompt,
730 organization_use_totp: self.organization_use_totp,
731 edit: self.edit,
732 permissions: self.permissions,
733 view_password: self.view_password,
734 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
735 attachments: Some(attachments),
736 attachment_decryption_failures: Some(attachment_decryption_failures),
737 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
738 password_history: self
739 .password_history
740 .decrypt(ctx, ciphers_key)
741 .ok()
742 .flatten(),
743 creation_date: self.creation_date,
744 deleted_date: self.deleted_date,
745 revision_date: self.revision_date,
746 archived_date: self.archived_date,
747 };
748
749 if cipher.key.is_some()
752 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
753 {
754 cipher.remove_invalid_checksums();
755 }
756
757 Ok(cipher)
758 }
759}
760
761impl Cipher {
762 #[instrument(err, skip_all)]
773 pub(crate) fn decrypt_cipher_key(
774 ctx: &mut KeyStoreContext<KeySlotIds>,
775 key: SymmetricKeySlotId,
776 ciphers_key: &Option<EncString>,
777 ) -> Result<SymmetricKeySlotId, CryptoError> {
778 match ciphers_key {
779 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
780 None => Ok(key),
781 }
782 }
783
784 fn get_kind(&self) -> Option<&dyn CipherKind> {
786 match self.r#type {
787 CipherType::Login => self.login.as_ref().map(|v| v as _),
788 CipherType::Card => self.card.as_ref().map(|v| v as _),
789 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
790 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
791 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
792 CipherType::BankAccount => self.bank_account.as_ref().map(|v| v as _),
793 CipherType::DriversLicense => self.drivers_license.as_ref().map(|v| v as _),
794 CipherType::Passport => self.passport.as_ref().map(|v| v as _),
795 }
796 }
797
798 fn decrypt_subtitle(
800 &self,
801 ctx: &mut KeyStoreContext<KeySlotIds>,
802 key: SymmetricKeySlotId,
803 ) -> Result<String, CryptoError> {
804 self.get_kind()
805 .map(|sub| sub.decrypt_subtitle(ctx, key))
806 .unwrap_or_else(|| Ok(String::new()))
807 }
808
809 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
812 self.get_kind()
813 .map(|kind| kind.get_copyable_fields(Some(self)))
814 .unwrap_or_default()
815 }
816
817 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
823 let data = self
824 .data
825 .as_ref()
826 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
827
828 match &self.r#type {
829 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
830 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
831 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
832 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
833 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
834 crate::CipherType::BankAccount => self.bank_account = serde_json::from_str(data)?,
835 crate::CipherType::DriversLicense => self.drivers_license = serde_json::from_str(data)?,
836 crate::CipherType::Passport => self.passport = serde_json::from_str(data)?,
837 }
838 Ok(())
839 }
840
841 pub(crate) fn soft_delete(&mut self) {
843 self.deleted_date = Some(Utc::now());
844 }
845}
846impl CipherView {
847 #[allow(missing_docs)]
848 pub fn generate_cipher_key(
849 &mut self,
850 ctx: &mut KeyStoreContext<KeySlotIds>,
851 wrapping_key: SymmetricKeySlotId,
852 ) -> Result<(), CryptoError> {
853 let old_unwrapping_key = self.key_identifier();
854 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, old_unwrapping_key, &self.key)?;
855
856 let new_key = ctx.generate_symmetric_key();
857
858 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
859 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
860
861 self.key = Some(ctx.wrap_symmetric_key(wrapping_key, new_key)?);
862 Ok(())
863 }
864
865 #[allow(missing_docs)]
866 pub fn generate_checksums(&mut self) {
867 if let Some(l) = self.login.as_mut() {
868 l.generate_checksums();
869 }
870 }
871
872 #[allow(missing_docs)]
873 pub fn remove_invalid_checksums(&mut self) {
874 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
875 uris.retain(|u| u.is_checksum_valid());
876 }
877 }
878
879 fn reencrypt_attachment_keys(
880 &mut self,
881 ctx: &mut KeyStoreContext<KeySlotIds>,
882 old_key: SymmetricKeySlotId,
883 new_key: SymmetricKeySlotId,
884 ) -> Result<(), CryptoError> {
885 if let Some(attachments) = &mut self.attachments {
886 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
887 }
888 Ok(())
889 }
890
891 #[allow(missing_docs)]
892 pub fn decrypt_fido2_credentials(
893 &self,
894 ctx: &mut KeyStoreContext<KeySlotIds>,
895 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
896 let key = self.key_identifier();
897 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
898
899 Ok(self
900 .login
901 .as_ref()
902 .and_then(|l| l.fido2_credentials.as_ref())
903 .map(|f| f.decrypt(ctx, ciphers_key))
904 .transpose()?
905 .unwrap_or_default())
906 }
907
908 fn reencrypt_fido2_credentials(
909 &mut self,
910 ctx: &mut KeyStoreContext<KeySlotIds>,
911 old_key: SymmetricKeySlotId,
912 new_key: SymmetricKeySlotId,
913 ) -> Result<(), CryptoError> {
914 if let Some(login) = self.login.as_mut() {
915 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
916 }
917 Ok(())
918 }
919
920 pub fn move_to_organization(
927 &mut self,
928 ctx: &mut KeyStoreContext<KeySlotIds>,
929 organization_id: OrganizationId,
930 ) -> Result<(), CipherError> {
931 let new_key = SymmetricKeySlotId::Organization(organization_id);
932
933 self.reencrypt_cipher_keys(ctx, new_key)?;
934 self.organization_id = Some(organization_id);
935
936 Ok(())
937 }
938
939 pub fn reencrypt_cipher_keys(
944 &mut self,
945 ctx: &mut KeyStoreContext<KeySlotIds>,
946 new_wrapping_key: SymmetricKeySlotId,
947 ) -> Result<(), CipherError> {
948 let old_key = self.key_identifier();
949
950 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
952 return Err(CipherError::AttachmentsWithoutKeys);
953 }
954
955 if self.key.is_some() {
957 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
959
960 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
962 } else {
963 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
966 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
967 }
968
969 Ok(())
970 }
971
972 #[allow(missing_docs)]
973 pub fn set_new_fido2_credentials(
974 &mut self,
975 ctx: &mut KeyStoreContext<KeySlotIds>,
976 creds: Vec<Fido2CredentialFullView>,
977 ) -> Result<(), CipherError> {
978 let key = self.key_identifier();
979
980 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
981
982 require!(self.login.as_mut()).fido2_credentials =
983 Some(creds.encrypt_composite(ctx, ciphers_key)?);
984
985 Ok(())
986 }
987
988 #[allow(missing_docs)]
989 pub fn get_fido2_credentials(
990 &self,
991 ctx: &mut KeyStoreContext<KeySlotIds>,
992 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
993 let key = self.key_identifier();
994
995 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
996
997 let login = require!(self.login.as_ref());
998 let creds = require!(login.fido2_credentials.as_ref());
999 let res = creds.decrypt(ctx, ciphers_key)?;
1000 Ok(res)
1001 }
1002
1003 #[allow(missing_docs)]
1004 pub fn decrypt_fido2_private_key(
1005 &self,
1006 ctx: &mut KeyStoreContext<KeySlotIds>,
1007 ) -> Result<String, CipherError> {
1008 let fido2_credential = self.get_fido2_credentials(ctx)?;
1009
1010 Ok(fido2_credential[0].key_value.clone())
1011 }
1012
1013 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
1014 let changes = self
1015 .login
1016 .as_mut()
1017 .map_or(vec![], |login| {
1018 login.detect_password_change(&original_cipher.login)
1019 })
1020 .into_iter()
1021 .chain(self.fields.as_deref().map_or(vec![], |fields| {
1022 FieldView::detect_hidden_field_changes(
1023 fields,
1024 original_cipher.fields.as_deref().unwrap_or(&[]),
1025 )
1026 }))
1027 .rev()
1028 .chain(original_cipher.password_history.iter().flatten().cloned())
1029 .take(MAX_PASSWORD_HISTORY_ENTRIES)
1030 .collect();
1031 self.password_history = Some(changes)
1032 }
1033}
1034
1035impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for Cipher {
1036 fn decrypt(
1037 &self,
1038 ctx: &mut KeyStoreContext<KeySlotIds>,
1039 key: SymmetricKeySlotId,
1040 ) -> Result<CipherListView, CryptoError> {
1041 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1042
1043 Ok(CipherListView {
1044 id: self.id,
1045 organization_id: self.organization_id,
1046 folder_id: self.folder_id,
1047 collection_ids: self.collection_ids.clone(),
1048 key: self.key.clone(),
1049 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
1050 subtitle: self
1051 .decrypt_subtitle(ctx, ciphers_key)
1052 .ok()
1053 .unwrap_or_default(),
1054 r#type: match self.r#type {
1055 CipherType::Login => {
1056 let login = self
1057 .login
1058 .as_ref()
1059 .ok_or(CryptoError::MissingField("login"))?;
1060 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
1061 }
1062 CipherType::SecureNote => CipherListViewType::SecureNote,
1063 CipherType::Card => {
1064 let card = self
1065 .card
1066 .as_ref()
1067 .ok_or(CryptoError::MissingField("card"))?;
1068 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
1069 }
1070 CipherType::Identity => CipherListViewType::Identity,
1071 CipherType::SshKey => CipherListViewType::SshKey,
1072 CipherType::BankAccount => CipherListViewType::BankAccount,
1073 CipherType::Passport => CipherListViewType::Passport,
1074 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1075 },
1076 favorite: self.favorite,
1077 reprompt: self.reprompt,
1078 organization_use_totp: self.organization_use_totp,
1079 edit: self.edit,
1080 permissions: self.permissions,
1081 view_password: self.view_password,
1082 attachments: self
1083 .attachments
1084 .as_ref()
1085 .map(|a| a.len() as u32)
1086 .unwrap_or(0),
1087 has_old_attachments: self
1088 .attachments
1089 .as_ref()
1090 .map(|a| a.iter().any(|att| att.key.is_none()))
1091 .unwrap_or(false),
1092 creation_date: self.creation_date,
1093 deleted_date: self.deleted_date,
1094 revision_date: self.revision_date,
1095 copyable_fields: self.get_copyable_fields(),
1096 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
1097 archived_date: self.archived_date,
1098 #[cfg(feature = "wasm")]
1099 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
1100 #[cfg(feature = "wasm")]
1101 fields: self.fields.as_ref().map(|fields| {
1102 fields
1103 .iter()
1104 .filter_map(|f| {
1105 f.decrypt(ctx, ciphers_key)
1106 .ok()
1107 .map(field::FieldListView::from)
1108 })
1109 .collect()
1110 }),
1111 #[cfg(feature = "wasm")]
1112 attachment_names: self.attachments.as_ref().map(|attachments| {
1113 attachments
1114 .iter()
1115 .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
1116 .collect()
1117 }),
1118 })
1119 }
1120}
1121
1122impl IdentifyKey<SymmetricKeySlotId> for Cipher {
1123 fn key_identifier(&self) -> SymmetricKeySlotId {
1124 match self.organization_id {
1125 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1126 None => SymmetricKeySlotId::User,
1127 }
1128 }
1129}
1130
1131impl IdentifyKey<SymmetricKeySlotId> for CipherView {
1132 fn key_identifier(&self) -> SymmetricKeySlotId {
1133 match self.organization_id {
1134 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1135 None => SymmetricKeySlotId::User,
1136 }
1137 }
1138}
1139
1140impl IdentifyKey<SymmetricKeySlotId> for CipherListView {
1141 fn key_identifier(&self) -> SymmetricKeySlotId {
1142 match self.organization_id {
1143 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1144 None => SymmetricKeySlotId::User,
1145 }
1146 }
1147}
1148
1149pub(crate) struct StrictDecrypt<T>(pub(crate) T);
1158
1159impl IdentifyKey<SymmetricKeySlotId> for StrictDecrypt<Cipher> {
1160 fn key_identifier(&self) -> SymmetricKeySlotId {
1161 self.0.key_identifier()
1162 }
1163}
1164
1165impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for StrictDecrypt<Cipher> {
1166 #[instrument(err, skip_all, fields(cipher_id = ?self.0.id, org_id = ?self.0.organization_id, kind = ?self.0.r#type))]
1167 fn decrypt(
1168 &self,
1169 ctx: &mut KeyStoreContext<KeySlotIds>,
1170 key: SymmetricKeySlotId,
1171 ) -> Result<CipherView, CryptoError> {
1172 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1173
1174 let (attachments, attachment_decryption_failures) =
1176 attachment::decrypt_attachments_with_failures(
1177 self.0.attachments.as_deref().unwrap_or_default(),
1178 ctx,
1179 ciphers_key,
1180 );
1181
1182 let mut cipher = CipherView {
1183 id: self.0.id,
1184 organization_id: self.0.organization_id,
1185 folder_id: self.0.folder_id,
1186 collection_ids: self.0.collection_ids.clone(),
1187 key: self.0.key.clone(),
1188 name: self.0.name.decrypt(ctx, ciphers_key)?,
1189 notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1190 r#type: self.0.r#type,
1191 login: self
1192 .0
1193 .login
1194 .as_ref()
1195 .map(|l| StrictDecrypt(l).decrypt(ctx, ciphers_key))
1196 .transpose()?,
1197 identity: self
1198 .0
1199 .identity
1200 .as_ref()
1201 .map(|i| StrictDecrypt(i).decrypt(ctx, ciphers_key))
1202 .transpose()?,
1203 card: self
1204 .0
1205 .card
1206 .as_ref()
1207 .map(|c| StrictDecrypt(c).decrypt(ctx, ciphers_key))
1208 .transpose()?,
1209 secure_note: self.0.secure_note.decrypt(ctx, ciphers_key)?,
1210 ssh_key: self.0.ssh_key.decrypt(ctx, ciphers_key)?,
1211 bank_account: self.0.bank_account.decrypt(ctx, ciphers_key)?,
1212 drivers_license: self.0.drivers_license.decrypt(ctx, ciphers_key)?,
1213 passport: self.0.passport.decrypt(ctx, ciphers_key)?,
1214 favorite: self.0.favorite,
1215 reprompt: self.0.reprompt,
1216 organization_use_totp: self.0.organization_use_totp,
1217 edit: self.0.edit,
1218 permissions: self.0.permissions,
1219 view_password: self.0.view_password,
1220 local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1221 attachments: Some(attachments),
1222 attachment_decryption_failures: Some(attachment_decryption_failures),
1223 fields: self
1224 .0
1225 .fields
1226 .as_ref()
1227 .map(|fields| {
1228 fields
1229 .iter()
1230 .map(|f| StrictDecrypt(f).decrypt(ctx, ciphers_key))
1231 .collect::<Result<Vec<_>, _>>()
1232 })
1233 .transpose()?,
1234 password_history: self.0.password_history.decrypt(ctx, ciphers_key)?,
1235 creation_date: self.0.creation_date,
1236 deleted_date: self.0.deleted_date,
1237 revision_date: self.0.revision_date,
1238 archived_date: self.0.archived_date,
1239 };
1240
1241 if cipher.key.is_some()
1244 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1245 {
1246 cipher.remove_invalid_checksums();
1247 }
1248
1249 Ok(cipher)
1250 }
1251}
1252
1253impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for StrictDecrypt<Cipher> {
1254 fn decrypt(
1255 &self,
1256 ctx: &mut KeyStoreContext<KeySlotIds>,
1257 key: SymmetricKeySlotId,
1258 ) -> Result<CipherListView, CryptoError> {
1259 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1260
1261 Ok(CipherListView {
1262 id: self.0.id,
1263 organization_id: self.0.organization_id,
1264 folder_id: self.0.folder_id,
1265 collection_ids: self.0.collection_ids.clone(),
1266 key: self.0.key.clone(),
1267 name: self.0.name.decrypt(ctx, ciphers_key)?,
1268 subtitle: self.0.decrypt_subtitle(ctx, ciphers_key)?,
1269 r#type: match self.0.r#type {
1270 CipherType::Login => {
1271 let login = self
1272 .0
1273 .login
1274 .as_ref()
1275 .ok_or(CryptoError::MissingField("login"))?;
1276 CipherListViewType::Login(StrictDecrypt(login).decrypt(ctx, ciphers_key)?)
1277 }
1278 CipherType::SecureNote => CipherListViewType::SecureNote,
1279 CipherType::Card => {
1280 let card = self
1281 .0
1282 .card
1283 .as_ref()
1284 .ok_or(CryptoError::MissingField("card"))?;
1285 CipherListViewType::Card(StrictDecrypt(card).decrypt(ctx, ciphers_key)?)
1286 }
1287 CipherType::Identity => CipherListViewType::Identity,
1288 CipherType::SshKey => CipherListViewType::SshKey,
1289 CipherType::BankAccount => CipherListViewType::BankAccount,
1290 CipherType::Passport => CipherListViewType::Passport,
1291 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1292 },
1293 favorite: self.0.favorite,
1294 reprompt: self.0.reprompt,
1295 organization_use_totp: self.0.organization_use_totp,
1296 edit: self.0.edit,
1297 permissions: self.0.permissions,
1298 view_password: self.0.view_password,
1299 attachments: self
1300 .0
1301 .attachments
1302 .as_ref()
1303 .map(|a| a.len() as u32)
1304 .unwrap_or(0),
1305 has_old_attachments: self
1306 .0
1307 .attachments
1308 .as_ref()
1309 .map(|a| a.iter().any(|att| att.key.is_none()))
1310 .unwrap_or(false),
1311 creation_date: self.0.creation_date,
1312 deleted_date: self.0.deleted_date,
1313 revision_date: self.0.revision_date,
1314 copyable_fields: self.0.get_copyable_fields(),
1315 local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1316 archived_date: self.0.archived_date,
1317 #[cfg(feature = "wasm")]
1318 notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1319 #[cfg(feature = "wasm")]
1320 fields: self
1321 .0
1322 .fields
1323 .as_ref()
1324 .map(|fields| {
1325 fields
1326 .iter()
1327 .map(|f| {
1328 StrictDecrypt(f)
1329 .decrypt(ctx, ciphers_key)
1330 .map(field::FieldListView::from)
1331 })
1332 .collect::<Result<Vec<_>, _>>()
1333 })
1334 .transpose()?,
1335 #[cfg(feature = "wasm")]
1336 attachment_names: self
1337 .0
1338 .attachments
1339 .as_ref()
1340 .map(|attachments| {
1341 attachments
1342 .iter()
1343 .map(|a| a.file_name.decrypt(ctx, ciphers_key))
1344 .collect::<Result<Vec<_>, _>>()
1345 })
1346 .transpose()?
1347 .map(|names| names.into_iter().flatten().collect()),
1348 })
1349 }
1350}
1351
1352impl TryFrom<CipherDetailsResponseModel> for Cipher {
1353 type Error = VaultParseError;
1354
1355 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
1356 Ok(Self {
1357 id: cipher.id.map(CipherId::new),
1358 organization_id: cipher.organization_id.map(OrganizationId::new),
1359 folder_id: cipher.folder_id.map(FolderId::new),
1360 collection_ids: cipher
1361 .collection_ids
1362 .unwrap_or_default()
1363 .into_iter()
1364 .map(CollectionId::new)
1365 .collect(),
1366 name: require!(EncString::try_from_optional(cipher.name)?),
1367 notes: EncString::try_from_optional(cipher.notes)?,
1368 r#type: require!(cipher.r#type).try_into()?,
1369 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1370 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1371 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1372 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1373 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1374 bank_account: cipher.bank_account.map(|b| (*b).try_into()).transpose()?,
1375 drivers_license: cipher
1376 .drivers_license
1377 .map(|d| (*d).try_into())
1378 .transpose()?,
1379 passport: cipher.passport.map(|p| (*p).try_into()).transpose()?,
1380 favorite: cipher.favorite.unwrap_or(false),
1381 reprompt: cipher
1382 .reprompt
1383 .map(|r| r.try_into())
1384 .transpose()?
1385 .unwrap_or(CipherRepromptType::None),
1386 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
1387 edit: cipher.edit.unwrap_or(true),
1388 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1389 view_password: cipher.view_password.unwrap_or(true),
1390 local_data: None, attachments: cipher
1392 .attachments
1393 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1394 .transpose()?,
1395 fields: cipher
1396 .fields
1397 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1398 .transpose()?,
1399 password_history: cipher
1400 .password_history
1401 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1402 .transpose()?,
1403 creation_date: require!(cipher.creation_date).parse()?,
1404 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1405 revision_date: require!(cipher.revision_date).parse()?,
1406 key: EncString::try_from_optional(cipher.key)?,
1407 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1408 data: cipher.data,
1409 })
1410 }
1411}
1412
1413impl PartialCipher for CipherDetailsResponseModel {
1414 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1415 Ok(Cipher {
1416 local_data: cipher.and_then(|c| c.local_data),
1417 ..self.try_into()?
1418 })
1419 }
1420}
1421
1422impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
1423 type Error = MissingFieldError;
1424
1425 fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
1426 Ok(match t {
1427 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1428 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1429 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1430 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1431 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1432 bitwarden_api_api::models::CipherType::BankAccount => CipherType::BankAccount,
1433 bitwarden_api_api::models::CipherType::Passport => CipherType::Passport,
1434 bitwarden_api_api::models::CipherType::DriversLicense => CipherType::DriversLicense,
1435 bitwarden_api_api::models::CipherType::__Unknown(_) => {
1436 return Err(MissingFieldError("type"));
1437 }
1438 })
1439 }
1440}
1441
1442impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1443 type Error = MissingFieldError;
1444
1445 fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
1446 Ok(match t {
1447 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1448 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1449 bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
1450 return Err(MissingFieldError("reprompt"));
1451 }
1452 })
1453 }
1454}
1455
1456pub(crate) trait PartialCipher {
1460 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1461}
1462
1463impl From<CipherType> for bitwarden_api_api::models::CipherType {
1464 fn from(t: CipherType) -> Self {
1465 match t {
1466 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1467 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1468 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1469 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1470 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1471 CipherType::BankAccount => bitwarden_api_api::models::CipherType::BankAccount,
1472 CipherType::Passport => bitwarden_api_api::models::CipherType::Passport,
1473 CipherType::DriversLicense => bitwarden_api_api::models::CipherType::DriversLicense,
1474 }
1475 }
1476}
1477
1478impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1479 fn from(t: CipherRepromptType) -> Self {
1480 match t {
1481 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1482 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1483 }
1484 }
1485}
1486
1487impl PartialCipher for CipherResponseModel {
1488 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1489 Ok(Cipher {
1490 collection_ids: cipher
1491 .as_ref()
1492 .map(|c| c.collection_ids.clone())
1493 .unwrap_or_default(),
1494 local_data: cipher.and_then(|c| c.local_data),
1495 id: self.id.map(CipherId::new),
1496 organization_id: self.organization_id.map(OrganizationId::new),
1497 folder_id: self.folder_id.map(FolderId::new),
1498 name: require!(self.name).parse()?,
1499 notes: EncString::try_from_optional(self.notes)?,
1500 r#type: require!(self.r#type).try_into()?,
1501 login: self.login.map(|l| (*l).try_into()).transpose()?,
1502 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1503 card: self.card.map(|c| (*c).try_into()).transpose()?,
1504 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1505 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1506 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1507 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1508 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1509 favorite: self.favorite.unwrap_or(false),
1510 reprompt: self
1511 .reprompt
1512 .map(|r| r.try_into())
1513 .transpose()?
1514 .unwrap_or(CipherRepromptType::None),
1515 organization_use_totp: self.organization_use_totp.unwrap_or(false),
1516 edit: self.edit.unwrap_or(false),
1517 permissions: self.permissions.map(|p| (*p).try_into()).transpose()?,
1518 view_password: self.view_password.unwrap_or(true),
1519 attachments: self
1520 .attachments
1521 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1522 .transpose()?,
1523 fields: self
1524 .fields
1525 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1526 .transpose()?,
1527 password_history: self
1528 .password_history
1529 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1530 .transpose()?,
1531 creation_date: require!(self.creation_date).parse()?,
1532 deleted_date: self.deleted_date.map(|d| d.parse()).transpose()?,
1533 revision_date: require!(self.revision_date).parse()?,
1534 key: EncString::try_from_optional(self.key)?,
1535 archived_date: self.archived_date.map(|d| d.parse()).transpose()?,
1536 data: self.data,
1537 })
1538 }
1539}
1540
1541impl PartialCipher for CipherMiniResponseModel {
1542 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1543 let cipher = cipher.as_ref();
1544 Ok(Cipher {
1545 id: self.id.map(CipherId::new),
1546 organization_id: self.organization_id.map(OrganizationId::new),
1547 key: EncString::try_from_optional(self.key)?,
1548 name: require!(EncString::try_from_optional(self.name)?),
1549 notes: EncString::try_from_optional(self.notes)?,
1550 r#type: require!(self.r#type).try_into()?,
1551 login: self.login.map(|l| (*l).try_into()).transpose()?,
1552 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1553 card: self.card.map(|c| (*c).try_into()).transpose()?,
1554 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1555 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1556 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1557 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1558 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1559 reprompt: self
1560 .reprompt
1561 .map(|r| r.try_into())
1562 .transpose()?
1563 .unwrap_or(CipherRepromptType::None),
1564 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1565 attachments: self
1566 .attachments
1567 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1568 .transpose()?,
1569 fields: self
1570 .fields
1571 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1572 .transpose()?,
1573 password_history: self
1574 .password_history
1575 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1576 .transpose()?,
1577 creation_date: require!(self.creation_date)
1578 .parse()
1579 .map_err(Into::<VaultParseError>::into)?,
1580 deleted_date: self
1581 .deleted_date
1582 .map(|d| d.parse())
1583 .transpose()
1584 .map_err(Into::<VaultParseError>::into)?,
1585 revision_date: require!(self.revision_date)
1586 .parse()
1587 .map_err(Into::<VaultParseError>::into)?,
1588 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1589 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1590 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1591 edit: cipher.map_or(Default::default(), |c| c.edit),
1592 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1593 view_password: cipher.is_none_or(|c| c.view_password),
1594 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1595 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1596 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1597 })
1598 }
1599}
1600
1601impl PartialCipher for CipherMiniDetailsResponseModel {
1602 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1603 let cipher = cipher.as_ref();
1604 Ok(Cipher {
1605 id: self.id.map(CipherId::new),
1606 organization_id: self.organization_id.map(OrganizationId::new),
1607 key: EncString::try_from_optional(self.key)?,
1608 name: require!(EncString::try_from_optional(self.name)?),
1609 notes: EncString::try_from_optional(self.notes)?,
1610 r#type: require!(self.r#type).try_into()?,
1611 login: self.login.map(|l| (*l).try_into()).transpose()?,
1612 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1613 card: self.card.map(|c| (*c).try_into()).transpose()?,
1614 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1615 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1616 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1617 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1618 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1619 reprompt: self
1620 .reprompt
1621 .map(|r| r.try_into())
1622 .transpose()?
1623 .unwrap_or(CipherRepromptType::None),
1624 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1625 attachments: self
1626 .attachments
1627 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1628 .transpose()?,
1629 fields: self
1630 .fields
1631 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1632 .transpose()?,
1633 password_history: self
1634 .password_history
1635 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1636 .transpose()?,
1637 creation_date: require!(self.creation_date)
1638 .parse()
1639 .map_err(Into::<VaultParseError>::into)?,
1640 deleted_date: self
1641 .deleted_date
1642 .map(|d| d.parse())
1643 .transpose()
1644 .map_err(Into::<VaultParseError>::into)?,
1645 revision_date: require!(self.revision_date)
1646 .parse()
1647 .map_err(Into::<VaultParseError>::into)?,
1648 collection_ids: self
1649 .collection_ids
1650 .into_iter()
1651 .flatten()
1652 .map(CollectionId::new)
1653 .collect(),
1654 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1655 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1656 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1657 edit: cipher.map_or(Default::default(), |c| c.edit),
1658 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1659 view_password: cipher.is_none_or(|c: &Cipher| c.view_password),
1660 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1661 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1662 })
1663 }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668
1669 use attachment::AttachmentView;
1670 use bitwarden_core::key_management::{
1671 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1672 };
1673 use bitwarden_crypto::SymmetricCryptoKey;
1674
1675 use super::*;
1676 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1677
1678 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1680 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1681 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1682 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1683 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1684 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1685 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1686
1687 fn generate_cipher() -> CipherView {
1688 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1689 CipherView {
1690 r#type: CipherType::Login,
1691 login: Some(LoginView {
1692 username: Some("test_username".to_string()),
1693 password: Some("test_password".to_string()),
1694 password_revision_date: None,
1695 uris: None,
1696 totp: None,
1697 autofill_on_page_load: None,
1698 fido2_credentials: None,
1699 }),
1700 id: Some(test_id),
1701 organization_id: None,
1702 folder_id: None,
1703 collection_ids: vec![],
1704 key: None,
1705 name: "My test login".to_string(),
1706 notes: None,
1707 identity: None,
1708 card: None,
1709 secure_note: None,
1710 ssh_key: None,
1711 bank_account: None,
1712 drivers_license: None,
1713 passport: None,
1714 favorite: false,
1715 reprompt: CipherRepromptType::None,
1716 organization_use_totp: true,
1717 edit: true,
1718 permissions: None,
1719 view_password: true,
1720 local_data: None,
1721 attachments: None,
1722 attachment_decryption_failures: None,
1723 fields: None,
1724 password_history: None,
1725 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1726 deleted_date: None,
1727 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1728 archived_date: None,
1729 }
1730 }
1731
1732 fn generate_fido2(
1733 ctx: &mut KeyStoreContext<KeySlotIds>,
1734 key: SymmetricKeySlotId,
1735 ) -> Fido2Credential {
1736 Fido2Credential {
1737 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1738 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1739 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1740 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1741 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1742 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1743 user_handle: None,
1744 user_name: None,
1745 counter: "123".to_string().encrypt(ctx, key).unwrap(),
1746 rp_name: None,
1747 user_display_name: None,
1748 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1749 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1750 }
1751 }
1752
1753 #[test]
1754 fn test_decrypt_cipher_list_view() {
1755 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1756 let key_store = create_test_crypto_with_user_key(key);
1757
1758 let cipher = Cipher {
1759 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1760 organization_id: None,
1761 folder_id: None,
1762 collection_ids: vec![],
1763 key: None,
1764 name: TEST_CIPHER_NAME.parse().unwrap(),
1765 notes: None,
1766 r#type: CipherType::Login,
1767 login: Some(Login {
1768 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1769 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1770 password_revision_date: None,
1771 uris: None,
1772 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1773 autofill_on_page_load: None,
1774 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
1775 }),
1776 identity: None,
1777 card: None,
1778 secure_note: None,
1779 ssh_key: None,
1780 bank_account: None,
1781 drivers_license: None,
1782 passport: None,
1783 favorite: false,
1784 reprompt: CipherRepromptType::None,
1785 organization_use_totp: false,
1786 edit: true,
1787 permissions: Some(CipherPermissions {
1788 delete: false,
1789 restore: false
1790 }),
1791 view_password: true,
1792 local_data: None,
1793 attachments: None,
1794 fields: None,
1795 password_history: None,
1796 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1797 deleted_date: None,
1798 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1799 archived_date: None,
1800 data: None,
1801 };
1802
1803 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1804
1805 assert_eq!(
1806 view,
1807 CipherListView {
1808 id: cipher.id,
1809 organization_id: cipher.organization_id,
1810 folder_id: cipher.folder_id,
1811 collection_ids: cipher.collection_ids,
1812 key: cipher.key,
1813 name: "My test login".to_string(),
1814 subtitle: "test_username".to_string(),
1815 r#type: CipherListViewType::Login(LoginListView {
1816 fido2_credentials: Some(vec![Fido2CredentialListView {
1817 credential_id: "123".to_string(),
1818 rp_id: "123".to_string(),
1819 user_handle: None,
1820 user_name: None,
1821 user_display_name: None,
1822 counter: "123".to_string(),
1823 }]),
1824 has_fido2: true,
1825 username: Some("test_username".to_string()),
1826 totp: cipher.login.as_ref().unwrap().totp.clone(),
1827 uris: None,
1828 }),
1829 favorite: cipher.favorite,
1830 reprompt: cipher.reprompt,
1831 organization_use_totp: cipher.organization_use_totp,
1832 edit: cipher.edit,
1833 permissions: cipher.permissions,
1834 view_password: cipher.view_password,
1835 attachments: 0,
1836 has_old_attachments: false,
1837 creation_date: cipher.creation_date,
1838 deleted_date: cipher.deleted_date,
1839 revision_date: cipher.revision_date,
1840 copyable_fields: vec![
1841 CopyableCipherFields::LoginUsername,
1842 CopyableCipherFields::LoginPassword,
1843 CopyableCipherFields::LoginTotp
1844 ],
1845 local_data: None,
1846 archived_date: cipher.archived_date,
1847 #[cfg(feature = "wasm")]
1848 notes: None,
1849 #[cfg(feature = "wasm")]
1850 fields: None,
1851 #[cfg(feature = "wasm")]
1852 attachment_names: None,
1853 }
1854 )
1855 }
1856
1857 #[test]
1858 fn test_decrypt_cipher_fails_with_invalid_name() {
1859 let key_store =
1860 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1861
1862 let cipher = key_store.encrypt(generate_cipher()).unwrap();
1864 let cipher = Cipher {
1865 name: TEST_CIPHER_NAME.parse().unwrap(), ..cipher
1867 };
1868
1869 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1871 assert!(
1872 lenient_result.is_ok(),
1873 "Lenient decryption should succeed even when name is encrypted with a different key"
1874 );
1875 assert_eq!(
1876 lenient_result.unwrap().name,
1877 String::new(),
1878 "Lenient decryption should yield an empty name on error"
1879 );
1880
1881 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1883 assert!(
1884 strict_result.is_err(),
1885 "Strict decryption should fail when name is encrypted with a different key"
1886 );
1887 }
1888
1889 #[test]
1890 fn test_decrypt_cipher_fails_with_invalid_login() {
1891 let key_store =
1892 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1893
1894 let cipher = key_store.encrypt(generate_cipher()).unwrap();
1896 let cipher = Cipher {
1897 login: Some(Login {
1898 username: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher.login.unwrap()
1900 }),
1901 ..cipher
1902 };
1903
1904 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1906 assert!(
1907 lenient_result.is_ok(),
1908 "Lenient decryption should succeed even when login username is encrypted with a different key"
1909 );
1910 let lenient_view = lenient_result.unwrap();
1911 assert!(
1912 lenient_view.login.is_some(),
1913 "Lenient decryption should still return the login object"
1914 );
1915 assert!(
1916 lenient_view.login.unwrap().username.is_none(),
1917 "Lenient decryption should null out the failing username field"
1918 );
1919
1920 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1922 assert!(
1923 strict_result.is_err(),
1924 "Strict decryption should fail when login username is encrypted with a different key"
1925 );
1926 }
1927
1928 #[test]
1929 fn test_generate_cipher_key() {
1930 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1931 let key_store = create_test_crypto_with_user_key(key);
1932
1933 let original_cipher = generate_cipher();
1934
1935 let cipher = generate_cipher();
1937 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1938 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1939 assert!(no_key_cipher_dec.key.is_none());
1940 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1941
1942 let mut cipher = generate_cipher();
1943 cipher
1944 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1945 .unwrap();
1946
1947 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1949 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1950 assert!(key_cipher_dec.key.is_some());
1951 assert_eq!(key_cipher_dec.name, original_cipher.name);
1952 }
1953
1954 #[test]
1955 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1956 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1957 let key_store = create_test_crypto_with_user_key(key);
1958
1959 let mut original_cipher = generate_cipher();
1960 {
1961 let mut ctx = key_store.context();
1962 let cipher_key = ctx.generate_symmetric_key();
1963
1964 original_cipher.key = Some(
1965 ctx.wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
1966 .unwrap(),
1967 );
1968 }
1969
1970 original_cipher
1971 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1972 .unwrap();
1973
1974 let wrapped_key = original_cipher.key.unwrap();
1976 let mut ctx = key_store.context();
1977 let _ = ctx
1978 .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_key)
1979 .unwrap();
1980 }
1981
1982 #[test]
1983 fn test_generate_cipher_key_ignores_attachments_without_key() {
1984 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1985 let key_store = create_test_crypto_with_user_key(key);
1986
1987 let mut cipher = generate_cipher();
1988 let attachment = AttachmentView {
1989 id: None,
1990 url: None,
1991 size: None,
1992 size_name: None,
1993 file_name: Some("Attachment test name".into()),
1994 key: None,
1995 #[cfg(feature = "wasm")]
1996 decrypted_key: None,
1997 };
1998 cipher.attachments = Some(vec![attachment]);
1999
2000 cipher
2001 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2002 .unwrap();
2003 assert!(cipher.attachments.unwrap()[0].key.is_none());
2004 }
2005
2006 #[test]
2007 fn test_reencrypt_cipher_key() {
2008 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2009 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2010 let key_store = create_test_crypto_with_user_key(old_key);
2011 let mut ctx = key_store.context_mut();
2012
2013 let mut cipher = generate_cipher();
2014 cipher
2015 .generate_cipher_key(&mut ctx, cipher.key_identifier())
2016 .unwrap();
2017
2018 let new_key_id = ctx.add_local_symmetric_key(new_key);
2020
2021 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
2022
2023 assert!(cipher.key.is_some());
2025 assert!(
2026 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
2027 .is_ok()
2028 );
2029 }
2030
2031 #[test]
2032 fn test_reencrypt_cipher_key_ignores_missing_key() {
2033 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2034 let key_store = create_test_crypto_with_user_key(key);
2035 let mut ctx = key_store.context_mut();
2036 let mut cipher = generate_cipher();
2037
2038 let new_cipher_key = ctx.generate_symmetric_key();
2040 cipher
2041 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
2042 .unwrap();
2043
2044 assert!(cipher.key.is_none());
2046 }
2047
2048 #[test]
2049 fn test_move_user_cipher_to_org() {
2050 let org = OrganizationId::new_v4();
2051 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2052 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2053 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2054
2055 let mut cipher = generate_cipher();
2057 cipher
2058 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2059 .unwrap();
2060
2061 cipher
2062 .move_to_organization(&mut key_store.context(), org)
2063 .unwrap();
2064 assert_eq!(cipher.organization_id, Some(org));
2065
2066 let cipher_enc = key_store.encrypt(cipher).unwrap();
2068 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
2069
2070 assert_eq!(cipher_dec.name, "My test login");
2071 }
2072
2073 #[test]
2074 fn test_move_user_cipher_to_org_manually() {
2075 let org = OrganizationId::new_v4();
2076 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2077 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2078 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2079
2080 let mut cipher = generate_cipher();
2082 cipher
2083 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2084 .unwrap();
2085
2086 cipher.organization_id = Some(org);
2087
2088 assert!(key_store.encrypt(cipher).is_err());
2091 }
2092
2093 #[test]
2094 fn test_move_user_cipher_with_attachment_without_key_to_org() {
2095 let org = OrganizationId::new_v4();
2096 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2097 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2098 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2099
2100 let mut cipher = generate_cipher();
2101 let attachment = AttachmentView {
2102 id: None,
2103 url: None,
2104 size: None,
2105 size_name: None,
2106 file_name: Some("Attachment test name".into()),
2107 key: None,
2108 #[cfg(feature = "wasm")]
2109 decrypted_key: None,
2110 };
2111 cipher.attachments = Some(vec![attachment]);
2112
2113 assert!(
2115 cipher
2116 .move_to_organization(&mut key_store.context(), org)
2117 .is_err()
2118 );
2119 }
2120
2121 #[test]
2122 fn test_move_user_cipher_with_attachment_with_key_to_org() {
2123 let org = OrganizationId::new_v4();
2124 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2125 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2126 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2127 let org_key = SymmetricKeySlotId::Organization(org);
2128
2129 let (attachment_key_enc, attachment_key_val) = {
2131 let mut ctx = key_store.context();
2132 let attachment_key = ctx.generate_symmetric_key();
2133 let attachment_key_enc = ctx
2134 .wrap_symmetric_key(SymmetricKeySlotId::User, attachment_key)
2135 .unwrap();
2136 #[allow(deprecated)]
2137 let attachment_key_val = ctx
2138 .dangerous_get_symmetric_key(attachment_key)
2139 .unwrap()
2140 .clone();
2141
2142 (attachment_key_enc, attachment_key_val)
2143 };
2144
2145 let mut cipher = generate_cipher();
2146 let attachment = AttachmentView {
2147 id: None,
2148 url: None,
2149 size: None,
2150 size_name: None,
2151 file_name: Some("Attachment test name".into()),
2152 key: Some(attachment_key_enc),
2153 #[cfg(feature = "wasm")]
2154 decrypted_key: None,
2155 };
2156 cipher.attachments = Some(vec![attachment]);
2157 let cred = generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User);
2158 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
2159
2160 cipher
2161 .move_to_organization(&mut key_store.context(), org)
2162 .unwrap();
2163
2164 assert!(cipher.key.is_none());
2165
2166 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
2169 let mut ctx = key_store.context();
2170 let new_attachment_key_id = ctx
2171 .unwrap_symmetric_key(org_key, &new_attachment_key)
2172 .unwrap();
2173 #[allow(deprecated)]
2174 let new_attachment_key_dec = ctx
2175 .dangerous_get_symmetric_key(new_attachment_key_id)
2176 .unwrap();
2177
2178 assert_eq!(*new_attachment_key_dec, attachment_key_val);
2179
2180 let cred2: Fido2CredentialFullView = cipher
2181 .login
2182 .unwrap()
2183 .fido2_credentials
2184 .unwrap()
2185 .first()
2186 .unwrap()
2187 .decrypt(&mut key_store.context(), org_key)
2188 .unwrap();
2189
2190 assert_eq!(cred2.credential_id, "123");
2191 }
2192
2193 #[test]
2194 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
2195 let org = OrganizationId::new_v4();
2196 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2197 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2198 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2199 let org_key = SymmetricKeySlotId::Organization(org);
2200
2201 let mut ctx = key_store.context();
2202
2203 let cipher_key = ctx.generate_symmetric_key();
2204 let cipher_key_enc = ctx
2205 .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
2206 .unwrap();
2207
2208 let attachment_key = ctx.generate_symmetric_key();
2210 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
2211
2212 let mut cipher = generate_cipher();
2213 cipher.key = Some(cipher_key_enc);
2214
2215 let attachment = AttachmentView {
2216 id: None,
2217 url: None,
2218 size: None,
2219 size_name: None,
2220 file_name: Some("Attachment test name".into()),
2221 key: Some(attachment_key_enc.clone()),
2222 #[cfg(feature = "wasm")]
2223 decrypted_key: None,
2224 };
2225 cipher.attachments = Some(vec![attachment]);
2226
2227 let cred = generate_fido2(&mut ctx, cipher_key);
2228 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
2229
2230 cipher.move_to_organization(&mut ctx, org).unwrap();
2231
2232 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
2234 let new_cipher_key_dec = ctx
2235 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
2236 .unwrap();
2237 #[allow(deprecated)]
2238 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
2239 #[allow(deprecated)]
2240 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
2241
2242 assert_eq!(new_cipher_key_dec, cipher_key_val);
2243
2244 assert_eq!(
2246 cipher.attachments.unwrap()[0]
2247 .key
2248 .as_ref()
2249 .unwrap()
2250 .to_string(),
2251 attachment_key_enc.to_string()
2252 );
2253
2254 let cred2: Fido2Credential = cipher
2255 .login
2256 .unwrap()
2257 .fido2_credentials
2258 .unwrap()
2259 .first()
2260 .unwrap()
2261 .clone();
2262
2263 assert_eq!(
2264 cred2.credential_id.to_string(),
2265 cred.credential_id.to_string()
2266 );
2267 }
2268
2269 #[test]
2270 fn test_decrypt_fido2_private_key() {
2271 let key_store =
2272 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2273 let mut ctx = key_store.context();
2274
2275 let mut cipher_view = generate_cipher();
2276 cipher_view
2277 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
2278 .unwrap();
2279
2280 let key_id = cipher_view.key_identifier();
2281 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
2282
2283 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
2284
2285 cipher_view.login.as_mut().unwrap().fido2_credentials =
2286 Some(vec![fido2_credential.clone()]);
2287
2288 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
2289 assert_eq!(decrypted_key_value, "123");
2290 }
2291
2292 #[test]
2293 fn test_password_history_on_password_change() {
2294 use chrono::Utc;
2295
2296 let original_cipher = generate_cipher();
2297 let mut new_cipher = generate_cipher();
2298
2299 if let Some(ref mut login) = new_cipher.login {
2301 login.password = Some("new_password123".to_string());
2302 }
2303
2304 let start = Utc::now();
2305 new_cipher.update_password_history(&original_cipher);
2306 let end = Utc::now();
2307
2308 assert!(new_cipher.password_history.is_some());
2309 let history = new_cipher.password_history.unwrap();
2310 assert_eq!(history.len(), 1);
2311 assert_eq!(history[0].password, "test_password");
2312 assert!(
2313 history[0].last_used_date >= start && history[0].last_used_date <= end,
2314 "last_used_date was not set properly"
2315 );
2316 }
2317
2318 #[test]
2319 fn test_password_history_on_unchanged_password() {
2320 let original_cipher = generate_cipher();
2321 let mut new_cipher = generate_cipher();
2322
2323 new_cipher.update_password_history(&original_cipher);
2324
2325 assert!(
2327 new_cipher.password_history.is_none()
2328 || new_cipher.password_history.as_ref().unwrap().is_empty()
2329 );
2330 }
2331
2332 #[test]
2333 fn test_password_history_is_preserved() {
2334 use chrono::TimeZone;
2335
2336 let mut original_cipher = generate_cipher();
2337 original_cipher.password_history = Some(
2338 (0..4)
2339 .map(|i| PasswordHistoryView {
2340 password: format!("old_password_{}", i),
2341 last_used_date: chrono::Utc
2342 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
2343 .unwrap(),
2344 })
2345 .collect(),
2346 );
2347
2348 let mut new_cipher = generate_cipher();
2349
2350 new_cipher.update_password_history(&original_cipher);
2351
2352 assert!(new_cipher.password_history.is_some());
2353 let history = new_cipher.password_history.unwrap();
2354 assert_eq!(history.len(), 4);
2355
2356 assert_eq!(history[0].password, "old_password_0");
2357 assert_eq!(
2358 history[0].last_used_date,
2359 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
2360 );
2361 assert_eq!(history[1].password, "old_password_1");
2362 assert_eq!(
2363 history[1].last_used_date,
2364 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
2365 );
2366 assert_eq!(history[2].password, "old_password_2");
2367 assert_eq!(
2368 history[2].last_used_date,
2369 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
2370 );
2371 assert_eq!(history[3].password, "old_password_3");
2372 assert_eq!(
2373 history[3].last_used_date,
2374 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
2375 );
2376 }
2377
2378 #[test]
2379 fn test_populate_cipher_types_login_with_valid_data() {
2380 let mut cipher = Cipher {
2381 id: Some(TEST_UUID.parse().unwrap()),
2382 organization_id: None,
2383 folder_id: None,
2384 collection_ids: vec![],
2385 key: None,
2386 name: TEST_CIPHER_NAME.parse().unwrap(),
2387 notes: None,
2388 r#type: CipherType::Login,
2389 login: None,
2390 identity: None,
2391 card: None,
2392 secure_note: None,
2393 ssh_key: None,
2394 bank_account: None,
2395 drivers_license: None,
2396 passport: None,
2397 favorite: false,
2398 reprompt: CipherRepromptType::None,
2399 organization_use_totp: false,
2400 edit: true,
2401 view_password: true,
2402 permissions: None,
2403 local_data: None,
2404 attachments: None,
2405 fields: None,
2406 password_history: None,
2407 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2408 deleted_date: None,
2409 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2410 archived_date: None,
2411 data: Some(format!(
2412 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2413 TEST_ENC_STRING_1, TEST_ENC_STRING_2
2414 )),
2415 };
2416
2417 cipher
2418 .populate_cipher_types()
2419 .expect("populate_cipher_types failed");
2420
2421 assert!(cipher.login.is_some());
2422 let login = cipher.login.unwrap();
2423 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
2424 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
2425 }
2426
2427 #[test]
2428 fn test_populate_cipher_types_secure_note() {
2429 let mut cipher = Cipher {
2430 id: Some(TEST_UUID.parse().unwrap()),
2431 organization_id: None,
2432 folder_id: None,
2433 collection_ids: vec![],
2434 key: None,
2435 name: TEST_CIPHER_NAME.parse().unwrap(),
2436 notes: None,
2437 r#type: CipherType::SecureNote,
2438 login: None,
2439 identity: None,
2440 card: None,
2441 secure_note: None,
2442 ssh_key: None,
2443 bank_account: None,
2444 drivers_license: None,
2445 passport: None,
2446 favorite: false,
2447 reprompt: CipherRepromptType::None,
2448 organization_use_totp: false,
2449 edit: true,
2450 view_password: true,
2451 permissions: None,
2452 local_data: None,
2453 attachments: None,
2454 fields: None,
2455 password_history: None,
2456 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2457 deleted_date: None,
2458 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2459 archived_date: None,
2460 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
2461 };
2462
2463 cipher
2464 .populate_cipher_types()
2465 .expect("populate_cipher_types failed");
2466
2467 assert!(cipher.secure_note.is_some());
2468 }
2469
2470 #[test]
2471 fn test_populate_cipher_types_card() {
2472 let mut cipher = Cipher {
2473 id: Some(TEST_UUID.parse().unwrap()),
2474 organization_id: None,
2475 folder_id: None,
2476 collection_ids: vec![],
2477 key: None,
2478 name: TEST_CIPHER_NAME.parse().unwrap(),
2479 notes: None,
2480 r#type: CipherType::Card,
2481 login: None,
2482 identity: None,
2483 card: None,
2484 secure_note: None,
2485 ssh_key: None,
2486 bank_account: None,
2487 drivers_license: None,
2488 passport: None,
2489 favorite: false,
2490 reprompt: CipherRepromptType::None,
2491 organization_use_totp: false,
2492 edit: true,
2493 view_password: true,
2494 permissions: None,
2495 local_data: None,
2496 attachments: None,
2497 fields: None,
2498 password_history: None,
2499 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2500 deleted_date: None,
2501 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2502 archived_date: None,
2503 data: Some(format!(
2504 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2505 TEST_ENC_STRING_1,
2506 TEST_ENC_STRING_2,
2507 TEST_ENC_STRING_3,
2508 TEST_ENC_STRING_4,
2509 TEST_ENC_STRING_5,
2510 TEST_ENC_STRING_1
2511 )),
2512 };
2513
2514 cipher
2515 .populate_cipher_types()
2516 .expect("populate_cipher_types failed");
2517
2518 assert!(cipher.card.is_some());
2519 let card = cipher.card.unwrap();
2520 assert_eq!(
2521 card.cardholder_name.as_ref().unwrap().to_string(),
2522 TEST_ENC_STRING_1
2523 );
2524 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
2525 assert_eq!(
2526 card.exp_month.as_ref().unwrap().to_string(),
2527 TEST_ENC_STRING_3
2528 );
2529 assert_eq!(
2530 card.exp_year.as_ref().unwrap().to_string(),
2531 TEST_ENC_STRING_4
2532 );
2533 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
2534 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
2535 }
2536
2537 #[test]
2538 fn test_populate_cipher_types_identity() {
2539 let mut cipher = Cipher {
2540 id: Some(TEST_UUID.parse().unwrap()),
2541 organization_id: None,
2542 folder_id: None,
2543 collection_ids: vec![],
2544 key: None,
2545 name: TEST_CIPHER_NAME.parse().unwrap(),
2546 notes: None,
2547 r#type: CipherType::Identity,
2548 login: None,
2549 identity: None,
2550 card: None,
2551 secure_note: None,
2552 ssh_key: None,
2553 bank_account: None,
2554 drivers_license: None,
2555 passport: None,
2556 favorite: false,
2557 reprompt: CipherRepromptType::None,
2558 organization_use_totp: false,
2559 edit: true,
2560 view_password: true,
2561 permissions: None,
2562 local_data: None,
2563 attachments: None,
2564 fields: None,
2565 password_history: None,
2566 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2567 deleted_date: None,
2568 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2569 archived_date: None,
2570 data: Some(format!(
2571 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2572 TEST_ENC_STRING_1,
2573 TEST_ENC_STRING_2,
2574 TEST_ENC_STRING_3,
2575 TEST_ENC_STRING_4,
2576 TEST_ENC_STRING_5,
2577 TEST_ENC_STRING_1,
2578 TEST_ENC_STRING_2,
2579 TEST_ENC_STRING_3,
2580 TEST_ENC_STRING_4,
2581 TEST_ENC_STRING_5
2582 )),
2583 };
2584
2585 cipher
2586 .populate_cipher_types()
2587 .expect("populate_cipher_types failed");
2588
2589 assert!(cipher.identity.is_some());
2590 let identity = cipher.identity.unwrap();
2591 assert_eq!(
2592 identity.first_name.as_ref().unwrap().to_string(),
2593 TEST_ENC_STRING_1
2594 );
2595 assert_eq!(
2596 identity.last_name.as_ref().unwrap().to_string(),
2597 TEST_ENC_STRING_2
2598 );
2599 assert_eq!(
2600 identity.email.as_ref().unwrap().to_string(),
2601 TEST_ENC_STRING_3
2602 );
2603 assert_eq!(
2604 identity.phone.as_ref().unwrap().to_string(),
2605 TEST_ENC_STRING_4
2606 );
2607 assert_eq!(
2608 identity.company.as_ref().unwrap().to_string(),
2609 TEST_ENC_STRING_5
2610 );
2611 assert_eq!(
2612 identity.address1.as_ref().unwrap().to_string(),
2613 TEST_ENC_STRING_1
2614 );
2615 assert_eq!(
2616 identity.city.as_ref().unwrap().to_string(),
2617 TEST_ENC_STRING_2
2618 );
2619 assert_eq!(
2620 identity.state.as_ref().unwrap().to_string(),
2621 TEST_ENC_STRING_3
2622 );
2623 assert_eq!(
2624 identity.postal_code.as_ref().unwrap().to_string(),
2625 TEST_ENC_STRING_4
2626 );
2627 assert_eq!(
2628 identity.country.as_ref().unwrap().to_string(),
2629 TEST_ENC_STRING_5
2630 );
2631 }
2632
2633 #[test]
2634
2635 fn test_password_history_with_hidden_fields() {
2636 let mut original_cipher = generate_cipher();
2637 original_cipher.fields = Some(vec![FieldView {
2638 name: Some("Secret Key".to_string()),
2639 value: Some("old_secret_value".to_string()),
2640 r#type: crate::FieldType::Hidden,
2641 linked_id: None,
2642 }]);
2643
2644 let mut new_cipher = generate_cipher();
2645 new_cipher.fields = Some(vec![FieldView {
2646 name: Some("Secret Key".to_string()),
2647 value: Some("new_secret_value".to_string()),
2648 r#type: crate::FieldType::Hidden,
2649 linked_id: None,
2650 }]);
2651
2652 new_cipher.update_password_history(&original_cipher);
2653
2654 assert!(new_cipher.password_history.is_some());
2655 let history = new_cipher.password_history.unwrap();
2656 assert_eq!(history.len(), 1);
2657 assert_eq!(history[0].password, "Secret Key: old_secret_value");
2658 }
2659
2660 #[test]
2661 fn test_password_history_length_limit() {
2662 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
2663
2664 let mut original_cipher = generate_cipher();
2665 original_cipher.password_history = Some(
2666 (0..10)
2667 .map(|i| PasswordHistoryView {
2668 password: format!("old_password_{}", i),
2669 last_used_date: chrono::Utc::now(),
2670 })
2671 .collect(),
2672 );
2673
2674 let mut new_cipher = original_cipher.clone();
2675 if let Some(ref mut login) = new_cipher.login {
2677 login.password = Some("brand_new_password".to_string());
2678 }
2679
2680 new_cipher.update_password_history(&original_cipher);
2681
2682 assert!(new_cipher.password_history.is_some());
2683 let history = new_cipher.password_history.unwrap();
2684
2685 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
2687
2688 assert_eq!(history[0].password, "test_password");
2690 assert_eq!(history[1].password, "old_password_0");
2692 assert_eq!(history[2].password, "old_password_1");
2693 assert_eq!(history[3].password, "old_password_2");
2694 assert_eq!(history[4].password, "old_password_3");
2695 }
2696
2697 #[test]
2698 fn test_populate_cipher_types_ssh_key() {
2699 let mut cipher = Cipher {
2700 id: Some(TEST_UUID.parse().unwrap()),
2701 organization_id: None,
2702 folder_id: None,
2703 collection_ids: vec![],
2704 key: None,
2705 name: TEST_CIPHER_NAME.parse().unwrap(),
2706 notes: None,
2707 r#type: CipherType::SshKey,
2708 login: None,
2709 identity: None,
2710 card: None,
2711 secure_note: None,
2712 ssh_key: None,
2713 bank_account: None,
2714 drivers_license: None,
2715 passport: None,
2716 favorite: false,
2717 reprompt: CipherRepromptType::None,
2718 organization_use_totp: false,
2719 edit: true,
2720 view_password: true,
2721 permissions: None,
2722 local_data: None,
2723 attachments: None,
2724 fields: None,
2725 password_history: None,
2726 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2727 deleted_date: None,
2728 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2729 archived_date: None,
2730 data: Some(format!(
2731 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2732 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2733 )),
2734 };
2735
2736 cipher
2737 .populate_cipher_types()
2738 .expect("populate_cipher_types failed");
2739
2740 assert!(cipher.ssh_key.is_some());
2741 let ssh_key = cipher.ssh_key.unwrap();
2742 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2743 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2744 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2745 }
2746
2747 #[test]
2748 fn test_populate_cipher_types_with_null_data() {
2749 let mut cipher = Cipher {
2750 id: Some(TEST_UUID.parse().unwrap()),
2751 organization_id: None,
2752 folder_id: None,
2753 collection_ids: vec![],
2754 key: None,
2755 name: TEST_CIPHER_NAME.parse().unwrap(),
2756 notes: None,
2757 r#type: CipherType::Login,
2758 login: None,
2759 identity: None,
2760 card: None,
2761 secure_note: None,
2762 ssh_key: None,
2763 bank_account: None,
2764 drivers_license: None,
2765 passport: None,
2766 favorite: false,
2767 reprompt: CipherRepromptType::None,
2768 organization_use_totp: false,
2769 edit: true,
2770 view_password: true,
2771 permissions: None,
2772 local_data: None,
2773 attachments: None,
2774 fields: None,
2775 password_history: None,
2776 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2777 deleted_date: None,
2778 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2779 archived_date: None,
2780 data: None,
2781 };
2782
2783 let result = cipher.populate_cipher_types();
2784 assert!(matches!(
2785 result,
2786 Err(VaultParseError::MissingField(MissingFieldError("data")))
2787 ));
2788 }
2789
2790 #[test]
2791 fn test_populate_cipher_types_with_invalid_json() {
2792 let mut cipher = Cipher {
2793 id: Some(TEST_UUID.parse().unwrap()),
2794 organization_id: None,
2795 folder_id: None,
2796 collection_ids: vec![],
2797 key: None,
2798 name: TEST_CIPHER_NAME.parse().unwrap(),
2799 notes: None,
2800 r#type: CipherType::Login,
2801 login: None,
2802 identity: None,
2803 card: None,
2804 secure_note: None,
2805 ssh_key: None,
2806 bank_account: None,
2807 drivers_license: None,
2808 passport: None,
2809 favorite: false,
2810 reprompt: CipherRepromptType::None,
2811 organization_use_totp: false,
2812 edit: true,
2813 view_password: true,
2814 permissions: None,
2815 local_data: None,
2816 attachments: None,
2817 fields: None,
2818 password_history: None,
2819 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2820 deleted_date: None,
2821 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2822 archived_date: None,
2823 data: Some("invalid json".to_string()),
2824 };
2825
2826 let result = cipher.populate_cipher_types();
2827
2828 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2829 }
2830
2831 #[test]
2832 fn test_decrypt_cipher_with_mixed_attachments() {
2833 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2834 let key_store = create_test_crypto_with_user_key(user_key);
2835
2836 let mut ctx = key_store.context();
2838 let valid1 = "valid_file_1.txt"
2839 .encrypt(&mut ctx, SymmetricKeySlotId::User)
2840 .unwrap();
2841 let valid2 = "valid_file_2.txt"
2842 .encrypt(&mut ctx, SymmetricKeySlotId::User)
2843 .unwrap();
2844
2845 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
2847 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
2848 let mut wrong_ctx = wrong_key_store.context();
2849 let corrupted = "corrupted_file.txt"
2850 .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
2851 .unwrap();
2852
2853 let cipher = Cipher {
2854 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2855 organization_id: None,
2856 folder_id: None,
2857 collection_ids: vec![],
2858 key: None,
2859 name: TEST_CIPHER_NAME.parse().unwrap(),
2860 notes: None,
2861 r#type: CipherType::Login,
2862 login: None,
2863 identity: None,
2864 card: None,
2865 secure_note: None,
2866 ssh_key: None,
2867 bank_account: None,
2868 drivers_license: None,
2869 passport: None,
2870 favorite: false,
2871 reprompt: CipherRepromptType::None,
2872 organization_use_totp: false,
2873 edit: true,
2874 permissions: None,
2875 view_password: true,
2876 local_data: None,
2877 attachments: Some(vec![
2878 attachment::Attachment {
2880 id: Some("valid-attachment".to_string()),
2881 url: Some("https://example.com/valid".to_string()),
2882 size: Some("100".to_string()),
2883 size_name: Some("100 Bytes".to_string()),
2884 file_name: Some(valid1),
2885 key: None,
2886 },
2887 attachment::Attachment {
2889 id: Some("corrupted-attachment".to_string()),
2890 url: Some("https://example.com/corrupted".to_string()),
2891 size: Some("200".to_string()),
2892 size_name: Some("200 Bytes".to_string()),
2893 file_name: Some(corrupted),
2894 key: None,
2895 },
2896 attachment::Attachment {
2898 id: Some("valid-attachment-2".to_string()),
2899 url: Some("https://example.com/valid2".to_string()),
2900 size: Some("150".to_string()),
2901 size_name: Some("150 Bytes".to_string()),
2902 file_name: Some(valid2),
2903 key: None,
2904 },
2905 ]),
2906 fields: None,
2907 password_history: None,
2908 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2909 deleted_date: None,
2910 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2911 archived_date: None,
2912 data: None,
2913 };
2914
2915 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2916
2917 assert!(view.attachments.is_some());
2919 let successes = view.attachments.as_ref().unwrap();
2920 assert_eq!(successes.len(), 2);
2921 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
2922 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
2923
2924 assert!(view.attachment_decryption_failures.is_some());
2926 let failures = view.attachment_decryption_failures.as_ref().unwrap();
2927 assert_eq!(failures.len(), 1);
2928 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
2929 assert_eq!(failures[0].file_name, None);
2930 }
2931
2932 #[test]
2933 fn test_decrypt_cipher_list_view_passport() {
2934 let key_store =
2935 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2936
2937 let cipher_view = CipherView {
2938 r#type: CipherType::Passport,
2939 passport: Some(passport::PassportView {
2940 given_name: Some("Jane".to_string()),
2941 surname: Some("Doe".to_string()),
2942 passport_number: Some("P12345678".to_string()),
2943 ..Default::default()
2944 }),
2945 login: None,
2946 ..generate_cipher()
2947 };
2948
2949 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2950 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2951
2952 assert_eq!(list_view.r#type, CipherListViewType::Passport);
2953 assert_eq!(list_view.subtitle, "Jane Doe");
2954 assert_eq!(
2955 list_view.copyable_fields,
2956 vec![
2957 CopyableCipherFields::PassportGivenName,
2958 CopyableCipherFields::PassportSurname,
2959 CopyableCipherFields::PassportPassportNumber,
2960 ]
2961 );
2962 }
2963
2964 #[test]
2965 fn test_decrypt_cipher_list_view_drivers_license() {
2966 let key_store =
2967 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2968
2969 let cipher_view = CipherView {
2970 r#type: CipherType::DriversLicense,
2971 drivers_license: Some(drivers_license::DriversLicenseView {
2972 first_name: Some("John".to_string()),
2973 last_name: Some("Doe".to_string()),
2974 license_number: Some("DL-987654".to_string()),
2975 ..Default::default()
2976 }),
2977 login: None,
2978 ..generate_cipher()
2979 };
2980
2981 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2982 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2983
2984 assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
2985 assert_eq!(list_view.subtitle, "John Doe");
2986 assert_eq!(
2987 list_view.copyable_fields,
2988 vec![
2989 CopyableCipherFields::DriversLicenseFirstName,
2990 CopyableCipherFields::DriversLicenseLastName,
2991 CopyableCipherFields::DriversLicenseLicenseNumber,
2992 ]
2993 );
2994 }
2995
2996 #[test]
2997 fn test_cipher_view_encrypt_decrypt_passport() {
2998 let key_store =
2999 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
3000
3001 let passport = passport::PassportView {
3002 given_name: Some("Jane".to_string()),
3003 surname: Some("Doe".to_string()),
3004 date_of_birth: Some("1990-01-01".to_string()),
3005 sex: Some("F".to_string()),
3006 birth_place: Some("New York".to_string()),
3007 nationality: Some("American".to_string()),
3008 issuing_country: Some("US".to_string()),
3009 passport_number: Some("P12345678".to_string()),
3010 passport_type: Some("P".to_string()),
3011 national_identification_number: Some("123-45-6789".to_string()),
3012 issuing_authority: Some("US State Department".to_string()),
3013 issue_date: Some("2020-01-01".to_string()),
3014 expiration_date: Some("2030-01-01".to_string()),
3015 };
3016
3017 let cipher_view = CipherView {
3018 r#type: CipherType::Passport,
3019 passport: Some(passport.clone()),
3020 login: None,
3021 ..generate_cipher()
3022 };
3023
3024 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3025 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3026
3027 assert_eq!(decrypted.r#type, CipherType::Passport);
3028 assert_eq!(decrypted.passport, Some(passport));
3029 assert!(decrypted.login.is_none());
3030 }
3031
3032 #[test]
3033 fn test_cipher_view_encrypt_decrypt_drivers_license() {
3034 let key_store =
3035 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
3036
3037 let dl = drivers_license::DriversLicenseView {
3038 first_name: Some("John".to_string()),
3039 middle_name: Some("Michael".to_string()),
3040 last_name: Some("Doe".to_string()),
3041 date_of_birth: Some("1985-06-15".to_string()),
3042 license_number: Some("DL-987654".to_string()),
3043 issuing_country: Some("US".to_string()),
3044 issuing_state: Some("NY".to_string()),
3045 issue_date: Some("2020-01-01".to_string()),
3046 expiration_date: Some("2028-01-01".to_string()),
3047 issuing_authority: Some("NY DMV".to_string()),
3048 license_class: Some("D".to_string()),
3049 };
3050
3051 let cipher_view = CipherView {
3052 r#type: CipherType::DriversLicense,
3053 drivers_license: Some(dl.clone()),
3054 login: None,
3055 ..generate_cipher()
3056 };
3057
3058 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3059 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3060
3061 assert_eq!(decrypted.r#type, CipherType::DriversLicense);
3062 assert_eq!(decrypted.drivers_license, Some(dl));
3063 assert!(decrypted.login.is_none());
3064 }
3065
3066 #[test]
3067 fn test_mini_response_model_view_password_defaults_to_true() {
3068 use chrono::Utc;
3069
3070 let mini_response = CipherMiniResponseModel {
3073 id: Some(TEST_UUID.parse().unwrap()),
3074 name: Some(TEST_CIPHER_NAME.to_string()),
3075 r#type: Some(bitwarden_api_api::models::CipherType::Login),
3076 creation_date: Some(Utc::now().to_rfc3339()),
3077 revision_date: Some(Utc::now().to_rfc3339()),
3078 ..Default::default()
3079 };
3080
3081 let cipher = mini_response.merge_with_cipher(None).unwrap();
3082 assert!(
3083 cipher.view_password,
3084 "view_password should default to true for CipherMiniResponseModel"
3085 );
3086
3087 let mini_details_response = CipherMiniDetailsResponseModel {
3089 id: Some(TEST_UUID.parse().unwrap()),
3090 name: Some(TEST_CIPHER_NAME.to_string()),
3091 r#type: Some(bitwarden_api_api::models::CipherType::Login),
3092 creation_date: Some(Utc::now().to_rfc3339()),
3093 revision_date: Some(Utc::now().to_rfc3339()),
3094 ..Default::default()
3095 };
3096
3097 let cipher = mini_details_response.merge_with_cipher(None).unwrap();
3098 assert!(
3099 cipher.view_password,
3100 "view_password should default to true for CipherMiniDetailsResponseModel"
3101 );
3102 }
3103}