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 BankAccountAccountNumber,
498 BankAccountRoutingNumber,
499 BankAccountPin,
500 BankAccountIban,
501 PassportPassportNumber,
502 DriversLicenseLicenseNumber,
503}
504
505#[allow(missing_docs)]
506#[derive(Serialize, Deserialize, Debug, PartialEq)]
507#[serde(rename_all = "camelCase", deny_unknown_fields)]
508#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
509#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
510pub struct CipherListView {
511 pub id: Option<CipherId>,
512 pub organization_id: Option<OrganizationId>,
513 pub folder_id: Option<FolderId>,
514 pub collection_ids: Vec<CollectionId>,
515
516 pub key: Option<EncString>,
518
519 pub name: String,
520 pub subtitle: String,
521
522 pub r#type: CipherListViewType,
523
524 pub favorite: bool,
525 pub reprompt: CipherRepromptType,
526 pub organization_use_totp: bool,
527 pub edit: bool,
528 pub permissions: Option<CipherPermissions>,
529
530 pub view_password: bool,
531
532 pub attachments: u32,
534 pub has_old_attachments: bool,
536
537 pub creation_date: DateTime<Utc>,
538 pub deleted_date: Option<DateTime<Utc>>,
539 pub revision_date: DateTime<Utc>,
540 pub archived_date: Option<DateTime<Utc>>,
541
542 pub copyable_fields: Vec<CopyableCipherFields>,
544
545 pub local_data: Option<LocalDataView>,
546
547 #[cfg(feature = "wasm")]
549 pub notes: Option<String>,
550 #[cfg(feature = "wasm")]
553 pub fields: Option<Vec<field::FieldListView>>,
554 #[cfg(feature = "wasm")]
556 pub attachment_names: Option<Vec<String>>,
557}
558
559#[derive(Serialize, Deserialize, Debug)]
565#[serde(rename_all = "camelCase", deny_unknown_fields)]
566#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
567#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
568pub struct DecryptCipherListResult {
569 pub successes: Vec<CipherListView>,
571 pub failures: Vec<Cipher>,
573}
574
575#[derive(Serialize, Deserialize, Debug)]
581#[serde(rename_all = "camelCase", deny_unknown_fields)]
582#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
583#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
584pub struct DecryptCipherResult {
585 pub successes: Vec<CipherView>,
587 pub failures: Vec<Cipher>,
589}
590
591#[derive(Serialize, Deserialize, Debug)]
595#[serde(rename_all = "camelCase", deny_unknown_fields)]
596#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
597#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
598pub struct ListOrganizationCiphersResult {
599 pub ciphers: Vec<Cipher>,
601 pub list_views: Vec<CipherListView>,
603}
604
605impl CipherListView {
606 pub(crate) fn get_totp_key(
607 self,
608 ctx: &mut KeyStoreContext<KeySlotIds>,
609 ) -> Result<Option<String>, CryptoError> {
610 let key = self.key_identifier();
611 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
612
613 let totp = match self.r#type {
614 CipherListViewType::Login(LoginListView { totp, .. }) => {
615 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
616 }
617 _ => None,
618 };
619
620 Ok(totp)
621 }
622}
623
624impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Cipher> for CipherView {
625 fn encrypt_composite(
626 &self,
627 ctx: &mut KeyStoreContext<KeySlotIds>,
628 key: SymmetricKeySlotId,
629 ) -> Result<Cipher, CryptoError> {
630 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
631
632 let mut cipher_view = self.clone();
633 cipher_view.generate_checksums();
634
635 Ok(Cipher {
636 id: cipher_view.id,
637 organization_id: cipher_view.organization_id,
638 folder_id: cipher_view.folder_id,
639 collection_ids: cipher_view.collection_ids,
640 key: cipher_view.key,
641 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
642 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
643 r#type: cipher_view.r#type,
644 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
645 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
646 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
647 secure_note: cipher_view
648 .secure_note
649 .encrypt_composite(ctx, ciphers_key)?,
650 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
651 bank_account: cipher_view
652 .bank_account
653 .encrypt_composite(ctx, ciphers_key)?,
654 drivers_license: cipher_view
655 .drivers_license
656 .encrypt_composite(ctx, ciphers_key)?,
657 passport: cipher_view.passport.encrypt_composite(ctx, ciphers_key)?,
658 favorite: cipher_view.favorite,
659 reprompt: cipher_view.reprompt,
660 organization_use_totp: cipher_view.organization_use_totp,
661 edit: cipher_view.edit,
662 view_password: cipher_view.view_password,
663 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
664 attachments: cipher_view
665 .attachments
666 .encrypt_composite(ctx, ciphers_key)?,
667 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
668 password_history: cipher_view
669 .password_history
670 .encrypt_composite(ctx, ciphers_key)?,
671 creation_date: cipher_view.creation_date,
672 deleted_date: cipher_view.deleted_date,
673 revision_date: cipher_view.revision_date,
674 permissions: cipher_view.permissions,
675 archived_date: cipher_view.archived_date,
676 data: None, })
678 }
679}
680
681impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for Cipher {
682 #[instrument(err, skip_all, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
683 fn decrypt(
684 &self,
685 ctx: &mut KeyStoreContext<KeySlotIds>,
686 key: SymmetricKeySlotId,
687 ) -> Result<CipherView, CryptoError> {
688 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
689
690 let (attachments, attachment_decryption_failures) =
692 attachment::decrypt_attachments_with_failures(
693 self.attachments.as_deref().unwrap_or_default(),
694 ctx,
695 ciphers_key,
696 );
697
698 let mut cipher = CipherView {
699 id: self.id,
700 organization_id: self.organization_id,
701 folder_id: self.folder_id,
702 collection_ids: self.collection_ids.clone(),
703 key: self.key.clone(),
704 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
705 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
706 r#type: self.r#type,
707 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
708 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
709 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
710 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
711 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
712 bank_account: self.bank_account.decrypt(ctx, ciphers_key).ok().flatten(),
713 drivers_license: self
714 .drivers_license
715 .decrypt(ctx, ciphers_key)
716 .ok()
717 .flatten(),
718 passport: self.passport.decrypt(ctx, ciphers_key).ok().flatten(),
719 favorite: self.favorite,
720 reprompt: self.reprompt,
721 organization_use_totp: self.organization_use_totp,
722 edit: self.edit,
723 permissions: self.permissions,
724 view_password: self.view_password,
725 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
726 attachments: Some(attachments),
727 attachment_decryption_failures: Some(attachment_decryption_failures),
728 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
729 password_history: self
730 .password_history
731 .decrypt(ctx, ciphers_key)
732 .ok()
733 .flatten(),
734 creation_date: self.creation_date,
735 deleted_date: self.deleted_date,
736 revision_date: self.revision_date,
737 archived_date: self.archived_date,
738 };
739
740 if cipher.key.is_some()
743 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
744 {
745 cipher.remove_invalid_checksums();
746 }
747
748 Ok(cipher)
749 }
750}
751
752impl Cipher {
753 #[instrument(err, skip_all)]
764 pub(crate) fn decrypt_cipher_key(
765 ctx: &mut KeyStoreContext<KeySlotIds>,
766 key: SymmetricKeySlotId,
767 ciphers_key: &Option<EncString>,
768 ) -> Result<SymmetricKeySlotId, CryptoError> {
769 match ciphers_key {
770 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
771 None => Ok(key),
772 }
773 }
774
775 fn get_kind(&self) -> Option<&dyn CipherKind> {
777 match self.r#type {
778 CipherType::Login => self.login.as_ref().map(|v| v as _),
779 CipherType::Card => self.card.as_ref().map(|v| v as _),
780 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
781 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
782 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
783 CipherType::BankAccount => self.bank_account.as_ref().map(|v| v as _),
784 CipherType::DriversLicense => self.drivers_license.as_ref().map(|v| v as _),
785 CipherType::Passport => self.passport.as_ref().map(|v| v as _),
786 }
787 }
788
789 fn decrypt_subtitle(
791 &self,
792 ctx: &mut KeyStoreContext<KeySlotIds>,
793 key: SymmetricKeySlotId,
794 ) -> Result<String, CryptoError> {
795 self.get_kind()
796 .map(|sub| sub.decrypt_subtitle(ctx, key))
797 .unwrap_or_else(|| Ok(String::new()))
798 }
799
800 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
803 self.get_kind()
804 .map(|kind| kind.get_copyable_fields(Some(self)))
805 .unwrap_or_default()
806 }
807
808 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
814 let data = self
815 .data
816 .as_ref()
817 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
818
819 match &self.r#type {
820 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
821 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
822 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
823 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
824 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
825 crate::CipherType::BankAccount => self.bank_account = serde_json::from_str(data)?,
826 crate::CipherType::DriversLicense => self.drivers_license = serde_json::from_str(data)?,
827 crate::CipherType::Passport => self.passport = serde_json::from_str(data)?,
828 }
829 Ok(())
830 }
831
832 pub(crate) fn soft_delete(&mut self) {
834 self.deleted_date = Some(Utc::now());
835 }
836}
837impl CipherView {
838 #[allow(missing_docs)]
839 pub fn generate_cipher_key(
840 &mut self,
841 ctx: &mut KeyStoreContext<KeySlotIds>,
842 wrapping_key: SymmetricKeySlotId,
843 ) -> Result<(), CryptoError> {
844 let old_unwrapping_key = self.key_identifier();
845 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, old_unwrapping_key, &self.key)?;
846
847 let new_key = ctx.generate_symmetric_key();
848
849 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
850 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
851
852 self.key = Some(ctx.wrap_symmetric_key(wrapping_key, new_key)?);
853 Ok(())
854 }
855
856 #[allow(missing_docs)]
857 pub fn generate_checksums(&mut self) {
858 if let Some(l) = self.login.as_mut() {
859 l.generate_checksums();
860 }
861 }
862
863 #[allow(missing_docs)]
864 pub fn remove_invalid_checksums(&mut self) {
865 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
866 uris.retain(|u| u.is_checksum_valid());
867 }
868 }
869
870 fn reencrypt_attachment_keys(
871 &mut self,
872 ctx: &mut KeyStoreContext<KeySlotIds>,
873 old_key: SymmetricKeySlotId,
874 new_key: SymmetricKeySlotId,
875 ) -> Result<(), CryptoError> {
876 if let Some(attachments) = &mut self.attachments {
877 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
878 }
879 Ok(())
880 }
881
882 #[allow(missing_docs)]
883 pub fn decrypt_fido2_credentials(
884 &self,
885 ctx: &mut KeyStoreContext<KeySlotIds>,
886 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
887 let key = self.key_identifier();
888 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
889
890 Ok(self
891 .login
892 .as_ref()
893 .and_then(|l| l.fido2_credentials.as_ref())
894 .map(|f| f.decrypt(ctx, ciphers_key))
895 .transpose()?
896 .unwrap_or_default())
897 }
898
899 fn reencrypt_fido2_credentials(
900 &mut self,
901 ctx: &mut KeyStoreContext<KeySlotIds>,
902 old_key: SymmetricKeySlotId,
903 new_key: SymmetricKeySlotId,
904 ) -> Result<(), CryptoError> {
905 if let Some(login) = self.login.as_mut() {
906 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
907 }
908 Ok(())
909 }
910
911 pub fn move_to_organization(
918 &mut self,
919 ctx: &mut KeyStoreContext<KeySlotIds>,
920 organization_id: OrganizationId,
921 ) -> Result<(), CipherError> {
922 let new_key = SymmetricKeySlotId::Organization(organization_id);
923
924 self.reencrypt_cipher_keys(ctx, new_key)?;
925 self.organization_id = Some(organization_id);
926
927 Ok(())
928 }
929
930 pub fn reencrypt_cipher_keys(
935 &mut self,
936 ctx: &mut KeyStoreContext<KeySlotIds>,
937 new_wrapping_key: SymmetricKeySlotId,
938 ) -> Result<(), CipherError> {
939 let old_key = self.key_identifier();
940
941 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
943 return Err(CipherError::AttachmentsWithoutKeys);
944 }
945
946 if self.key.is_some() {
948 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
950
951 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
953 } else {
954 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
957 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
958 }
959
960 Ok(())
961 }
962
963 #[allow(missing_docs)]
964 pub fn set_new_fido2_credentials(
965 &mut self,
966 ctx: &mut KeyStoreContext<KeySlotIds>,
967 creds: Vec<Fido2CredentialFullView>,
968 ) -> Result<(), CipherError> {
969 let key = self.key_identifier();
970
971 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
972
973 require!(self.login.as_mut()).fido2_credentials =
974 Some(creds.encrypt_composite(ctx, ciphers_key)?);
975
976 Ok(())
977 }
978
979 #[allow(missing_docs)]
980 pub fn get_fido2_credentials(
981 &self,
982 ctx: &mut KeyStoreContext<KeySlotIds>,
983 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
984 let key = self.key_identifier();
985
986 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
987
988 let login = require!(self.login.as_ref());
989 let creds = require!(login.fido2_credentials.as_ref());
990 let res = creds.decrypt(ctx, ciphers_key)?;
991 Ok(res)
992 }
993
994 #[allow(missing_docs)]
995 pub fn decrypt_fido2_private_key(
996 &self,
997 ctx: &mut KeyStoreContext<KeySlotIds>,
998 ) -> Result<String, CipherError> {
999 let fido2_credential = self.get_fido2_credentials(ctx)?;
1000
1001 Ok(fido2_credential[0].key_value.clone())
1002 }
1003
1004 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
1005 let changes = self
1006 .login
1007 .as_mut()
1008 .map_or(vec![], |login| {
1009 login.detect_password_change(&original_cipher.login)
1010 })
1011 .into_iter()
1012 .chain(self.fields.as_deref().map_or(vec![], |fields| {
1013 FieldView::detect_hidden_field_changes(
1014 fields,
1015 original_cipher.fields.as_deref().unwrap_or(&[]),
1016 )
1017 }))
1018 .rev()
1019 .chain(original_cipher.password_history.iter().flatten().cloned())
1020 .take(MAX_PASSWORD_HISTORY_ENTRIES)
1021 .collect();
1022 self.password_history = Some(changes)
1023 }
1024}
1025
1026impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for Cipher {
1027 fn decrypt(
1028 &self,
1029 ctx: &mut KeyStoreContext<KeySlotIds>,
1030 key: SymmetricKeySlotId,
1031 ) -> Result<CipherListView, CryptoError> {
1032 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1033
1034 Ok(CipherListView {
1035 id: self.id,
1036 organization_id: self.organization_id,
1037 folder_id: self.folder_id,
1038 collection_ids: self.collection_ids.clone(),
1039 key: self.key.clone(),
1040 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
1041 subtitle: self
1042 .decrypt_subtitle(ctx, ciphers_key)
1043 .ok()
1044 .unwrap_or_default(),
1045 r#type: match self.r#type {
1046 CipherType::Login => {
1047 let login = self
1048 .login
1049 .as_ref()
1050 .ok_or(CryptoError::MissingField("login"))?;
1051 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
1052 }
1053 CipherType::SecureNote => CipherListViewType::SecureNote,
1054 CipherType::Card => {
1055 let card = self
1056 .card
1057 .as_ref()
1058 .ok_or(CryptoError::MissingField("card"))?;
1059 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
1060 }
1061 CipherType::Identity => CipherListViewType::Identity,
1062 CipherType::SshKey => CipherListViewType::SshKey,
1063 CipherType::BankAccount => CipherListViewType::BankAccount,
1064 CipherType::Passport => CipherListViewType::Passport,
1065 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1066 },
1067 favorite: self.favorite,
1068 reprompt: self.reprompt,
1069 organization_use_totp: self.organization_use_totp,
1070 edit: self.edit,
1071 permissions: self.permissions,
1072 view_password: self.view_password,
1073 attachments: self
1074 .attachments
1075 .as_ref()
1076 .map(|a| a.len() as u32)
1077 .unwrap_or(0),
1078 has_old_attachments: self
1079 .attachments
1080 .as_ref()
1081 .map(|a| a.iter().any(|att| att.key.is_none()))
1082 .unwrap_or(false),
1083 creation_date: self.creation_date,
1084 deleted_date: self.deleted_date,
1085 revision_date: self.revision_date,
1086 copyable_fields: self.get_copyable_fields(),
1087 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
1088 archived_date: self.archived_date,
1089 #[cfg(feature = "wasm")]
1090 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
1091 #[cfg(feature = "wasm")]
1092 fields: self.fields.as_ref().map(|fields| {
1093 fields
1094 .iter()
1095 .filter_map(|f| {
1096 f.decrypt(ctx, ciphers_key)
1097 .ok()
1098 .map(field::FieldListView::from)
1099 })
1100 .collect()
1101 }),
1102 #[cfg(feature = "wasm")]
1103 attachment_names: self.attachments.as_ref().map(|attachments| {
1104 attachments
1105 .iter()
1106 .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
1107 .collect()
1108 }),
1109 })
1110 }
1111}
1112
1113impl IdentifyKey<SymmetricKeySlotId> for Cipher {
1114 fn key_identifier(&self) -> SymmetricKeySlotId {
1115 match self.organization_id {
1116 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1117 None => SymmetricKeySlotId::User,
1118 }
1119 }
1120}
1121
1122impl IdentifyKey<SymmetricKeySlotId> for CipherView {
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 CipherListView {
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
1140pub(crate) struct StrictDecrypt<T>(pub(crate) T);
1149
1150impl IdentifyKey<SymmetricKeySlotId> for StrictDecrypt<Cipher> {
1151 fn key_identifier(&self) -> SymmetricKeySlotId {
1152 self.0.key_identifier()
1153 }
1154}
1155
1156impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for StrictDecrypt<Cipher> {
1157 #[instrument(err, skip_all, fields(cipher_id = ?self.0.id, org_id = ?self.0.organization_id, kind = ?self.0.r#type))]
1158 fn decrypt(
1159 &self,
1160 ctx: &mut KeyStoreContext<KeySlotIds>,
1161 key: SymmetricKeySlotId,
1162 ) -> Result<CipherView, CryptoError> {
1163 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1164
1165 let (attachments, attachment_decryption_failures) =
1167 attachment::decrypt_attachments_with_failures(
1168 self.0.attachments.as_deref().unwrap_or_default(),
1169 ctx,
1170 ciphers_key,
1171 );
1172
1173 let mut cipher = CipherView {
1174 id: self.0.id,
1175 organization_id: self.0.organization_id,
1176 folder_id: self.0.folder_id,
1177 collection_ids: self.0.collection_ids.clone(),
1178 key: self.0.key.clone(),
1179 name: self.0.name.decrypt(ctx, ciphers_key)?,
1180 notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1181 r#type: self.0.r#type,
1182 login: self
1183 .0
1184 .login
1185 .as_ref()
1186 .map(|l| StrictDecrypt(l).decrypt(ctx, ciphers_key))
1187 .transpose()?,
1188 identity: self
1189 .0
1190 .identity
1191 .as_ref()
1192 .map(|i| StrictDecrypt(i).decrypt(ctx, ciphers_key))
1193 .transpose()?,
1194 card: self
1195 .0
1196 .card
1197 .as_ref()
1198 .map(|c| StrictDecrypt(c).decrypt(ctx, ciphers_key))
1199 .transpose()?,
1200 secure_note: self.0.secure_note.decrypt(ctx, ciphers_key)?,
1201 ssh_key: self.0.ssh_key.decrypt(ctx, ciphers_key)?,
1202 bank_account: self.0.bank_account.decrypt(ctx, ciphers_key)?,
1203 drivers_license: self.0.drivers_license.decrypt(ctx, ciphers_key)?,
1204 passport: self.0.passport.decrypt(ctx, ciphers_key)?,
1205 favorite: self.0.favorite,
1206 reprompt: self.0.reprompt,
1207 organization_use_totp: self.0.organization_use_totp,
1208 edit: self.0.edit,
1209 permissions: self.0.permissions,
1210 view_password: self.0.view_password,
1211 local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1212 attachments: Some(attachments),
1213 attachment_decryption_failures: Some(attachment_decryption_failures),
1214 fields: self
1215 .0
1216 .fields
1217 .as_ref()
1218 .map(|fields| {
1219 fields
1220 .iter()
1221 .map(|f| StrictDecrypt(f).decrypt(ctx, ciphers_key))
1222 .collect::<Result<Vec<_>, _>>()
1223 })
1224 .transpose()?,
1225 password_history: self.0.password_history.decrypt(ctx, ciphers_key)?,
1226 creation_date: self.0.creation_date,
1227 deleted_date: self.0.deleted_date,
1228 revision_date: self.0.revision_date,
1229 archived_date: self.0.archived_date,
1230 };
1231
1232 if cipher.key.is_some()
1235 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1236 {
1237 cipher.remove_invalid_checksums();
1238 }
1239
1240 Ok(cipher)
1241 }
1242}
1243
1244impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for StrictDecrypt<Cipher> {
1245 fn decrypt(
1246 &self,
1247 ctx: &mut KeyStoreContext<KeySlotIds>,
1248 key: SymmetricKeySlotId,
1249 ) -> Result<CipherListView, CryptoError> {
1250 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1251
1252 Ok(CipherListView {
1253 id: self.0.id,
1254 organization_id: self.0.organization_id,
1255 folder_id: self.0.folder_id,
1256 collection_ids: self.0.collection_ids.clone(),
1257 key: self.0.key.clone(),
1258 name: self.0.name.decrypt(ctx, ciphers_key)?,
1259 subtitle: self.0.decrypt_subtitle(ctx, ciphers_key)?,
1260 r#type: match self.0.r#type {
1261 CipherType::Login => {
1262 let login = self
1263 .0
1264 .login
1265 .as_ref()
1266 .ok_or(CryptoError::MissingField("login"))?;
1267 CipherListViewType::Login(StrictDecrypt(login).decrypt(ctx, ciphers_key)?)
1268 }
1269 CipherType::SecureNote => CipherListViewType::SecureNote,
1270 CipherType::Card => {
1271 let card = self
1272 .0
1273 .card
1274 .as_ref()
1275 .ok_or(CryptoError::MissingField("card"))?;
1276 CipherListViewType::Card(StrictDecrypt(card).decrypt(ctx, ciphers_key)?)
1277 }
1278 CipherType::Identity => CipherListViewType::Identity,
1279 CipherType::SshKey => CipherListViewType::SshKey,
1280 CipherType::BankAccount => CipherListViewType::BankAccount,
1281 CipherType::Passport => CipherListViewType::Passport,
1282 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1283 },
1284 favorite: self.0.favorite,
1285 reprompt: self.0.reprompt,
1286 organization_use_totp: self.0.organization_use_totp,
1287 edit: self.0.edit,
1288 permissions: self.0.permissions,
1289 view_password: self.0.view_password,
1290 attachments: self
1291 .0
1292 .attachments
1293 .as_ref()
1294 .map(|a| a.len() as u32)
1295 .unwrap_or(0),
1296 has_old_attachments: self
1297 .0
1298 .attachments
1299 .as_ref()
1300 .map(|a| a.iter().any(|att| att.key.is_none()))
1301 .unwrap_or(false),
1302 creation_date: self.0.creation_date,
1303 deleted_date: self.0.deleted_date,
1304 revision_date: self.0.revision_date,
1305 copyable_fields: self.0.get_copyable_fields(),
1306 local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1307 archived_date: self.0.archived_date,
1308 #[cfg(feature = "wasm")]
1309 notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1310 #[cfg(feature = "wasm")]
1311 fields: self
1312 .0
1313 .fields
1314 .as_ref()
1315 .map(|fields| {
1316 fields
1317 .iter()
1318 .map(|f| {
1319 StrictDecrypt(f)
1320 .decrypt(ctx, ciphers_key)
1321 .map(field::FieldListView::from)
1322 })
1323 .collect::<Result<Vec<_>, _>>()
1324 })
1325 .transpose()?,
1326 #[cfg(feature = "wasm")]
1327 attachment_names: self
1328 .0
1329 .attachments
1330 .as_ref()
1331 .map(|attachments| {
1332 attachments
1333 .iter()
1334 .map(|a| a.file_name.decrypt(ctx, ciphers_key))
1335 .collect::<Result<Vec<_>, _>>()
1336 })
1337 .transpose()?
1338 .map(|names| names.into_iter().flatten().collect()),
1339 })
1340 }
1341}
1342
1343impl TryFrom<CipherDetailsResponseModel> for Cipher {
1344 type Error = VaultParseError;
1345
1346 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
1347 Ok(Self {
1348 id: cipher.id.map(CipherId::new),
1349 organization_id: cipher.organization_id.map(OrganizationId::new),
1350 folder_id: cipher.folder_id.map(FolderId::new),
1351 collection_ids: cipher
1352 .collection_ids
1353 .unwrap_or_default()
1354 .into_iter()
1355 .map(CollectionId::new)
1356 .collect(),
1357 name: require!(EncString::try_from_optional(cipher.name)?),
1358 notes: EncString::try_from_optional(cipher.notes)?,
1359 r#type: require!(cipher.r#type).try_into()?,
1360 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1361 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1362 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1363 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1364 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1365 bank_account: cipher.bank_account.map(|b| (*b).try_into()).transpose()?,
1366 drivers_license: cipher
1367 .drivers_license
1368 .map(|d| (*d).try_into())
1369 .transpose()?,
1370 passport: cipher.passport.map(|p| (*p).try_into()).transpose()?,
1371 favorite: cipher.favorite.unwrap_or(false),
1372 reprompt: cipher
1373 .reprompt
1374 .map(|r| r.try_into())
1375 .transpose()?
1376 .unwrap_or(CipherRepromptType::None),
1377 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
1378 edit: cipher.edit.unwrap_or(true),
1379 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1380 view_password: cipher.view_password.unwrap_or(true),
1381 local_data: None, attachments: cipher
1383 .attachments
1384 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1385 .transpose()?,
1386 fields: cipher
1387 .fields
1388 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1389 .transpose()?,
1390 password_history: cipher
1391 .password_history
1392 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1393 .transpose()?,
1394 creation_date: require!(cipher.creation_date).parse()?,
1395 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1396 revision_date: require!(cipher.revision_date).parse()?,
1397 key: EncString::try_from_optional(cipher.key)?,
1398 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1399 data: cipher.data,
1400 })
1401 }
1402}
1403
1404impl PartialCipher for CipherDetailsResponseModel {
1405 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1406 Ok(Cipher {
1407 local_data: cipher.and_then(|c| c.local_data),
1408 ..self.try_into()?
1409 })
1410 }
1411}
1412
1413impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
1414 type Error = MissingFieldError;
1415
1416 fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
1417 Ok(match t {
1418 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1419 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1420 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1421 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1422 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1423 bitwarden_api_api::models::CipherType::BankAccount => CipherType::BankAccount,
1424 bitwarden_api_api::models::CipherType::Passport => CipherType::Passport,
1425 bitwarden_api_api::models::CipherType::DriversLicense => CipherType::DriversLicense,
1426 bitwarden_api_api::models::CipherType::__Unknown(_) => {
1427 return Err(MissingFieldError("type"));
1428 }
1429 })
1430 }
1431}
1432
1433impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1434 type Error = MissingFieldError;
1435
1436 fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
1437 Ok(match t {
1438 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1439 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1440 bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
1441 return Err(MissingFieldError("reprompt"));
1442 }
1443 })
1444 }
1445}
1446
1447pub(crate) trait PartialCipher {
1451 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1452}
1453
1454impl From<CipherType> for bitwarden_api_api::models::CipherType {
1455 fn from(t: CipherType) -> Self {
1456 match t {
1457 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1458 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1459 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1460 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1461 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1462 CipherType::BankAccount => bitwarden_api_api::models::CipherType::BankAccount,
1463 CipherType::Passport => bitwarden_api_api::models::CipherType::Passport,
1464 CipherType::DriversLicense => bitwarden_api_api::models::CipherType::DriversLicense,
1465 }
1466 }
1467}
1468
1469impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1470 fn from(t: CipherRepromptType) -> Self {
1471 match t {
1472 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1473 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1474 }
1475 }
1476}
1477
1478impl PartialCipher for CipherResponseModel {
1479 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1480 Ok(Cipher {
1481 collection_ids: cipher
1482 .as_ref()
1483 .map(|c| c.collection_ids.clone())
1484 .unwrap_or_default(),
1485 local_data: cipher.and_then(|c| c.local_data),
1486 id: self.id.map(CipherId::new),
1487 organization_id: self.organization_id.map(OrganizationId::new),
1488 folder_id: self.folder_id.map(FolderId::new),
1489 name: require!(self.name).parse()?,
1490 notes: EncString::try_from_optional(self.notes)?,
1491 r#type: require!(self.r#type).try_into()?,
1492 login: self.login.map(|l| (*l).try_into()).transpose()?,
1493 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1494 card: self.card.map(|c| (*c).try_into()).transpose()?,
1495 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1496 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1497 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1498 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1499 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1500 favorite: self.favorite.unwrap_or(false),
1501 reprompt: self
1502 .reprompt
1503 .map(|r| r.try_into())
1504 .transpose()?
1505 .unwrap_or(CipherRepromptType::None),
1506 organization_use_totp: self.organization_use_totp.unwrap_or(false),
1507 edit: self.edit.unwrap_or(false),
1508 permissions: self.permissions.map(|p| (*p).try_into()).transpose()?,
1509 view_password: self.view_password.unwrap_or(true),
1510 attachments: self
1511 .attachments
1512 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1513 .transpose()?,
1514 fields: self
1515 .fields
1516 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1517 .transpose()?,
1518 password_history: self
1519 .password_history
1520 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1521 .transpose()?,
1522 creation_date: require!(self.creation_date).parse()?,
1523 deleted_date: self.deleted_date.map(|d| d.parse()).transpose()?,
1524 revision_date: require!(self.revision_date).parse()?,
1525 key: EncString::try_from_optional(self.key)?,
1526 archived_date: self.archived_date.map(|d| d.parse()).transpose()?,
1527 data: self.data,
1528 })
1529 }
1530}
1531
1532impl PartialCipher for CipherMiniResponseModel {
1533 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1534 let cipher = cipher.as_ref();
1535 Ok(Cipher {
1536 id: self.id.map(CipherId::new),
1537 organization_id: self.organization_id.map(OrganizationId::new),
1538 key: EncString::try_from_optional(self.key)?,
1539 name: require!(EncString::try_from_optional(self.name)?),
1540 notes: EncString::try_from_optional(self.notes)?,
1541 r#type: require!(self.r#type).try_into()?,
1542 login: self.login.map(|l| (*l).try_into()).transpose()?,
1543 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1544 card: self.card.map(|c| (*c).try_into()).transpose()?,
1545 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1546 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1547 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1548 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1549 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1550 reprompt: self
1551 .reprompt
1552 .map(|r| r.try_into())
1553 .transpose()?
1554 .unwrap_or(CipherRepromptType::None),
1555 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1556 attachments: self
1557 .attachments
1558 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1559 .transpose()?,
1560 fields: self
1561 .fields
1562 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1563 .transpose()?,
1564 password_history: self
1565 .password_history
1566 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1567 .transpose()?,
1568 creation_date: require!(self.creation_date)
1569 .parse()
1570 .map_err(Into::<VaultParseError>::into)?,
1571 deleted_date: self
1572 .deleted_date
1573 .map(|d| d.parse())
1574 .transpose()
1575 .map_err(Into::<VaultParseError>::into)?,
1576 revision_date: require!(self.revision_date)
1577 .parse()
1578 .map_err(Into::<VaultParseError>::into)?,
1579 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1580 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1581 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1582 edit: cipher.map_or(Default::default(), |c| c.edit),
1583 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1584 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1585 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1586 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1587 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1588 })
1589 }
1590}
1591
1592impl PartialCipher for CipherMiniDetailsResponseModel {
1593 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1594 let cipher = cipher.as_ref();
1595 Ok(Cipher {
1596 id: self.id.map(CipherId::new),
1597 organization_id: self.organization_id.map(OrganizationId::new),
1598 key: EncString::try_from_optional(self.key)?,
1599 name: require!(EncString::try_from_optional(self.name)?),
1600 notes: EncString::try_from_optional(self.notes)?,
1601 r#type: require!(self.r#type).try_into()?,
1602 login: self.login.map(|l| (*l).try_into()).transpose()?,
1603 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1604 card: self.card.map(|c| (*c).try_into()).transpose()?,
1605 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1606 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1607 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1608 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1609 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1610 reprompt: self
1611 .reprompt
1612 .map(|r| r.try_into())
1613 .transpose()?
1614 .unwrap_or(CipherRepromptType::None),
1615 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1616 attachments: self
1617 .attachments
1618 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1619 .transpose()?,
1620 fields: self
1621 .fields
1622 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1623 .transpose()?,
1624 password_history: self
1625 .password_history
1626 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1627 .transpose()?,
1628 creation_date: require!(self.creation_date)
1629 .parse()
1630 .map_err(Into::<VaultParseError>::into)?,
1631 deleted_date: self
1632 .deleted_date
1633 .map(|d| d.parse())
1634 .transpose()
1635 .map_err(Into::<VaultParseError>::into)?,
1636 revision_date: require!(self.revision_date)
1637 .parse()
1638 .map_err(Into::<VaultParseError>::into)?,
1639 collection_ids: self
1640 .collection_ids
1641 .into_iter()
1642 .flatten()
1643 .map(CollectionId::new)
1644 .collect(),
1645 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1646 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1647 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1648 edit: cipher.map_or(Default::default(), |c| c.edit),
1649 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1650 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1651 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1652 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1653 })
1654 }
1655}
1656
1657#[cfg(test)]
1658mod tests {
1659
1660 use attachment::AttachmentView;
1661 use bitwarden_core::key_management::{
1662 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1663 };
1664 use bitwarden_crypto::SymmetricCryptoKey;
1665
1666 use super::*;
1667 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1668
1669 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1671 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1672 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1673 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1674 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1675 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1676 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1677
1678 fn generate_cipher() -> CipherView {
1679 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1680 CipherView {
1681 r#type: CipherType::Login,
1682 login: Some(LoginView {
1683 username: Some("test_username".to_string()),
1684 password: Some("test_password".to_string()),
1685 password_revision_date: None,
1686 uris: None,
1687 totp: None,
1688 autofill_on_page_load: None,
1689 fido2_credentials: None,
1690 }),
1691 id: Some(test_id),
1692 organization_id: None,
1693 folder_id: None,
1694 collection_ids: vec![],
1695 key: None,
1696 name: "My test login".to_string(),
1697 notes: None,
1698 identity: None,
1699 card: None,
1700 secure_note: None,
1701 ssh_key: None,
1702 bank_account: None,
1703 drivers_license: None,
1704 passport: None,
1705 favorite: false,
1706 reprompt: CipherRepromptType::None,
1707 organization_use_totp: true,
1708 edit: true,
1709 permissions: None,
1710 view_password: true,
1711 local_data: None,
1712 attachments: None,
1713 attachment_decryption_failures: None,
1714 fields: None,
1715 password_history: None,
1716 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1717 deleted_date: None,
1718 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1719 archived_date: None,
1720 }
1721 }
1722
1723 fn generate_fido2(
1724 ctx: &mut KeyStoreContext<KeySlotIds>,
1725 key: SymmetricKeySlotId,
1726 ) -> Fido2Credential {
1727 Fido2Credential {
1728 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1729 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1730 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1731 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1732 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1733 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1734 user_handle: None,
1735 user_name: None,
1736 counter: "123".to_string().encrypt(ctx, key).unwrap(),
1737 rp_name: None,
1738 user_display_name: None,
1739 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1740 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1741 }
1742 }
1743
1744 #[test]
1745 fn test_decrypt_cipher_list_view() {
1746 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1747 let key_store = create_test_crypto_with_user_key(key);
1748
1749 let cipher = Cipher {
1750 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1751 organization_id: None,
1752 folder_id: None,
1753 collection_ids: vec![],
1754 key: None,
1755 name: TEST_CIPHER_NAME.parse().unwrap(),
1756 notes: None,
1757 r#type: CipherType::Login,
1758 login: Some(Login {
1759 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1760 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1761 password_revision_date: None,
1762 uris: None,
1763 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1764 autofill_on_page_load: None,
1765 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
1766 }),
1767 identity: None,
1768 card: None,
1769 secure_note: None,
1770 ssh_key: None,
1771 bank_account: None,
1772 drivers_license: None,
1773 passport: None,
1774 favorite: false,
1775 reprompt: CipherRepromptType::None,
1776 organization_use_totp: false,
1777 edit: true,
1778 permissions: Some(CipherPermissions {
1779 delete: false,
1780 restore: false
1781 }),
1782 view_password: true,
1783 local_data: None,
1784 attachments: None,
1785 fields: None,
1786 password_history: None,
1787 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1788 deleted_date: None,
1789 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1790 archived_date: None,
1791 data: None,
1792 };
1793
1794 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1795
1796 assert_eq!(
1797 view,
1798 CipherListView {
1799 id: cipher.id,
1800 organization_id: cipher.organization_id,
1801 folder_id: cipher.folder_id,
1802 collection_ids: cipher.collection_ids,
1803 key: cipher.key,
1804 name: "My test login".to_string(),
1805 subtitle: "test_username".to_string(),
1806 r#type: CipherListViewType::Login(LoginListView {
1807 fido2_credentials: Some(vec![Fido2CredentialListView {
1808 credential_id: "123".to_string(),
1809 rp_id: "123".to_string(),
1810 user_handle: None,
1811 user_name: None,
1812 user_display_name: None,
1813 counter: "123".to_string(),
1814 }]),
1815 has_fido2: true,
1816 username: Some("test_username".to_string()),
1817 totp: cipher.login.as_ref().unwrap().totp.clone(),
1818 uris: None,
1819 }),
1820 favorite: cipher.favorite,
1821 reprompt: cipher.reprompt,
1822 organization_use_totp: cipher.organization_use_totp,
1823 edit: cipher.edit,
1824 permissions: cipher.permissions,
1825 view_password: cipher.view_password,
1826 attachments: 0,
1827 has_old_attachments: false,
1828 creation_date: cipher.creation_date,
1829 deleted_date: cipher.deleted_date,
1830 revision_date: cipher.revision_date,
1831 copyable_fields: vec![
1832 CopyableCipherFields::LoginUsername,
1833 CopyableCipherFields::LoginPassword,
1834 CopyableCipherFields::LoginTotp
1835 ],
1836 local_data: None,
1837 archived_date: cipher.archived_date,
1838 #[cfg(feature = "wasm")]
1839 notes: None,
1840 #[cfg(feature = "wasm")]
1841 fields: None,
1842 #[cfg(feature = "wasm")]
1843 attachment_names: None,
1844 }
1845 )
1846 }
1847
1848 #[test]
1849 fn test_decrypt_cipher_fails_with_invalid_name() {
1850 let key_store =
1851 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1852
1853 let cipher = key_store.encrypt(generate_cipher()).unwrap();
1855 let cipher = Cipher {
1856 name: TEST_CIPHER_NAME.parse().unwrap(), ..cipher
1858 };
1859
1860 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1862 assert!(
1863 lenient_result.is_ok(),
1864 "Lenient decryption should succeed even when name is encrypted with a different key"
1865 );
1866 assert_eq!(
1867 lenient_result.unwrap().name,
1868 String::new(),
1869 "Lenient decryption should yield an empty name on error"
1870 );
1871
1872 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1874 assert!(
1875 strict_result.is_err(),
1876 "Strict decryption should fail when name is encrypted with a different key"
1877 );
1878 }
1879
1880 #[test]
1881 fn test_decrypt_cipher_fails_with_invalid_login() {
1882 let key_store =
1883 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1884
1885 let cipher = key_store.encrypt(generate_cipher()).unwrap();
1887 let cipher = Cipher {
1888 login: Some(Login {
1889 username: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher.login.unwrap()
1891 }),
1892 ..cipher
1893 };
1894
1895 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1897 assert!(
1898 lenient_result.is_ok(),
1899 "Lenient decryption should succeed even when login username is encrypted with a different key"
1900 );
1901 let lenient_view = lenient_result.unwrap();
1902 assert!(
1903 lenient_view.login.is_some(),
1904 "Lenient decryption should still return the login object"
1905 );
1906 assert!(
1907 lenient_view.login.unwrap().username.is_none(),
1908 "Lenient decryption should null out the failing username field"
1909 );
1910
1911 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1913 assert!(
1914 strict_result.is_err(),
1915 "Strict decryption should fail when login username is encrypted with a different key"
1916 );
1917 }
1918
1919 #[test]
1920 fn test_generate_cipher_key() {
1921 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1922 let key_store = create_test_crypto_with_user_key(key);
1923
1924 let original_cipher = generate_cipher();
1925
1926 let cipher = generate_cipher();
1928 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1929 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1930 assert!(no_key_cipher_dec.key.is_none());
1931 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1932
1933 let mut cipher = generate_cipher();
1934 cipher
1935 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1936 .unwrap();
1937
1938 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1940 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1941 assert!(key_cipher_dec.key.is_some());
1942 assert_eq!(key_cipher_dec.name, original_cipher.name);
1943 }
1944
1945 #[test]
1946 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1947 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1948 let key_store = create_test_crypto_with_user_key(key);
1949
1950 let mut original_cipher = generate_cipher();
1951 {
1952 let mut ctx = key_store.context();
1953 let cipher_key = ctx.generate_symmetric_key();
1954
1955 original_cipher.key = Some(
1956 ctx.wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
1957 .unwrap(),
1958 );
1959 }
1960
1961 original_cipher
1962 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1963 .unwrap();
1964
1965 let wrapped_key = original_cipher.key.unwrap();
1967 let mut ctx = key_store.context();
1968 let _ = ctx
1969 .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_key)
1970 .unwrap();
1971 }
1972
1973 #[test]
1974 fn test_generate_cipher_key_ignores_attachments_without_key() {
1975 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1976 let key_store = create_test_crypto_with_user_key(key);
1977
1978 let mut cipher = generate_cipher();
1979 let attachment = AttachmentView {
1980 id: None,
1981 url: None,
1982 size: None,
1983 size_name: None,
1984 file_name: Some("Attachment test name".into()),
1985 key: None,
1986 #[cfg(feature = "wasm")]
1987 decrypted_key: None,
1988 };
1989 cipher.attachments = Some(vec![attachment]);
1990
1991 cipher
1992 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1993 .unwrap();
1994 assert!(cipher.attachments.unwrap()[0].key.is_none());
1995 }
1996
1997 #[test]
1998 fn test_reencrypt_cipher_key() {
1999 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2000 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2001 let key_store = create_test_crypto_with_user_key(old_key);
2002 let mut ctx = key_store.context_mut();
2003
2004 let mut cipher = generate_cipher();
2005 cipher
2006 .generate_cipher_key(&mut ctx, cipher.key_identifier())
2007 .unwrap();
2008
2009 let new_key_id = ctx.add_local_symmetric_key(new_key);
2011
2012 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
2013
2014 assert!(cipher.key.is_some());
2016 assert!(
2017 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
2018 .is_ok()
2019 );
2020 }
2021
2022 #[test]
2023 fn test_reencrypt_cipher_key_ignores_missing_key() {
2024 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2025 let key_store = create_test_crypto_with_user_key(key);
2026 let mut ctx = key_store.context_mut();
2027 let mut cipher = generate_cipher();
2028
2029 let new_cipher_key = ctx.generate_symmetric_key();
2031 cipher
2032 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
2033 .unwrap();
2034
2035 assert!(cipher.key.is_none());
2037 }
2038
2039 #[test]
2040 fn test_move_user_cipher_to_org() {
2041 let org = OrganizationId::new_v4();
2042 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2043 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2044 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2045
2046 let mut cipher = generate_cipher();
2048 cipher
2049 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2050 .unwrap();
2051
2052 cipher
2053 .move_to_organization(&mut key_store.context(), org)
2054 .unwrap();
2055 assert_eq!(cipher.organization_id, Some(org));
2056
2057 let cipher_enc = key_store.encrypt(cipher).unwrap();
2059 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
2060
2061 assert_eq!(cipher_dec.name, "My test login");
2062 }
2063
2064 #[test]
2065 fn test_move_user_cipher_to_org_manually() {
2066 let org = OrganizationId::new_v4();
2067 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2068 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2069 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2070
2071 let mut cipher = generate_cipher();
2073 cipher
2074 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2075 .unwrap();
2076
2077 cipher.organization_id = Some(org);
2078
2079 assert!(key_store.encrypt(cipher).is_err());
2082 }
2083
2084 #[test]
2085 fn test_move_user_cipher_with_attachment_without_key_to_org() {
2086 let org = OrganizationId::new_v4();
2087 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2088 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2089 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2090
2091 let mut cipher = generate_cipher();
2092 let attachment = AttachmentView {
2093 id: None,
2094 url: None,
2095 size: None,
2096 size_name: None,
2097 file_name: Some("Attachment test name".into()),
2098 key: None,
2099 #[cfg(feature = "wasm")]
2100 decrypted_key: None,
2101 };
2102 cipher.attachments = Some(vec![attachment]);
2103
2104 assert!(
2106 cipher
2107 .move_to_organization(&mut key_store.context(), org)
2108 .is_err()
2109 );
2110 }
2111
2112 #[test]
2113 fn test_move_user_cipher_with_attachment_with_key_to_org() {
2114 let org = OrganizationId::new_v4();
2115 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2116 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2117 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2118 let org_key = SymmetricKeySlotId::Organization(org);
2119
2120 let (attachment_key_enc, attachment_key_val) = {
2122 let mut ctx = key_store.context();
2123 let attachment_key = ctx.generate_symmetric_key();
2124 let attachment_key_enc = ctx
2125 .wrap_symmetric_key(SymmetricKeySlotId::User, attachment_key)
2126 .unwrap();
2127 #[allow(deprecated)]
2128 let attachment_key_val = ctx
2129 .dangerous_get_symmetric_key(attachment_key)
2130 .unwrap()
2131 .clone();
2132
2133 (attachment_key_enc, attachment_key_val)
2134 };
2135
2136 let mut cipher = generate_cipher();
2137 let attachment = AttachmentView {
2138 id: None,
2139 url: None,
2140 size: None,
2141 size_name: None,
2142 file_name: Some("Attachment test name".into()),
2143 key: Some(attachment_key_enc),
2144 #[cfg(feature = "wasm")]
2145 decrypted_key: None,
2146 };
2147 cipher.attachments = Some(vec![attachment]);
2148 let cred = generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User);
2149 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
2150
2151 cipher
2152 .move_to_organization(&mut key_store.context(), org)
2153 .unwrap();
2154
2155 assert!(cipher.key.is_none());
2156
2157 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
2160 let mut ctx = key_store.context();
2161 let new_attachment_key_id = ctx
2162 .unwrap_symmetric_key(org_key, &new_attachment_key)
2163 .unwrap();
2164 #[allow(deprecated)]
2165 let new_attachment_key_dec = ctx
2166 .dangerous_get_symmetric_key(new_attachment_key_id)
2167 .unwrap();
2168
2169 assert_eq!(*new_attachment_key_dec, attachment_key_val);
2170
2171 let cred2: Fido2CredentialFullView = cipher
2172 .login
2173 .unwrap()
2174 .fido2_credentials
2175 .unwrap()
2176 .first()
2177 .unwrap()
2178 .decrypt(&mut key_store.context(), org_key)
2179 .unwrap();
2180
2181 assert_eq!(cred2.credential_id, "123");
2182 }
2183
2184 #[test]
2185 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
2186 let org = OrganizationId::new_v4();
2187 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2188 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2189 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2190 let org_key = SymmetricKeySlotId::Organization(org);
2191
2192 let mut ctx = key_store.context();
2193
2194 let cipher_key = ctx.generate_symmetric_key();
2195 let cipher_key_enc = ctx
2196 .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
2197 .unwrap();
2198
2199 let attachment_key = ctx.generate_symmetric_key();
2201 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
2202
2203 let mut cipher = generate_cipher();
2204 cipher.key = Some(cipher_key_enc);
2205
2206 let attachment = AttachmentView {
2207 id: None,
2208 url: None,
2209 size: None,
2210 size_name: None,
2211 file_name: Some("Attachment test name".into()),
2212 key: Some(attachment_key_enc.clone()),
2213 #[cfg(feature = "wasm")]
2214 decrypted_key: None,
2215 };
2216 cipher.attachments = Some(vec![attachment]);
2217
2218 let cred = generate_fido2(&mut ctx, cipher_key);
2219 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
2220
2221 cipher.move_to_organization(&mut ctx, org).unwrap();
2222
2223 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
2225 let new_cipher_key_dec = ctx
2226 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
2227 .unwrap();
2228 #[allow(deprecated)]
2229 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
2230 #[allow(deprecated)]
2231 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
2232
2233 assert_eq!(new_cipher_key_dec, cipher_key_val);
2234
2235 assert_eq!(
2237 cipher.attachments.unwrap()[0]
2238 .key
2239 .as_ref()
2240 .unwrap()
2241 .to_string(),
2242 attachment_key_enc.to_string()
2243 );
2244
2245 let cred2: Fido2Credential = cipher
2246 .login
2247 .unwrap()
2248 .fido2_credentials
2249 .unwrap()
2250 .first()
2251 .unwrap()
2252 .clone();
2253
2254 assert_eq!(
2255 cred2.credential_id.to_string(),
2256 cred.credential_id.to_string()
2257 );
2258 }
2259
2260 #[test]
2261 fn test_decrypt_fido2_private_key() {
2262 let key_store =
2263 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2264 let mut ctx = key_store.context();
2265
2266 let mut cipher_view = generate_cipher();
2267 cipher_view
2268 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
2269 .unwrap();
2270
2271 let key_id = cipher_view.key_identifier();
2272 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
2273
2274 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
2275
2276 cipher_view.login.as_mut().unwrap().fido2_credentials =
2277 Some(vec![fido2_credential.clone()]);
2278
2279 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
2280 assert_eq!(decrypted_key_value, "123");
2281 }
2282
2283 #[test]
2284 fn test_password_history_on_password_change() {
2285 use chrono::Utc;
2286
2287 let original_cipher = generate_cipher();
2288 let mut new_cipher = generate_cipher();
2289
2290 if let Some(ref mut login) = new_cipher.login {
2292 login.password = Some("new_password123".to_string());
2293 }
2294
2295 let start = Utc::now();
2296 new_cipher.update_password_history(&original_cipher);
2297 let end = Utc::now();
2298
2299 assert!(new_cipher.password_history.is_some());
2300 let history = new_cipher.password_history.unwrap();
2301 assert_eq!(history.len(), 1);
2302 assert_eq!(history[0].password, "test_password");
2303 assert!(
2304 history[0].last_used_date >= start && history[0].last_used_date <= end,
2305 "last_used_date was not set properly"
2306 );
2307 }
2308
2309 #[test]
2310 fn test_password_history_on_unchanged_password() {
2311 let original_cipher = generate_cipher();
2312 let mut new_cipher = generate_cipher();
2313
2314 new_cipher.update_password_history(&original_cipher);
2315
2316 assert!(
2318 new_cipher.password_history.is_none()
2319 || new_cipher.password_history.as_ref().unwrap().is_empty()
2320 );
2321 }
2322
2323 #[test]
2324 fn test_password_history_is_preserved() {
2325 use chrono::TimeZone;
2326
2327 let mut original_cipher = generate_cipher();
2328 original_cipher.password_history = Some(
2329 (0..4)
2330 .map(|i| PasswordHistoryView {
2331 password: format!("old_password_{}", i),
2332 last_used_date: chrono::Utc
2333 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
2334 .unwrap(),
2335 })
2336 .collect(),
2337 );
2338
2339 let mut new_cipher = generate_cipher();
2340
2341 new_cipher.update_password_history(&original_cipher);
2342
2343 assert!(new_cipher.password_history.is_some());
2344 let history = new_cipher.password_history.unwrap();
2345 assert_eq!(history.len(), 4);
2346
2347 assert_eq!(history[0].password, "old_password_0");
2348 assert_eq!(
2349 history[0].last_used_date,
2350 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
2351 );
2352 assert_eq!(history[1].password, "old_password_1");
2353 assert_eq!(
2354 history[1].last_used_date,
2355 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
2356 );
2357 assert_eq!(history[2].password, "old_password_2");
2358 assert_eq!(
2359 history[2].last_used_date,
2360 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
2361 );
2362 assert_eq!(history[3].password, "old_password_3");
2363 assert_eq!(
2364 history[3].last_used_date,
2365 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
2366 );
2367 }
2368
2369 #[test]
2370 fn test_populate_cipher_types_login_with_valid_data() {
2371 let mut cipher = Cipher {
2372 id: Some(TEST_UUID.parse().unwrap()),
2373 organization_id: None,
2374 folder_id: None,
2375 collection_ids: vec![],
2376 key: None,
2377 name: TEST_CIPHER_NAME.parse().unwrap(),
2378 notes: None,
2379 r#type: CipherType::Login,
2380 login: None,
2381 identity: None,
2382 card: None,
2383 secure_note: None,
2384 ssh_key: None,
2385 bank_account: None,
2386 drivers_license: None,
2387 passport: None,
2388 favorite: false,
2389 reprompt: CipherRepromptType::None,
2390 organization_use_totp: false,
2391 edit: true,
2392 view_password: true,
2393 permissions: None,
2394 local_data: None,
2395 attachments: None,
2396 fields: None,
2397 password_history: None,
2398 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2399 deleted_date: None,
2400 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2401 archived_date: None,
2402 data: Some(format!(
2403 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2404 TEST_ENC_STRING_1, TEST_ENC_STRING_2
2405 )),
2406 };
2407
2408 cipher
2409 .populate_cipher_types()
2410 .expect("populate_cipher_types failed");
2411
2412 assert!(cipher.login.is_some());
2413 let login = cipher.login.unwrap();
2414 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
2415 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
2416 }
2417
2418 #[test]
2419 fn test_populate_cipher_types_secure_note() {
2420 let mut cipher = Cipher {
2421 id: Some(TEST_UUID.parse().unwrap()),
2422 organization_id: None,
2423 folder_id: None,
2424 collection_ids: vec![],
2425 key: None,
2426 name: TEST_CIPHER_NAME.parse().unwrap(),
2427 notes: None,
2428 r#type: CipherType::SecureNote,
2429 login: None,
2430 identity: None,
2431 card: None,
2432 secure_note: None,
2433 ssh_key: None,
2434 bank_account: None,
2435 drivers_license: None,
2436 passport: None,
2437 favorite: false,
2438 reprompt: CipherRepromptType::None,
2439 organization_use_totp: false,
2440 edit: true,
2441 view_password: true,
2442 permissions: None,
2443 local_data: None,
2444 attachments: None,
2445 fields: None,
2446 password_history: None,
2447 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2448 deleted_date: None,
2449 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2450 archived_date: None,
2451 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
2452 };
2453
2454 cipher
2455 .populate_cipher_types()
2456 .expect("populate_cipher_types failed");
2457
2458 assert!(cipher.secure_note.is_some());
2459 }
2460
2461 #[test]
2462 fn test_populate_cipher_types_card() {
2463 let mut cipher = Cipher {
2464 id: Some(TEST_UUID.parse().unwrap()),
2465 organization_id: None,
2466 folder_id: None,
2467 collection_ids: vec![],
2468 key: None,
2469 name: TEST_CIPHER_NAME.parse().unwrap(),
2470 notes: None,
2471 r#type: CipherType::Card,
2472 login: None,
2473 identity: None,
2474 card: None,
2475 secure_note: None,
2476 ssh_key: None,
2477 bank_account: None,
2478 drivers_license: None,
2479 passport: None,
2480 favorite: false,
2481 reprompt: CipherRepromptType::None,
2482 organization_use_totp: false,
2483 edit: true,
2484 view_password: true,
2485 permissions: None,
2486 local_data: None,
2487 attachments: None,
2488 fields: None,
2489 password_history: None,
2490 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2491 deleted_date: None,
2492 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2493 archived_date: None,
2494 data: Some(format!(
2495 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2496 TEST_ENC_STRING_1,
2497 TEST_ENC_STRING_2,
2498 TEST_ENC_STRING_3,
2499 TEST_ENC_STRING_4,
2500 TEST_ENC_STRING_5,
2501 TEST_ENC_STRING_1
2502 )),
2503 };
2504
2505 cipher
2506 .populate_cipher_types()
2507 .expect("populate_cipher_types failed");
2508
2509 assert!(cipher.card.is_some());
2510 let card = cipher.card.unwrap();
2511 assert_eq!(
2512 card.cardholder_name.as_ref().unwrap().to_string(),
2513 TEST_ENC_STRING_1
2514 );
2515 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
2516 assert_eq!(
2517 card.exp_month.as_ref().unwrap().to_string(),
2518 TEST_ENC_STRING_3
2519 );
2520 assert_eq!(
2521 card.exp_year.as_ref().unwrap().to_string(),
2522 TEST_ENC_STRING_4
2523 );
2524 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
2525 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
2526 }
2527
2528 #[test]
2529 fn test_populate_cipher_types_identity() {
2530 let mut cipher = Cipher {
2531 id: Some(TEST_UUID.parse().unwrap()),
2532 organization_id: None,
2533 folder_id: None,
2534 collection_ids: vec![],
2535 key: None,
2536 name: TEST_CIPHER_NAME.parse().unwrap(),
2537 notes: None,
2538 r#type: CipherType::Identity,
2539 login: None,
2540 identity: None,
2541 card: None,
2542 secure_note: None,
2543 ssh_key: None,
2544 bank_account: None,
2545 drivers_license: None,
2546 passport: None,
2547 favorite: false,
2548 reprompt: CipherRepromptType::None,
2549 organization_use_totp: false,
2550 edit: true,
2551 view_password: true,
2552 permissions: None,
2553 local_data: None,
2554 attachments: None,
2555 fields: None,
2556 password_history: None,
2557 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2558 deleted_date: None,
2559 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2560 archived_date: None,
2561 data: Some(format!(
2562 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2563 TEST_ENC_STRING_1,
2564 TEST_ENC_STRING_2,
2565 TEST_ENC_STRING_3,
2566 TEST_ENC_STRING_4,
2567 TEST_ENC_STRING_5,
2568 TEST_ENC_STRING_1,
2569 TEST_ENC_STRING_2,
2570 TEST_ENC_STRING_3,
2571 TEST_ENC_STRING_4,
2572 TEST_ENC_STRING_5
2573 )),
2574 };
2575
2576 cipher
2577 .populate_cipher_types()
2578 .expect("populate_cipher_types failed");
2579
2580 assert!(cipher.identity.is_some());
2581 let identity = cipher.identity.unwrap();
2582 assert_eq!(
2583 identity.first_name.as_ref().unwrap().to_string(),
2584 TEST_ENC_STRING_1
2585 );
2586 assert_eq!(
2587 identity.last_name.as_ref().unwrap().to_string(),
2588 TEST_ENC_STRING_2
2589 );
2590 assert_eq!(
2591 identity.email.as_ref().unwrap().to_string(),
2592 TEST_ENC_STRING_3
2593 );
2594 assert_eq!(
2595 identity.phone.as_ref().unwrap().to_string(),
2596 TEST_ENC_STRING_4
2597 );
2598 assert_eq!(
2599 identity.company.as_ref().unwrap().to_string(),
2600 TEST_ENC_STRING_5
2601 );
2602 assert_eq!(
2603 identity.address1.as_ref().unwrap().to_string(),
2604 TEST_ENC_STRING_1
2605 );
2606 assert_eq!(
2607 identity.city.as_ref().unwrap().to_string(),
2608 TEST_ENC_STRING_2
2609 );
2610 assert_eq!(
2611 identity.state.as_ref().unwrap().to_string(),
2612 TEST_ENC_STRING_3
2613 );
2614 assert_eq!(
2615 identity.postal_code.as_ref().unwrap().to_string(),
2616 TEST_ENC_STRING_4
2617 );
2618 assert_eq!(
2619 identity.country.as_ref().unwrap().to_string(),
2620 TEST_ENC_STRING_5
2621 );
2622 }
2623
2624 #[test]
2625
2626 fn test_password_history_with_hidden_fields() {
2627 let mut original_cipher = generate_cipher();
2628 original_cipher.fields = Some(vec![FieldView {
2629 name: Some("Secret Key".to_string()),
2630 value: Some("old_secret_value".to_string()),
2631 r#type: crate::FieldType::Hidden,
2632 linked_id: None,
2633 }]);
2634
2635 let mut new_cipher = generate_cipher();
2636 new_cipher.fields = Some(vec![FieldView {
2637 name: Some("Secret Key".to_string()),
2638 value: Some("new_secret_value".to_string()),
2639 r#type: crate::FieldType::Hidden,
2640 linked_id: None,
2641 }]);
2642
2643 new_cipher.update_password_history(&original_cipher);
2644
2645 assert!(new_cipher.password_history.is_some());
2646 let history = new_cipher.password_history.unwrap();
2647 assert_eq!(history.len(), 1);
2648 assert_eq!(history[0].password, "Secret Key: old_secret_value");
2649 }
2650
2651 #[test]
2652 fn test_password_history_length_limit() {
2653 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
2654
2655 let mut original_cipher = generate_cipher();
2656 original_cipher.password_history = Some(
2657 (0..10)
2658 .map(|i| PasswordHistoryView {
2659 password: format!("old_password_{}", i),
2660 last_used_date: chrono::Utc::now(),
2661 })
2662 .collect(),
2663 );
2664
2665 let mut new_cipher = original_cipher.clone();
2666 if let Some(ref mut login) = new_cipher.login {
2668 login.password = Some("brand_new_password".to_string());
2669 }
2670
2671 new_cipher.update_password_history(&original_cipher);
2672
2673 assert!(new_cipher.password_history.is_some());
2674 let history = new_cipher.password_history.unwrap();
2675
2676 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
2678
2679 assert_eq!(history[0].password, "test_password");
2681 assert_eq!(history[1].password, "old_password_0");
2683 assert_eq!(history[2].password, "old_password_1");
2684 assert_eq!(history[3].password, "old_password_2");
2685 assert_eq!(history[4].password, "old_password_3");
2686 }
2687
2688 #[test]
2689 fn test_populate_cipher_types_ssh_key() {
2690 let mut cipher = Cipher {
2691 id: Some(TEST_UUID.parse().unwrap()),
2692 organization_id: None,
2693 folder_id: None,
2694 collection_ids: vec![],
2695 key: None,
2696 name: TEST_CIPHER_NAME.parse().unwrap(),
2697 notes: None,
2698 r#type: CipherType::SshKey,
2699 login: None,
2700 identity: None,
2701 card: None,
2702 secure_note: None,
2703 ssh_key: None,
2704 bank_account: None,
2705 drivers_license: None,
2706 passport: None,
2707 favorite: false,
2708 reprompt: CipherRepromptType::None,
2709 organization_use_totp: false,
2710 edit: true,
2711 view_password: true,
2712 permissions: None,
2713 local_data: None,
2714 attachments: None,
2715 fields: None,
2716 password_history: None,
2717 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2718 deleted_date: None,
2719 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2720 archived_date: None,
2721 data: Some(format!(
2722 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2723 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2724 )),
2725 };
2726
2727 cipher
2728 .populate_cipher_types()
2729 .expect("populate_cipher_types failed");
2730
2731 assert!(cipher.ssh_key.is_some());
2732 let ssh_key = cipher.ssh_key.unwrap();
2733 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2734 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2735 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2736 }
2737
2738 #[test]
2739 fn test_populate_cipher_types_with_null_data() {
2740 let mut cipher = Cipher {
2741 id: Some(TEST_UUID.parse().unwrap()),
2742 organization_id: None,
2743 folder_id: None,
2744 collection_ids: vec![],
2745 key: None,
2746 name: TEST_CIPHER_NAME.parse().unwrap(),
2747 notes: None,
2748 r#type: CipherType::Login,
2749 login: None,
2750 identity: None,
2751 card: None,
2752 secure_note: None,
2753 ssh_key: None,
2754 bank_account: None,
2755 drivers_license: None,
2756 passport: None,
2757 favorite: false,
2758 reprompt: CipherRepromptType::None,
2759 organization_use_totp: false,
2760 edit: true,
2761 view_password: true,
2762 permissions: None,
2763 local_data: None,
2764 attachments: None,
2765 fields: None,
2766 password_history: None,
2767 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2768 deleted_date: None,
2769 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2770 archived_date: None,
2771 data: None,
2772 };
2773
2774 let result = cipher.populate_cipher_types();
2775 assert!(matches!(
2776 result,
2777 Err(VaultParseError::MissingField(MissingFieldError("data")))
2778 ));
2779 }
2780
2781 #[test]
2782 fn test_populate_cipher_types_with_invalid_json() {
2783 let mut cipher = Cipher {
2784 id: Some(TEST_UUID.parse().unwrap()),
2785 organization_id: None,
2786 folder_id: None,
2787 collection_ids: vec![],
2788 key: None,
2789 name: TEST_CIPHER_NAME.parse().unwrap(),
2790 notes: None,
2791 r#type: CipherType::Login,
2792 login: None,
2793 identity: None,
2794 card: None,
2795 secure_note: None,
2796 ssh_key: None,
2797 bank_account: None,
2798 drivers_license: None,
2799 passport: None,
2800 favorite: false,
2801 reprompt: CipherRepromptType::None,
2802 organization_use_totp: false,
2803 edit: true,
2804 view_password: true,
2805 permissions: None,
2806 local_data: None,
2807 attachments: None,
2808 fields: None,
2809 password_history: None,
2810 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2811 deleted_date: None,
2812 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2813 archived_date: None,
2814 data: Some("invalid json".to_string()),
2815 };
2816
2817 let result = cipher.populate_cipher_types();
2818
2819 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2820 }
2821
2822 #[test]
2823 fn test_decrypt_cipher_with_mixed_attachments() {
2824 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2825 let key_store = create_test_crypto_with_user_key(user_key);
2826
2827 let mut ctx = key_store.context();
2829 let valid1 = "valid_file_1.txt"
2830 .encrypt(&mut ctx, SymmetricKeySlotId::User)
2831 .unwrap();
2832 let valid2 = "valid_file_2.txt"
2833 .encrypt(&mut ctx, SymmetricKeySlotId::User)
2834 .unwrap();
2835
2836 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
2838 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
2839 let mut wrong_ctx = wrong_key_store.context();
2840 let corrupted = "corrupted_file.txt"
2841 .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
2842 .unwrap();
2843
2844 let cipher = Cipher {
2845 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2846 organization_id: None,
2847 folder_id: None,
2848 collection_ids: vec![],
2849 key: None,
2850 name: TEST_CIPHER_NAME.parse().unwrap(),
2851 notes: None,
2852 r#type: CipherType::Login,
2853 login: None,
2854 identity: None,
2855 card: None,
2856 secure_note: None,
2857 ssh_key: None,
2858 bank_account: None,
2859 drivers_license: None,
2860 passport: None,
2861 favorite: false,
2862 reprompt: CipherRepromptType::None,
2863 organization_use_totp: false,
2864 edit: true,
2865 permissions: None,
2866 view_password: true,
2867 local_data: None,
2868 attachments: Some(vec![
2869 attachment::Attachment {
2871 id: Some("valid-attachment".to_string()),
2872 url: Some("https://example.com/valid".to_string()),
2873 size: Some("100".to_string()),
2874 size_name: Some("100 Bytes".to_string()),
2875 file_name: Some(valid1),
2876 key: None,
2877 },
2878 attachment::Attachment {
2880 id: Some("corrupted-attachment".to_string()),
2881 url: Some("https://example.com/corrupted".to_string()),
2882 size: Some("200".to_string()),
2883 size_name: Some("200 Bytes".to_string()),
2884 file_name: Some(corrupted),
2885 key: None,
2886 },
2887 attachment::Attachment {
2889 id: Some("valid-attachment-2".to_string()),
2890 url: Some("https://example.com/valid2".to_string()),
2891 size: Some("150".to_string()),
2892 size_name: Some("150 Bytes".to_string()),
2893 file_name: Some(valid2),
2894 key: None,
2895 },
2896 ]),
2897 fields: None,
2898 password_history: None,
2899 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2900 deleted_date: None,
2901 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2902 archived_date: None,
2903 data: None,
2904 };
2905
2906 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2907
2908 assert!(view.attachments.is_some());
2910 let successes = view.attachments.as_ref().unwrap();
2911 assert_eq!(successes.len(), 2);
2912 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
2913 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
2914
2915 assert!(view.attachment_decryption_failures.is_some());
2917 let failures = view.attachment_decryption_failures.as_ref().unwrap();
2918 assert_eq!(failures.len(), 1);
2919 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
2920 assert_eq!(failures[0].file_name, None);
2921 }
2922
2923 #[test]
2924 fn test_decrypt_cipher_list_view_passport() {
2925 let key_store =
2926 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2927
2928 let cipher_view = CipherView {
2929 r#type: CipherType::Passport,
2930 passport: Some(passport::PassportView {
2931 given_name: Some("Jane".to_string()),
2932 surname: Some("Doe".to_string()),
2933 passport_number: Some("P12345678".to_string()),
2934 ..Default::default()
2935 }),
2936 login: None,
2937 ..generate_cipher()
2938 };
2939
2940 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2941 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2942
2943 assert_eq!(list_view.r#type, CipherListViewType::Passport);
2944 assert_eq!(list_view.subtitle, "Jane Doe");
2945 assert_eq!(
2946 list_view.copyable_fields,
2947 vec![CopyableCipherFields::PassportPassportNumber]
2948 );
2949 }
2950
2951 #[test]
2952 fn test_decrypt_cipher_list_view_drivers_license() {
2953 let key_store =
2954 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2955
2956 let cipher_view = CipherView {
2957 r#type: CipherType::DriversLicense,
2958 drivers_license: Some(drivers_license::DriversLicenseView {
2959 first_name: Some("John".to_string()),
2960 last_name: Some("Doe".to_string()),
2961 license_number: Some("DL-987654".to_string()),
2962 ..Default::default()
2963 }),
2964 login: None,
2965 ..generate_cipher()
2966 };
2967
2968 let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2969 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2970
2971 assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
2972 assert_eq!(list_view.subtitle, "John Doe");
2973 assert_eq!(
2974 list_view.copyable_fields,
2975 vec![CopyableCipherFields::DriversLicenseLicenseNumber]
2976 );
2977 }
2978
2979 #[test]
2980 fn test_cipher_view_encrypt_decrypt_passport() {
2981 let key_store =
2982 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2983
2984 let passport = passport::PassportView {
2985 given_name: Some("Jane".to_string()),
2986 surname: Some("Doe".to_string()),
2987 date_of_birth: Some("1990-01-01".to_string()),
2988 sex: Some("F".to_string()),
2989 birth_place: Some("New York".to_string()),
2990 nationality: Some("American".to_string()),
2991 issuing_country: Some("US".to_string()),
2992 passport_number: Some("P12345678".to_string()),
2993 passport_type: Some("P".to_string()),
2994 national_identification_number: Some("123-45-6789".to_string()),
2995 issuing_authority: Some("US State Department".to_string()),
2996 issue_date: Some("2020-01-01".to_string()),
2997 expiration_date: Some("2030-01-01".to_string()),
2998 };
2999
3000 let cipher_view = CipherView {
3001 r#type: CipherType::Passport,
3002 passport: Some(passport.clone()),
3003 login: None,
3004 ..generate_cipher()
3005 };
3006
3007 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3008 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3009
3010 assert_eq!(decrypted.r#type, CipherType::Passport);
3011 assert_eq!(decrypted.passport, Some(passport));
3012 assert!(decrypted.login.is_none());
3013 }
3014
3015 #[test]
3016 fn test_cipher_view_encrypt_decrypt_drivers_license() {
3017 let key_store =
3018 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
3019
3020 let dl = drivers_license::DriversLicenseView {
3021 first_name: Some("John".to_string()),
3022 middle_name: Some("Michael".to_string()),
3023 last_name: Some("Doe".to_string()),
3024 date_of_birth: Some("1985-06-15".to_string()),
3025 license_number: Some("DL-987654".to_string()),
3026 issuing_country: Some("US".to_string()),
3027 issuing_state: Some("NY".to_string()),
3028 issue_date: Some("2020-01-01".to_string()),
3029 expiration_date: Some("2028-01-01".to_string()),
3030 issuing_authority: Some("NY DMV".to_string()),
3031 license_class: Some("D".to_string()),
3032 };
3033
3034 let cipher_view = CipherView {
3035 r#type: CipherType::DriversLicense,
3036 drivers_license: Some(dl.clone()),
3037 login: None,
3038 ..generate_cipher()
3039 };
3040
3041 let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3042 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3043
3044 assert_eq!(decrypted.r#type, CipherType::DriversLicense);
3045 assert_eq!(decrypted.drivers_license, Some(dl));
3046 assert!(decrypted.login.is_none());
3047 }
3048}