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