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::{KeyIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeyId},
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, 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<KeyIds>,
85 key: SymmetricKeyId,
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}
104
105#[allow(missing_docs)]
106#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
107#[repr(u8)]
108#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
109#[cfg_attr(feature = "wasm", wasm_bindgen)]
110pub enum CipherRepromptType {
111 #[default]
112 None = 0,
113 Password = 1,
114}
115
116#[allow(missing_docs)]
117#[derive(Serialize, Deserialize, Debug, Clone)]
118#[serde(rename_all = "camelCase", deny_unknown_fields)]
119#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
120#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
121pub struct EncryptionContext {
122 pub encrypted_for: UserId,
125 pub cipher: Cipher,
126}
127
128impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
129 type Error = CipherError;
130 fn try_from(
131 EncryptionContext {
132 cipher,
133 encrypted_for,
134 }: EncryptionContext,
135 ) -> Result<Self, Self::Error> {
136 Ok(Self {
137 id: require!(cipher.id).into(),
138 encrypted_for: Some(encrypted_for.into()),
139 r#type: Some(cipher.r#type.into()),
140 organization_id: cipher.organization_id.map(|o| o.to_string()),
141 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
142 favorite: cipher.favorite.into(),
143 reprompt: Some(cipher.reprompt.into()),
144 key: cipher.key.map(|k| k.to_string()),
145 name: cipher.name.to_string(),
146 notes: cipher.notes.map(|n| n.to_string()),
147 fields: Some(
148 cipher
149 .fields
150 .into_iter()
151 .flatten()
152 .map(Into::into)
153 .collect(),
154 ),
155 password_history: Some(
156 cipher
157 .password_history
158 .into_iter()
159 .flatten()
160 .map(Into::into)
161 .collect(),
162 ),
163 attachments: None,
164 attachments2: Some(
165 cipher
166 .attachments
167 .into_iter()
168 .flatten()
169 .filter_map(|a| {
170 a.id.map(|id| {
171 (
172 id,
173 bitwarden_api_api::models::CipherAttachmentModel {
174 file_name: a.file_name.map(|n| n.to_string()),
175 key: a.key.map(|k| k.to_string()),
176 },
177 )
178 })
179 })
180 .collect(),
181 ),
182 login: cipher.login.map(|l| Box::new(l.into())),
183 card: cipher.card.map(|c| Box::new(c.into())),
184 identity: cipher.identity.map(|i| Box::new(i.into())),
185 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
186 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
187 data: None, last_known_revision_date: Some(
189 cipher
190 .revision_date
191 .to_rfc3339_opts(SecondsFormat::Millis, true),
192 ),
193 archived_date: cipher
194 .archived_date
195 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
196 })
197 }
198}
199
200impl From<EncryptionContext> for CipherRequestModel {
201 fn from(
202 EncryptionContext {
203 cipher,
204 encrypted_for,
205 }: EncryptionContext,
206 ) -> Self {
207 Self {
208 encrypted_for: Some(encrypted_for.into()),
209 r#type: Some(cipher.r#type.into()),
210 organization_id: cipher.organization_id.map(|o| o.to_string()),
211 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
212 favorite: cipher.favorite.into(),
213 reprompt: Some(cipher.reprompt.into()),
214 key: cipher.key.map(|k| k.to_string()),
215 name: cipher.name.to_string(),
216 notes: cipher.notes.map(|n| n.to_string()),
217 fields: Some(
218 cipher
219 .fields
220 .into_iter()
221 .flatten()
222 .map(Into::into)
223 .collect(),
224 ),
225 password_history: Some(
226 cipher
227 .password_history
228 .into_iter()
229 .flatten()
230 .map(Into::into)
231 .collect(),
232 ),
233 attachments: None,
234 attachments2: Some(
235 cipher
236 .attachments
237 .into_iter()
238 .flatten()
239 .filter_map(|a| {
240 a.id.map(|id| {
241 (
242 id,
243 bitwarden_api_api::models::CipherAttachmentModel {
244 file_name: a.file_name.map(|n| n.to_string()),
245 key: a.key.map(|k| k.to_string()),
246 },
247 )
248 })
249 })
250 .collect(),
251 ),
252 login: cipher.login.map(|l| Box::new(l.into())),
253 card: cipher.card.map(|c| Box::new(c.into())),
254 identity: cipher.identity.map(|i| Box::new(i.into())),
255 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
256 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
257 data: None, last_known_revision_date: Some(
259 cipher
260 .revision_date
261 .to_rfc3339_opts(SecondsFormat::Millis, true),
262 ),
263 archived_date: cipher
264 .archived_date
265 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
266 }
267 }
268}
269
270#[allow(missing_docs)]
271#[derive(Serialize, Deserialize, Debug, Clone)]
272#[serde(rename_all = "camelCase", deny_unknown_fields)]
273#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
274#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
275pub struct Cipher {
276 pub id: Option<CipherId>,
277 pub organization_id: Option<OrganizationId>,
278 pub folder_id: Option<FolderId>,
279 pub collection_ids: Vec<CollectionId>,
280 pub key: Option<EncString>,
283
284 pub name: EncString,
285 pub notes: Option<EncString>,
286
287 pub r#type: CipherType,
288 pub login: Option<Login>,
289 pub identity: Option<identity::Identity>,
290 pub card: Option<card::Card>,
291 pub secure_note: Option<secure_note::SecureNote>,
292 pub ssh_key: Option<ssh_key::SshKey>,
293
294 pub favorite: bool,
295 pub reprompt: CipherRepromptType,
296 pub organization_use_totp: bool,
297 pub edit: bool,
298 pub permissions: Option<CipherPermissions>,
299 pub view_password: bool,
300 pub local_data: Option<LocalData>,
301
302 pub attachments: Option<Vec<attachment::Attachment>>,
303 pub fields: Option<Vec<field::Field>>,
304 pub password_history: Option<Vec<password_history::PasswordHistory>>,
305
306 pub creation_date: DateTime<Utc>,
307 pub deleted_date: Option<DateTime<Utc>>,
308 pub revision_date: DateTime<Utc>,
309 pub archived_date: Option<DateTime<Utc>>,
310 pub data: Option<String>,
311}
312
313bitwarden_state::register_repository_item!(Cipher, "Cipher");
314
315#[allow(missing_docs)]
316#[derive(Serialize, Deserialize, Debug, Clone)]
317#[serde(rename_all = "camelCase", deny_unknown_fields)]
318#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
319#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
320pub struct CipherView {
321 pub id: Option<CipherId>,
322 pub organization_id: Option<OrganizationId>,
323 pub folder_id: Option<FolderId>,
324 pub collection_ids: Vec<CollectionId>,
325
326 pub key: Option<EncString>,
328
329 pub name: String,
330 pub notes: Option<String>,
331
332 pub r#type: CipherType,
333 pub login: Option<LoginView>,
334 pub identity: Option<identity::IdentityView>,
335 pub card: Option<card::CardView>,
336 pub secure_note: Option<secure_note::SecureNoteView>,
337 pub ssh_key: Option<ssh_key::SshKeyView>,
338
339 pub favorite: bool,
340 pub reprompt: CipherRepromptType,
341 pub organization_use_totp: bool,
342 pub edit: bool,
343 pub permissions: Option<CipherPermissions>,
344 pub view_password: bool,
345 pub local_data: Option<LocalDataView>,
346
347 pub attachments: Option<Vec<attachment::AttachmentView>>,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
351 pub fields: Option<Vec<field::FieldView>>,
352 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
353 pub creation_date: DateTime<Utc>,
354 pub deleted_date: Option<DateTime<Utc>>,
355 pub revision_date: DateTime<Utc>,
356 pub archived_date: Option<DateTime<Utc>>,
357}
358
359#[allow(missing_docs)]
360#[derive(Serialize, Deserialize, Debug, PartialEq)]
361#[serde(rename_all = "camelCase", deny_unknown_fields)]
362#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
363#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
364pub enum CipherListViewType {
365 Login(LoginListView),
366 SecureNote,
367 Card(CardListView),
368 Identity,
369 SshKey,
370}
371
372#[derive(Serialize, Deserialize, Debug, PartialEq)]
374#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
375#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
376pub enum CopyableCipherFields {
377 LoginUsername,
378 LoginPassword,
379 LoginTotp,
380 CardNumber,
381 CardSecurityCode,
382 IdentityUsername,
383 IdentityEmail,
384 IdentityPhone,
385 IdentityAddress,
386 SshKey,
387 SecureNotes,
388}
389
390#[allow(missing_docs)]
391#[derive(Serialize, Deserialize, Debug, PartialEq)]
392#[serde(rename_all = "camelCase", deny_unknown_fields)]
393#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
394#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
395pub struct CipherListView {
396 pub id: Option<CipherId>,
397 pub organization_id: Option<OrganizationId>,
398 pub folder_id: Option<FolderId>,
399 pub collection_ids: Vec<CollectionId>,
400
401 pub key: Option<EncString>,
403
404 pub name: String,
405 pub subtitle: String,
406
407 pub r#type: CipherListViewType,
408
409 pub favorite: bool,
410 pub reprompt: CipherRepromptType,
411 pub organization_use_totp: bool,
412 pub edit: bool,
413 pub permissions: Option<CipherPermissions>,
414
415 pub view_password: bool,
416
417 pub attachments: u32,
419 pub has_old_attachments: bool,
421
422 pub creation_date: DateTime<Utc>,
423 pub deleted_date: Option<DateTime<Utc>>,
424 pub revision_date: DateTime<Utc>,
425 pub archived_date: Option<DateTime<Utc>>,
426
427 pub copyable_fields: Vec<CopyableCipherFields>,
429
430 pub local_data: Option<LocalDataView>,
431}
432
433#[derive(Serialize, Deserialize, Debug)]
439#[serde(rename_all = "camelCase", deny_unknown_fields)]
440#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
441#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
442pub struct DecryptCipherListResult {
443 pub successes: Vec<CipherListView>,
445 pub failures: Vec<Cipher>,
447}
448
449impl CipherListView {
450 pub(crate) fn get_totp_key(
451 self,
452 ctx: &mut KeyStoreContext<KeyIds>,
453 ) -> Result<Option<String>, CryptoError> {
454 let key = self.key_identifier();
455 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
456
457 let totp = match self.r#type {
458 CipherListViewType::Login(LoginListView { totp, .. }) => {
459 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
460 }
461 _ => None,
462 };
463
464 Ok(totp)
465 }
466}
467
468impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
469 fn encrypt_composite(
470 &self,
471 ctx: &mut KeyStoreContext<KeyIds>,
472 key: SymmetricKeyId,
473 ) -> Result<Cipher, CryptoError> {
474 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
475
476 let mut cipher_view = self.clone();
477 cipher_view.generate_checksums();
478
479 Ok(Cipher {
480 id: cipher_view.id,
481 organization_id: cipher_view.organization_id,
482 folder_id: cipher_view.folder_id,
483 collection_ids: cipher_view.collection_ids,
484 key: cipher_view.key,
485 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
486 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
487 r#type: cipher_view.r#type,
488 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
489 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
490 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
491 secure_note: cipher_view
492 .secure_note
493 .encrypt_composite(ctx, ciphers_key)?,
494 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
495 favorite: cipher_view.favorite,
496 reprompt: cipher_view.reprompt,
497 organization_use_totp: cipher_view.organization_use_totp,
498 edit: cipher_view.edit,
499 view_password: cipher_view.view_password,
500 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
501 attachments: cipher_view
502 .attachments
503 .encrypt_composite(ctx, ciphers_key)?,
504 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
505 password_history: cipher_view
506 .password_history
507 .encrypt_composite(ctx, ciphers_key)?,
508 creation_date: cipher_view.creation_date,
509 deleted_date: cipher_view.deleted_date,
510 revision_date: cipher_view.revision_date,
511 permissions: cipher_view.permissions,
512 archived_date: cipher_view.archived_date,
513 data: None, })
515 }
516}
517
518impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
519 #[instrument(err, skip_all, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
520 fn decrypt(
521 &self,
522 ctx: &mut KeyStoreContext<KeyIds>,
523 key: SymmetricKeyId,
524 ) -> Result<CipherView, CryptoError> {
525 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
526
527 let (attachments, attachment_decryption_failures) =
529 attachment::decrypt_attachments_with_failures(
530 self.attachments.as_deref().unwrap_or_default(),
531 ctx,
532 ciphers_key,
533 );
534
535 let mut cipher = CipherView {
536 id: self.id,
537 organization_id: self.organization_id,
538 folder_id: self.folder_id,
539 collection_ids: self.collection_ids.clone(),
540 key: self.key.clone(),
541 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
542 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
543 r#type: self.r#type,
544 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
545 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
546 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
547 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
548 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
549 favorite: self.favorite,
550 reprompt: self.reprompt,
551 organization_use_totp: self.organization_use_totp,
552 edit: self.edit,
553 permissions: self.permissions,
554 view_password: self.view_password,
555 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
556 attachments: Some(attachments),
557 attachment_decryption_failures: Some(attachment_decryption_failures),
558 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
559 password_history: self
560 .password_history
561 .decrypt(ctx, ciphers_key)
562 .ok()
563 .flatten(),
564 creation_date: self.creation_date,
565 deleted_date: self.deleted_date,
566 revision_date: self.revision_date,
567 archived_date: self.archived_date,
568 };
569
570 if cipher.key.is_some()
573 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
574 {
575 cipher.remove_invalid_checksums();
576 }
577
578 Ok(cipher)
579 }
580}
581
582impl Cipher {
583 #[instrument(err, skip_all)]
594 pub(super) fn decrypt_cipher_key(
595 ctx: &mut KeyStoreContext<KeyIds>,
596 key: SymmetricKeyId,
597 ciphers_key: &Option<EncString>,
598 ) -> Result<SymmetricKeyId, CryptoError> {
599 match ciphers_key {
600 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
601 None => Ok(key),
602 }
603 }
604
605 fn get_kind(&self) -> Option<&dyn CipherKind> {
607 match self.r#type {
608 CipherType::Login => self.login.as_ref().map(|v| v as _),
609 CipherType::Card => self.card.as_ref().map(|v| v as _),
610 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
611 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
612 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
613 }
614 }
615
616 fn decrypt_subtitle(
618 &self,
619 ctx: &mut KeyStoreContext<KeyIds>,
620 key: SymmetricKeyId,
621 ) -> Result<String, CryptoError> {
622 self.get_kind()
623 .map(|sub| sub.decrypt_subtitle(ctx, key))
624 .unwrap_or_else(|| Ok(String::new()))
625 }
626
627 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
630 self.get_kind()
631 .map(|kind| kind.get_copyable_fields(Some(self)))
632 .unwrap_or_default()
633 }
634
635 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
640 let data = self
641 .data
642 .as_ref()
643 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
644
645 match &self.r#type {
646 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
647 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
648 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
649 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
650 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
651 }
652 Ok(())
653 }
654
655 pub(crate) fn soft_delete(&mut self) {
657 self.deleted_date = Some(Utc::now());
658 }
659}
660impl CipherView {
661 #[allow(missing_docs)]
662 pub fn generate_cipher_key(
663 &mut self,
664 ctx: &mut KeyStoreContext<KeyIds>,
665 key: SymmetricKeyId,
666 ) -> Result<(), CryptoError> {
667 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
668
669 let new_key = ctx.generate_symmetric_key();
670
671 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
672 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
673
674 self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
675 Ok(())
676 }
677
678 #[allow(missing_docs)]
679 pub fn generate_checksums(&mut self) {
680 if let Some(l) = self.login.as_mut() {
681 l.generate_checksums();
682 }
683 }
684
685 #[allow(missing_docs)]
686 pub fn remove_invalid_checksums(&mut self) {
687 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
688 uris.retain(|u| u.is_checksum_valid());
689 }
690 }
691
692 fn reencrypt_attachment_keys(
693 &mut self,
694 ctx: &mut KeyStoreContext<KeyIds>,
695 old_key: SymmetricKeyId,
696 new_key: SymmetricKeyId,
697 ) -> Result<(), CryptoError> {
698 if let Some(attachments) = &mut self.attachments {
699 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
700 }
701 Ok(())
702 }
703
704 #[allow(missing_docs)]
705 pub fn decrypt_fido2_credentials(
706 &self,
707 ctx: &mut KeyStoreContext<KeyIds>,
708 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
709 let key = self.key_identifier();
710 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
711
712 Ok(self
713 .login
714 .as_ref()
715 .and_then(|l| l.fido2_credentials.as_ref())
716 .map(|f| f.decrypt(ctx, ciphers_key))
717 .transpose()?
718 .unwrap_or_default())
719 }
720
721 fn reencrypt_fido2_credentials(
722 &mut self,
723 ctx: &mut KeyStoreContext<KeyIds>,
724 old_key: SymmetricKeyId,
725 new_key: SymmetricKeyId,
726 ) -> Result<(), CryptoError> {
727 if let Some(login) = self.login.as_mut() {
728 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
729 }
730 Ok(())
731 }
732
733 pub fn move_to_organization(
740 &mut self,
741 ctx: &mut KeyStoreContext<KeyIds>,
742 organization_id: OrganizationId,
743 ) -> Result<(), CipherError> {
744 let new_key = SymmetricKeyId::Organization(organization_id);
745
746 self.reencrypt_cipher_keys(ctx, new_key)?;
747 self.organization_id = Some(organization_id);
748
749 Ok(())
750 }
751
752 pub fn reencrypt_cipher_keys(
757 &mut self,
758 ctx: &mut KeyStoreContext<KeyIds>,
759 new_wrapping_key: SymmetricKeyId,
760 ) -> Result<(), CipherError> {
761 let old_key = self.key_identifier();
762
763 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
765 return Err(CipherError::AttachmentsWithoutKeys);
766 }
767
768 if self.key.is_some() {
770 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
772
773 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
775 } else {
776 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
779 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
780 }
781
782 Ok(())
783 }
784
785 #[allow(missing_docs)]
786 pub fn set_new_fido2_credentials(
787 &mut self,
788 ctx: &mut KeyStoreContext<KeyIds>,
789 creds: Vec<Fido2CredentialFullView>,
790 ) -> Result<(), CipherError> {
791 let key = self.key_identifier();
792
793 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
794
795 require!(self.login.as_mut()).fido2_credentials =
796 Some(creds.encrypt_composite(ctx, ciphers_key)?);
797
798 Ok(())
799 }
800
801 #[allow(missing_docs)]
802 pub fn get_fido2_credentials(
803 &self,
804 ctx: &mut KeyStoreContext<KeyIds>,
805 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
806 let key = self.key_identifier();
807
808 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
809
810 let login = require!(self.login.as_ref());
811 let creds = require!(login.fido2_credentials.as_ref());
812 let res = creds.decrypt(ctx, ciphers_key)?;
813 Ok(res)
814 }
815
816 #[allow(missing_docs)]
817 pub fn decrypt_fido2_private_key(
818 &self,
819 ctx: &mut KeyStoreContext<KeyIds>,
820 ) -> Result<String, CipherError> {
821 let fido2_credential = self.get_fido2_credentials(ctx)?;
822
823 Ok(fido2_credential[0].key_value.clone())
824 }
825
826 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
827 let changes = self
828 .login
829 .as_mut()
830 .map_or(vec![], |login| {
831 login.detect_password_change(&original_cipher.login)
832 })
833 .into_iter()
834 .chain(self.fields.as_deref().map_or(vec![], |fields| {
835 FieldView::detect_hidden_field_changes(
836 fields,
837 original_cipher.fields.as_deref().unwrap_or(&[]),
838 )
839 }))
840 .rev()
841 .chain(original_cipher.password_history.iter().flatten().cloned())
842 .take(MAX_PASSWORD_HISTORY_ENTRIES)
843 .collect();
844 self.password_history = Some(changes)
845 }
846}
847
848impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
849 fn decrypt(
850 &self,
851 ctx: &mut KeyStoreContext<KeyIds>,
852 key: SymmetricKeyId,
853 ) -> Result<CipherListView, CryptoError> {
854 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
855
856 Ok(CipherListView {
857 id: self.id,
858 organization_id: self.organization_id,
859 folder_id: self.folder_id,
860 collection_ids: self.collection_ids.clone(),
861 key: self.key.clone(),
862 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
863 subtitle: self
864 .decrypt_subtitle(ctx, ciphers_key)
865 .ok()
866 .unwrap_or_default(),
867 r#type: match self.r#type {
868 CipherType::Login => {
869 let login = self
870 .login
871 .as_ref()
872 .ok_or(CryptoError::MissingField("login"))?;
873 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
874 }
875 CipherType::SecureNote => CipherListViewType::SecureNote,
876 CipherType::Card => {
877 let card = self
878 .card
879 .as_ref()
880 .ok_or(CryptoError::MissingField("card"))?;
881 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
882 }
883 CipherType::Identity => CipherListViewType::Identity,
884 CipherType::SshKey => CipherListViewType::SshKey,
885 },
886 favorite: self.favorite,
887 reprompt: self.reprompt,
888 organization_use_totp: self.organization_use_totp,
889 edit: self.edit,
890 permissions: self.permissions,
891 view_password: self.view_password,
892 attachments: self
893 .attachments
894 .as_ref()
895 .map(|a| a.len() as u32)
896 .unwrap_or(0),
897 has_old_attachments: self
898 .attachments
899 .as_ref()
900 .map(|a| a.iter().any(|att| att.key.is_none()))
901 .unwrap_or(false),
902 creation_date: self.creation_date,
903 deleted_date: self.deleted_date,
904 revision_date: self.revision_date,
905 copyable_fields: self.get_copyable_fields(),
906 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
907 archived_date: self.archived_date,
908 })
909 }
910}
911
912impl IdentifyKey<SymmetricKeyId> for Cipher {
913 fn key_identifier(&self) -> SymmetricKeyId {
914 match self.organization_id {
915 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
916 None => SymmetricKeyId::User,
917 }
918 }
919}
920
921impl IdentifyKey<SymmetricKeyId> for CipherView {
922 fn key_identifier(&self) -> SymmetricKeyId {
923 match self.organization_id {
924 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
925 None => SymmetricKeyId::User,
926 }
927 }
928}
929
930impl IdentifyKey<SymmetricKeyId> for CipherListView {
931 fn key_identifier(&self) -> SymmetricKeyId {
932 match self.organization_id {
933 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
934 None => SymmetricKeyId::User,
935 }
936 }
937}
938
939impl TryFrom<CipherDetailsResponseModel> for Cipher {
940 type Error = VaultParseError;
941
942 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
943 Ok(Self {
944 id: cipher.id.map(CipherId::new),
945 organization_id: cipher.organization_id.map(OrganizationId::new),
946 folder_id: cipher.folder_id.map(FolderId::new),
947 collection_ids: cipher
948 .collection_ids
949 .unwrap_or_default()
950 .into_iter()
951 .map(CollectionId::new)
952 .collect(),
953 name: require!(EncString::try_from_optional(cipher.name)?),
954 notes: EncString::try_from_optional(cipher.notes)?,
955 r#type: require!(cipher.r#type).into(),
956 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
957 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
958 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
959 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
960 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
961 favorite: cipher.favorite.unwrap_or(false),
962 reprompt: cipher
963 .reprompt
964 .map(|r| r.into())
965 .unwrap_or(CipherRepromptType::None),
966 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
967 edit: cipher.edit.unwrap_or(true),
968 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
969 view_password: cipher.view_password.unwrap_or(true),
970 local_data: None, attachments: cipher
972 .attachments
973 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
974 .transpose()?,
975 fields: cipher
976 .fields
977 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
978 .transpose()?,
979 password_history: cipher
980 .password_history
981 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
982 .transpose()?,
983 creation_date: require!(cipher.creation_date).parse()?,
984 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
985 revision_date: require!(cipher.revision_date).parse()?,
986 key: EncString::try_from_optional(cipher.key)?,
987 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
988 data: cipher.data,
989 })
990 }
991}
992
993impl PartialCipher for CipherDetailsResponseModel {
994 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
995 Ok(Cipher {
996 local_data: cipher.and_then(|c| c.local_data),
997 ..self.try_into()?
998 })
999 }
1000}
1001
1002impl From<bitwarden_api_api::models::CipherType> for CipherType {
1003 fn from(t: bitwarden_api_api::models::CipherType) -> Self {
1004 match t {
1005 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1006 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1007 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1008 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1009 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1010 }
1011 }
1012}
1013
1014impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1015 fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
1016 match t {
1017 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1018 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1019 }
1020 }
1021}
1022
1023pub(crate) trait PartialCipher {
1027 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1028}
1029
1030impl From<CipherType> for bitwarden_api_api::models::CipherType {
1031 fn from(t: CipherType) -> Self {
1032 match t {
1033 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1034 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1035 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1036 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1037 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1038 }
1039 }
1040}
1041
1042impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1043 fn from(t: CipherRepromptType) -> Self {
1044 match t {
1045 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1046 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1047 }
1048 }
1049}
1050
1051impl TryFrom<CipherResponseModel> for Cipher {
1052 type Error = VaultParseError;
1053
1054 fn try_from(cipher: CipherResponseModel) -> Result<Self, Self::Error> {
1055 Ok(Self {
1056 id: cipher.id.map(CipherId::new),
1057 organization_id: cipher.organization_id.map(OrganizationId::new),
1058 folder_id: cipher.folder_id.map(FolderId::new),
1059 collection_ids: vec![], name: require!(cipher.name).parse()?,
1061 notes: EncString::try_from_optional(cipher.notes)?,
1062 r#type: require!(cipher.r#type).into(),
1063 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1064 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1065 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1066 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1067 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1068 favorite: cipher.favorite.unwrap_or(false),
1069 reprompt: cipher
1070 .reprompt
1071 .map(|r| r.into())
1072 .unwrap_or(CipherRepromptType::None),
1073 organization_use_totp: cipher.organization_use_totp.unwrap_or(false),
1074 edit: cipher.edit.unwrap_or(false),
1075 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1076 view_password: cipher.view_password.unwrap_or(true),
1077 local_data: None, attachments: cipher
1079 .attachments
1080 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1081 .transpose()?,
1082 fields: cipher
1083 .fields
1084 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1085 .transpose()?,
1086 password_history: cipher
1087 .password_history
1088 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1089 .transpose()?,
1090 creation_date: require!(cipher.creation_date).parse()?,
1091 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1092 revision_date: require!(cipher.revision_date).parse()?,
1093 key: EncString::try_from_optional(cipher.key)?,
1094 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1095 data: cipher.data,
1096 })
1097 }
1098}
1099
1100impl PartialCipher for CipherMiniResponseModel {
1101 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1102 let cipher = cipher.as_ref();
1103 Ok(Cipher {
1104 id: self.id.map(CipherId::new),
1105 organization_id: self.organization_id.map(OrganizationId::new),
1106 key: EncString::try_from_optional(self.key)?,
1107 name: require!(EncString::try_from_optional(self.name)?),
1108 notes: EncString::try_from_optional(self.notes)?,
1109 r#type: require!(self.r#type).into(),
1110 login: self.login.map(|l| (*l).try_into()).transpose()?,
1111 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1112 card: self.card.map(|c| (*c).try_into()).transpose()?,
1113 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1114 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1115 reprompt: self
1116 .reprompt
1117 .map(|r| r.into())
1118 .unwrap_or(CipherRepromptType::None),
1119 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1120 attachments: self
1121 .attachments
1122 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1123 .transpose()?,
1124 fields: self
1125 .fields
1126 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1127 .transpose()?,
1128 password_history: self
1129 .password_history
1130 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1131 .transpose()?,
1132 creation_date: require!(self.creation_date)
1133 .parse()
1134 .map_err(Into::<VaultParseError>::into)?,
1135 deleted_date: self
1136 .deleted_date
1137 .map(|d| d.parse())
1138 .transpose()
1139 .map_err(Into::<VaultParseError>::into)?,
1140 revision_date: require!(self.revision_date)
1141 .parse()
1142 .map_err(Into::<VaultParseError>::into)?,
1143 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1144 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1145 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1146 edit: cipher.map_or(Default::default(), |c| c.edit),
1147 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1148 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1149 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1150 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1151 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1152 })
1153 }
1154}
1155
1156impl PartialCipher for CipherMiniDetailsResponseModel {
1157 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1158 let cipher = cipher.as_ref();
1159 Ok(Cipher {
1160 id: self.id.map(CipherId::new),
1161 organization_id: self.organization_id.map(OrganizationId::new),
1162 key: EncString::try_from_optional(self.key)?,
1163 name: require!(EncString::try_from_optional(self.name)?),
1164 notes: EncString::try_from_optional(self.notes)?,
1165 r#type: require!(self.r#type).into(),
1166 login: self.login.map(|l| (*l).try_into()).transpose()?,
1167 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1168 card: self.card.map(|c| (*c).try_into()).transpose()?,
1169 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1170 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1171 reprompt: self
1172 .reprompt
1173 .map(|r| r.into())
1174 .unwrap_or(CipherRepromptType::None),
1175 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1176 attachments: self
1177 .attachments
1178 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1179 .transpose()?,
1180 fields: self
1181 .fields
1182 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1183 .transpose()?,
1184 password_history: self
1185 .password_history
1186 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1187 .transpose()?,
1188 creation_date: require!(self.creation_date)
1189 .parse()
1190 .map_err(Into::<VaultParseError>::into)?,
1191 deleted_date: self
1192 .deleted_date
1193 .map(|d| d.parse())
1194 .transpose()
1195 .map_err(Into::<VaultParseError>::into)?,
1196 revision_date: require!(self.revision_date)
1197 .parse()
1198 .map_err(Into::<VaultParseError>::into)?,
1199 collection_ids: self
1200 .collection_ids
1201 .into_iter()
1202 .flatten()
1203 .map(CollectionId::new)
1204 .collect(),
1205 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1206 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1207 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1208 edit: cipher.map_or(Default::default(), |c| c.edit),
1209 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1210 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1211 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1212 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1213 })
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219
1220 use attachment::AttachmentView;
1221 use bitwarden_core::key_management::{
1222 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1223 };
1224 use bitwarden_crypto::SymmetricCryptoKey;
1225
1226 use super::*;
1227 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1228
1229 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1231 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1232 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1233 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1234 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1235 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1236 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1237
1238 fn generate_cipher() -> CipherView {
1239 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1240 CipherView {
1241 r#type: CipherType::Login,
1242 login: Some(LoginView {
1243 username: Some("test_username".to_string()),
1244 password: Some("test_password".to_string()),
1245 password_revision_date: None,
1246 uris: None,
1247 totp: None,
1248 autofill_on_page_load: None,
1249 fido2_credentials: None,
1250 }),
1251 id: Some(test_id),
1252 organization_id: None,
1253 folder_id: None,
1254 collection_ids: vec![],
1255 key: None,
1256 name: "My test login".to_string(),
1257 notes: None,
1258 identity: None,
1259 card: None,
1260 secure_note: None,
1261 ssh_key: None,
1262 favorite: false,
1263 reprompt: CipherRepromptType::None,
1264 organization_use_totp: true,
1265 edit: true,
1266 permissions: None,
1267 view_password: true,
1268 local_data: None,
1269 attachments: None,
1270 attachment_decryption_failures: None,
1271 fields: None,
1272 password_history: None,
1273 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1274 deleted_date: None,
1275 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1276 archived_date: None,
1277 }
1278 }
1279
1280 fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
1281 Fido2Credential {
1282 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1283 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1284 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1285 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1286 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1287 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1288 user_handle: None,
1289 user_name: None,
1290 counter: "123".to_string().encrypt(ctx, key).unwrap(),
1291 rp_name: None,
1292 user_display_name: None,
1293 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1294 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1295 }
1296 }
1297
1298 #[test]
1299 fn test_decrypt_cipher_list_view() {
1300 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1301 let key_store = create_test_crypto_with_user_key(key);
1302
1303 let cipher = Cipher {
1304 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1305 organization_id: None,
1306 folder_id: None,
1307 collection_ids: vec![],
1308 key: None,
1309 name: TEST_CIPHER_NAME.parse().unwrap(),
1310 notes: None,
1311 r#type: CipherType::Login,
1312 login: Some(Login {
1313 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1314 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1315 password_revision_date: None,
1316 uris: None,
1317 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1318 autofill_on_page_load: None,
1319 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
1320 }),
1321 identity: None,
1322 card: None,
1323 secure_note: None,
1324 ssh_key: None,
1325 favorite: false,
1326 reprompt: CipherRepromptType::None,
1327 organization_use_totp: false,
1328 edit: true,
1329 permissions: Some(CipherPermissions {
1330 delete: false,
1331 restore: false
1332 }),
1333 view_password: true,
1334 local_data: None,
1335 attachments: None,
1336 fields: None,
1337 password_history: None,
1338 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1339 deleted_date: None,
1340 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1341 archived_date: None,
1342 data: None,
1343 };
1344
1345 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1346
1347 assert_eq!(
1348 view,
1349 CipherListView {
1350 id: cipher.id,
1351 organization_id: cipher.organization_id,
1352 folder_id: cipher.folder_id,
1353 collection_ids: cipher.collection_ids,
1354 key: cipher.key,
1355 name: "My test login".to_string(),
1356 subtitle: "test_username".to_string(),
1357 r#type: CipherListViewType::Login(LoginListView {
1358 fido2_credentials: Some(vec![Fido2CredentialListView {
1359 credential_id: "123".to_string(),
1360 rp_id: "123".to_string(),
1361 user_handle: None,
1362 user_name: None,
1363 user_display_name: None,
1364 counter: "123".to_string(),
1365 }]),
1366 has_fido2: true,
1367 username: Some("test_username".to_string()),
1368 totp: cipher.login.as_ref().unwrap().totp.clone(),
1369 uris: None,
1370 }),
1371 favorite: cipher.favorite,
1372 reprompt: cipher.reprompt,
1373 organization_use_totp: cipher.organization_use_totp,
1374 edit: cipher.edit,
1375 permissions: cipher.permissions,
1376 view_password: cipher.view_password,
1377 attachments: 0,
1378 has_old_attachments: false,
1379 creation_date: cipher.creation_date,
1380 deleted_date: cipher.deleted_date,
1381 revision_date: cipher.revision_date,
1382 copyable_fields: vec![
1383 CopyableCipherFields::LoginUsername,
1384 CopyableCipherFields::LoginPassword,
1385 CopyableCipherFields::LoginTotp
1386 ],
1387 local_data: None,
1388 archived_date: cipher.archived_date,
1389 }
1390 )
1391 }
1392
1393 #[test]
1394 fn test_generate_cipher_key() {
1395 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1396 let key_store = create_test_crypto_with_user_key(key);
1397
1398 let original_cipher = generate_cipher();
1399
1400 let cipher = generate_cipher();
1402 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1403 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1404 assert!(no_key_cipher_dec.key.is_none());
1405 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1406
1407 let mut cipher = generate_cipher();
1408 cipher
1409 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1410 .unwrap();
1411
1412 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1414 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1415 assert!(key_cipher_dec.key.is_some());
1416 assert_eq!(key_cipher_dec.name, original_cipher.name);
1417 }
1418
1419 #[test]
1420 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1421 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1422 let key_store = create_test_crypto_with_user_key(key);
1423
1424 let mut original_cipher = generate_cipher();
1425 {
1426 let mut ctx = key_store.context();
1427 let cipher_key = ctx.generate_symmetric_key();
1428
1429 original_cipher.key = Some(
1430 ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1431 .unwrap(),
1432 );
1433 }
1434
1435 original_cipher
1436 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1437 .unwrap();
1438
1439 let wrapped_key = original_cipher.key.unwrap();
1441 let mut ctx = key_store.context();
1442 ctx.unwrap_symmetric_key(SymmetricKeyId::User, &wrapped_key)
1443 .unwrap();
1444 }
1445
1446 #[test]
1447 fn test_generate_cipher_key_ignores_attachments_without_key() {
1448 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1449 let key_store = create_test_crypto_with_user_key(key);
1450
1451 let mut cipher = generate_cipher();
1452 let attachment = AttachmentView {
1453 id: None,
1454 url: None,
1455 size: None,
1456 size_name: None,
1457 file_name: Some("Attachment test name".into()),
1458 key: None,
1459 #[cfg(feature = "wasm")]
1460 decrypted_key: None,
1461 };
1462 cipher.attachments = Some(vec![attachment]);
1463
1464 cipher
1465 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1466 .unwrap();
1467 assert!(cipher.attachments.unwrap()[0].key.is_none());
1468 }
1469
1470 #[test]
1471 fn test_reencrypt_cipher_key() {
1472 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1473 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1474 let key_store = create_test_crypto_with_user_key(old_key);
1475 let mut ctx = key_store.context_mut();
1476
1477 let mut cipher = generate_cipher();
1478 cipher
1479 .generate_cipher_key(&mut ctx, cipher.key_identifier())
1480 .unwrap();
1481
1482 let new_key_id = ctx.add_local_symmetric_key(new_key);
1484
1485 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
1486
1487 assert!(cipher.key.is_some());
1489 assert!(
1490 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
1491 .is_ok()
1492 );
1493 }
1494
1495 #[test]
1496 fn test_reencrypt_cipher_key_ignores_missing_key() {
1497 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1498 let key_store = create_test_crypto_with_user_key(key);
1499 let mut ctx = key_store.context_mut();
1500 let mut cipher = generate_cipher();
1501
1502 let new_cipher_key = ctx.generate_symmetric_key();
1504 cipher
1505 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
1506 .unwrap();
1507
1508 assert!(cipher.key.is_none());
1510 }
1511
1512 #[test]
1513 fn test_move_user_cipher_to_org() {
1514 let org = OrganizationId::new_v4();
1515 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1516 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1517 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1518
1519 let mut cipher = generate_cipher();
1521 cipher
1522 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1523 .unwrap();
1524
1525 cipher
1526 .move_to_organization(&mut key_store.context(), org)
1527 .unwrap();
1528 assert_eq!(cipher.organization_id, Some(org));
1529
1530 let cipher_enc = key_store.encrypt(cipher).unwrap();
1532 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1533
1534 assert_eq!(cipher_dec.name, "My test login");
1535 }
1536
1537 #[test]
1538 fn test_move_user_cipher_to_org_manually() {
1539 let org = OrganizationId::new_v4();
1540 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1541 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1542 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1543
1544 let mut cipher = generate_cipher();
1546 cipher
1547 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1548 .unwrap();
1549
1550 cipher.organization_id = Some(org);
1551
1552 assert!(key_store.encrypt(cipher).is_err());
1555 }
1556
1557 #[test]
1558 fn test_move_user_cipher_with_attachment_without_key_to_org() {
1559 let org = OrganizationId::new_v4();
1560 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1561 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1562 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1563
1564 let mut cipher = generate_cipher();
1565 let attachment = AttachmentView {
1566 id: None,
1567 url: None,
1568 size: None,
1569 size_name: None,
1570 file_name: Some("Attachment test name".into()),
1571 key: None,
1572 #[cfg(feature = "wasm")]
1573 decrypted_key: None,
1574 };
1575 cipher.attachments = Some(vec![attachment]);
1576
1577 assert!(
1579 cipher
1580 .move_to_organization(&mut key_store.context(), org)
1581 .is_err()
1582 );
1583 }
1584
1585 #[test]
1586 fn test_move_user_cipher_with_attachment_with_key_to_org() {
1587 let org = OrganizationId::new_v4();
1588 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1589 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1590 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1591 let org_key = SymmetricKeyId::Organization(org);
1592
1593 let (attachment_key_enc, attachment_key_val) = {
1595 let mut ctx = key_store.context();
1596 let attachment_key = ctx.generate_symmetric_key();
1597 let attachment_key_enc = ctx
1598 .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1599 .unwrap();
1600 #[allow(deprecated)]
1601 let attachment_key_val = ctx
1602 .dangerous_get_symmetric_key(attachment_key)
1603 .unwrap()
1604 .clone();
1605
1606 (attachment_key_enc, attachment_key_val)
1607 };
1608
1609 let mut cipher = generate_cipher();
1610 let attachment = AttachmentView {
1611 id: None,
1612 url: None,
1613 size: None,
1614 size_name: None,
1615 file_name: Some("Attachment test name".into()),
1616 key: Some(attachment_key_enc),
1617 #[cfg(feature = "wasm")]
1618 decrypted_key: None,
1619 };
1620 cipher.attachments = Some(vec![attachment]);
1621 let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1622 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1623
1624 cipher
1625 .move_to_organization(&mut key_store.context(), org)
1626 .unwrap();
1627
1628 assert!(cipher.key.is_none());
1629
1630 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1633 let mut ctx = key_store.context();
1634 let new_attachment_key_id = ctx
1635 .unwrap_symmetric_key(org_key, &new_attachment_key)
1636 .unwrap();
1637 #[allow(deprecated)]
1638 let new_attachment_key_dec = ctx
1639 .dangerous_get_symmetric_key(new_attachment_key_id)
1640 .unwrap();
1641
1642 assert_eq!(*new_attachment_key_dec, attachment_key_val);
1643
1644 let cred2: Fido2CredentialFullView = cipher
1645 .login
1646 .unwrap()
1647 .fido2_credentials
1648 .unwrap()
1649 .first()
1650 .unwrap()
1651 .decrypt(&mut key_store.context(), org_key)
1652 .unwrap();
1653
1654 assert_eq!(cred2.credential_id, "123");
1655 }
1656
1657 #[test]
1658 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1659 let org = OrganizationId::new_v4();
1660 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1661 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1662 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1663 let org_key = SymmetricKeyId::Organization(org);
1664
1665 let mut ctx = key_store.context();
1666
1667 let cipher_key = ctx.generate_symmetric_key();
1668 let cipher_key_enc = ctx
1669 .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1670 .unwrap();
1671
1672 let attachment_key = ctx.generate_symmetric_key();
1674 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1675
1676 let mut cipher = generate_cipher();
1677 cipher.key = Some(cipher_key_enc);
1678
1679 let attachment = AttachmentView {
1680 id: None,
1681 url: None,
1682 size: None,
1683 size_name: None,
1684 file_name: Some("Attachment test name".into()),
1685 key: Some(attachment_key_enc.clone()),
1686 #[cfg(feature = "wasm")]
1687 decrypted_key: None,
1688 };
1689 cipher.attachments = Some(vec![attachment]);
1690
1691 let cred = generate_fido2(&mut ctx, cipher_key);
1692 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1693
1694 cipher.move_to_organization(&mut ctx, org).unwrap();
1695
1696 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1698 let new_cipher_key_dec = ctx
1699 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
1700 .unwrap();
1701 #[allow(deprecated)]
1702 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1703 #[allow(deprecated)]
1704 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1705
1706 assert_eq!(new_cipher_key_dec, cipher_key_val);
1707
1708 assert_eq!(
1710 cipher.attachments.unwrap()[0]
1711 .key
1712 .as_ref()
1713 .unwrap()
1714 .to_string(),
1715 attachment_key_enc.to_string()
1716 );
1717
1718 let cred2: Fido2Credential = cipher
1719 .login
1720 .unwrap()
1721 .fido2_credentials
1722 .unwrap()
1723 .first()
1724 .unwrap()
1725 .clone();
1726
1727 assert_eq!(
1728 cred2.credential_id.to_string(),
1729 cred.credential_id.to_string()
1730 );
1731 }
1732
1733 #[test]
1734 fn test_decrypt_fido2_private_key() {
1735 let key_store =
1736 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1737 let mut ctx = key_store.context();
1738
1739 let mut cipher_view = generate_cipher();
1740 cipher_view
1741 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1742 .unwrap();
1743
1744 let key_id = cipher_view.key_identifier();
1745 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1746
1747 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1748
1749 cipher_view.login.as_mut().unwrap().fido2_credentials =
1750 Some(vec![fido2_credential.clone()]);
1751
1752 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1753 assert_eq!(decrypted_key_value, "123");
1754 }
1755
1756 #[test]
1757 fn test_password_history_on_password_change() {
1758 use chrono::Utc;
1759
1760 let original_cipher = generate_cipher();
1761 let mut new_cipher = generate_cipher();
1762
1763 if let Some(ref mut login) = new_cipher.login {
1765 login.password = Some("new_password123".to_string());
1766 }
1767
1768 let start = Utc::now();
1769 new_cipher.update_password_history(&original_cipher);
1770 let end = Utc::now();
1771
1772 assert!(new_cipher.password_history.is_some());
1773 let history = new_cipher.password_history.unwrap();
1774 assert_eq!(history.len(), 1);
1775 assert_eq!(history[0].password, "test_password");
1776 assert!(
1777 history[0].last_used_date >= start && history[0].last_used_date <= end,
1778 "last_used_date was not set properly"
1779 );
1780 }
1781
1782 #[test]
1783 fn test_password_history_on_unchanged_password() {
1784 let original_cipher = generate_cipher();
1785 let mut new_cipher = generate_cipher();
1786
1787 new_cipher.update_password_history(&original_cipher);
1788
1789 assert!(
1791 new_cipher.password_history.is_none()
1792 || new_cipher.password_history.as_ref().unwrap().is_empty()
1793 );
1794 }
1795
1796 #[test]
1797 fn test_password_history_is_preserved() {
1798 use chrono::TimeZone;
1799
1800 let mut original_cipher = generate_cipher();
1801 original_cipher.password_history = Some(
1802 (0..4)
1803 .map(|i| PasswordHistoryView {
1804 password: format!("old_password_{}", i),
1805 last_used_date: chrono::Utc
1806 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
1807 .unwrap(),
1808 })
1809 .collect(),
1810 );
1811
1812 let mut new_cipher = generate_cipher();
1813
1814 new_cipher.update_password_history(&original_cipher);
1815
1816 assert!(new_cipher.password_history.is_some());
1817 let history = new_cipher.password_history.unwrap();
1818 assert_eq!(history.len(), 4);
1819
1820 assert_eq!(history[0].password, "old_password_0");
1821 assert_eq!(
1822 history[0].last_used_date,
1823 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
1824 );
1825 assert_eq!(history[1].password, "old_password_1");
1826 assert_eq!(
1827 history[1].last_used_date,
1828 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
1829 );
1830 assert_eq!(history[2].password, "old_password_2");
1831 assert_eq!(
1832 history[2].last_used_date,
1833 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
1834 );
1835 assert_eq!(history[3].password, "old_password_3");
1836 assert_eq!(
1837 history[3].last_used_date,
1838 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
1839 );
1840 }
1841
1842 #[test]
1843 fn test_populate_cipher_types_login_with_valid_data() {
1844 let mut cipher = Cipher {
1845 id: Some(TEST_UUID.parse().unwrap()),
1846 organization_id: None,
1847 folder_id: None,
1848 collection_ids: vec![],
1849 key: None,
1850 name: TEST_CIPHER_NAME.parse().unwrap(),
1851 notes: None,
1852 r#type: CipherType::Login,
1853 login: None,
1854 identity: None,
1855 card: None,
1856 secure_note: None,
1857 ssh_key: None,
1858 favorite: false,
1859 reprompt: CipherRepromptType::None,
1860 organization_use_totp: false,
1861 edit: true,
1862 view_password: true,
1863 permissions: None,
1864 local_data: None,
1865 attachments: None,
1866 fields: None,
1867 password_history: None,
1868 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1869 deleted_date: None,
1870 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1871 archived_date: None,
1872 data: Some(format!(
1873 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1874 TEST_ENC_STRING_1, TEST_ENC_STRING_2
1875 )),
1876 };
1877
1878 cipher
1879 .populate_cipher_types()
1880 .expect("populate_cipher_types failed");
1881
1882 assert!(cipher.login.is_some());
1883 let login = cipher.login.unwrap();
1884 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
1885 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
1886 }
1887
1888 #[test]
1889 fn test_populate_cipher_types_secure_note() {
1890 let mut cipher = Cipher {
1891 id: Some(TEST_UUID.parse().unwrap()),
1892 organization_id: None,
1893 folder_id: None,
1894 collection_ids: vec![],
1895 key: None,
1896 name: TEST_CIPHER_NAME.parse().unwrap(),
1897 notes: None,
1898 r#type: CipherType::SecureNote,
1899 login: None,
1900 identity: None,
1901 card: None,
1902 secure_note: None,
1903 ssh_key: None,
1904 favorite: false,
1905 reprompt: CipherRepromptType::None,
1906 organization_use_totp: false,
1907 edit: true,
1908 view_password: true,
1909 permissions: None,
1910 local_data: None,
1911 attachments: None,
1912 fields: None,
1913 password_history: None,
1914 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1915 deleted_date: None,
1916 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1917 archived_date: None,
1918 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
1919 };
1920
1921 cipher
1922 .populate_cipher_types()
1923 .expect("populate_cipher_types failed");
1924
1925 assert!(cipher.secure_note.is_some());
1926 }
1927
1928 #[test]
1929 fn test_populate_cipher_types_card() {
1930 let mut cipher = Cipher {
1931 id: Some(TEST_UUID.parse().unwrap()),
1932 organization_id: None,
1933 folder_id: None,
1934 collection_ids: vec![],
1935 key: None,
1936 name: TEST_CIPHER_NAME.parse().unwrap(),
1937 notes: None,
1938 r#type: CipherType::Card,
1939 login: None,
1940 identity: None,
1941 card: None,
1942 secure_note: None,
1943 ssh_key: None,
1944 favorite: false,
1945 reprompt: CipherRepromptType::None,
1946 organization_use_totp: false,
1947 edit: true,
1948 view_password: true,
1949 permissions: None,
1950 local_data: None,
1951 attachments: None,
1952 fields: None,
1953 password_history: None,
1954 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1955 deleted_date: None,
1956 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1957 archived_date: None,
1958 data: Some(format!(
1959 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1960 TEST_ENC_STRING_1,
1961 TEST_ENC_STRING_2,
1962 TEST_ENC_STRING_3,
1963 TEST_ENC_STRING_4,
1964 TEST_ENC_STRING_5,
1965 TEST_ENC_STRING_1
1966 )),
1967 };
1968
1969 cipher
1970 .populate_cipher_types()
1971 .expect("populate_cipher_types failed");
1972
1973 assert!(cipher.card.is_some());
1974 let card = cipher.card.unwrap();
1975 assert_eq!(
1976 card.cardholder_name.as_ref().unwrap().to_string(),
1977 TEST_ENC_STRING_1
1978 );
1979 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
1980 assert_eq!(
1981 card.exp_month.as_ref().unwrap().to_string(),
1982 TEST_ENC_STRING_3
1983 );
1984 assert_eq!(
1985 card.exp_year.as_ref().unwrap().to_string(),
1986 TEST_ENC_STRING_4
1987 );
1988 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
1989 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
1990 }
1991
1992 #[test]
1993 fn test_populate_cipher_types_identity() {
1994 let mut cipher = Cipher {
1995 id: Some(TEST_UUID.parse().unwrap()),
1996 organization_id: None,
1997 folder_id: None,
1998 collection_ids: vec![],
1999 key: None,
2000 name: TEST_CIPHER_NAME.parse().unwrap(),
2001 notes: None,
2002 r#type: CipherType::Identity,
2003 login: None,
2004 identity: None,
2005 card: None,
2006 secure_note: None,
2007 ssh_key: None,
2008 favorite: false,
2009 reprompt: CipherRepromptType::None,
2010 organization_use_totp: false,
2011 edit: true,
2012 view_password: true,
2013 permissions: None,
2014 local_data: None,
2015 attachments: None,
2016 fields: None,
2017 password_history: None,
2018 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2019 deleted_date: None,
2020 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2021 archived_date: None,
2022 data: Some(format!(
2023 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2024 TEST_ENC_STRING_1,
2025 TEST_ENC_STRING_2,
2026 TEST_ENC_STRING_3,
2027 TEST_ENC_STRING_4,
2028 TEST_ENC_STRING_5,
2029 TEST_ENC_STRING_1,
2030 TEST_ENC_STRING_2,
2031 TEST_ENC_STRING_3,
2032 TEST_ENC_STRING_4,
2033 TEST_ENC_STRING_5
2034 )),
2035 };
2036
2037 cipher
2038 .populate_cipher_types()
2039 .expect("populate_cipher_types failed");
2040
2041 assert!(cipher.identity.is_some());
2042 let identity = cipher.identity.unwrap();
2043 assert_eq!(
2044 identity.first_name.as_ref().unwrap().to_string(),
2045 TEST_ENC_STRING_1
2046 );
2047 assert_eq!(
2048 identity.last_name.as_ref().unwrap().to_string(),
2049 TEST_ENC_STRING_2
2050 );
2051 assert_eq!(
2052 identity.email.as_ref().unwrap().to_string(),
2053 TEST_ENC_STRING_3
2054 );
2055 assert_eq!(
2056 identity.phone.as_ref().unwrap().to_string(),
2057 TEST_ENC_STRING_4
2058 );
2059 assert_eq!(
2060 identity.company.as_ref().unwrap().to_string(),
2061 TEST_ENC_STRING_5
2062 );
2063 assert_eq!(
2064 identity.address1.as_ref().unwrap().to_string(),
2065 TEST_ENC_STRING_1
2066 );
2067 assert_eq!(
2068 identity.city.as_ref().unwrap().to_string(),
2069 TEST_ENC_STRING_2
2070 );
2071 assert_eq!(
2072 identity.state.as_ref().unwrap().to_string(),
2073 TEST_ENC_STRING_3
2074 );
2075 assert_eq!(
2076 identity.postal_code.as_ref().unwrap().to_string(),
2077 TEST_ENC_STRING_4
2078 );
2079 assert_eq!(
2080 identity.country.as_ref().unwrap().to_string(),
2081 TEST_ENC_STRING_5
2082 );
2083 }
2084
2085 #[test]
2086
2087 fn test_password_history_with_hidden_fields() {
2088 let mut original_cipher = generate_cipher();
2089 original_cipher.fields = Some(vec![FieldView {
2090 name: Some("Secret Key".to_string()),
2091 value: Some("old_secret_value".to_string()),
2092 r#type: crate::FieldType::Hidden,
2093 linked_id: None,
2094 }]);
2095
2096 let mut new_cipher = generate_cipher();
2097 new_cipher.fields = Some(vec![FieldView {
2098 name: Some("Secret Key".to_string()),
2099 value: Some("new_secret_value".to_string()),
2100 r#type: crate::FieldType::Hidden,
2101 linked_id: None,
2102 }]);
2103
2104 new_cipher.update_password_history(&original_cipher);
2105
2106 assert!(new_cipher.password_history.is_some());
2107 let history = new_cipher.password_history.unwrap();
2108 assert_eq!(history.len(), 1);
2109 assert_eq!(history[0].password, "Secret Key: old_secret_value");
2110 }
2111
2112 #[test]
2113 fn test_password_history_length_limit() {
2114 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
2115
2116 let mut original_cipher = generate_cipher();
2117 original_cipher.password_history = Some(
2118 (0..10)
2119 .map(|i| PasswordHistoryView {
2120 password: format!("old_password_{}", i),
2121 last_used_date: chrono::Utc::now(),
2122 })
2123 .collect(),
2124 );
2125
2126 let mut new_cipher = original_cipher.clone();
2127 if let Some(ref mut login) = new_cipher.login {
2129 login.password = Some("brand_new_password".to_string());
2130 }
2131
2132 new_cipher.update_password_history(&original_cipher);
2133
2134 assert!(new_cipher.password_history.is_some());
2135 let history = new_cipher.password_history.unwrap();
2136
2137 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
2139
2140 assert_eq!(history[0].password, "test_password");
2142 assert_eq!(history[1].password, "old_password_0");
2144 assert_eq!(history[2].password, "old_password_1");
2145 assert_eq!(history[3].password, "old_password_2");
2146 assert_eq!(history[4].password, "old_password_3");
2147 }
2148
2149 #[test]
2150 fn test_populate_cipher_types_ssh_key() {
2151 let mut cipher = Cipher {
2152 id: Some(TEST_UUID.parse().unwrap()),
2153 organization_id: None,
2154 folder_id: None,
2155 collection_ids: vec![],
2156 key: None,
2157 name: TEST_CIPHER_NAME.parse().unwrap(),
2158 notes: None,
2159 r#type: CipherType::SshKey,
2160 login: None,
2161 identity: None,
2162 card: None,
2163 secure_note: None,
2164 ssh_key: None,
2165 favorite: false,
2166 reprompt: CipherRepromptType::None,
2167 organization_use_totp: false,
2168 edit: true,
2169 view_password: true,
2170 permissions: None,
2171 local_data: None,
2172 attachments: None,
2173 fields: None,
2174 password_history: None,
2175 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2176 deleted_date: None,
2177 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2178 archived_date: None,
2179 data: Some(format!(
2180 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2181 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2182 )),
2183 };
2184
2185 cipher
2186 .populate_cipher_types()
2187 .expect("populate_cipher_types failed");
2188
2189 assert!(cipher.ssh_key.is_some());
2190 let ssh_key = cipher.ssh_key.unwrap();
2191 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2192 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2193 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2194 }
2195
2196 #[test]
2197 fn test_populate_cipher_types_with_null_data() {
2198 let mut cipher = Cipher {
2199 id: Some(TEST_UUID.parse().unwrap()),
2200 organization_id: None,
2201 folder_id: None,
2202 collection_ids: vec![],
2203 key: None,
2204 name: TEST_CIPHER_NAME.parse().unwrap(),
2205 notes: None,
2206 r#type: CipherType::Login,
2207 login: None,
2208 identity: None,
2209 card: None,
2210 secure_note: None,
2211 ssh_key: None,
2212 favorite: false,
2213 reprompt: CipherRepromptType::None,
2214 organization_use_totp: false,
2215 edit: true,
2216 view_password: true,
2217 permissions: None,
2218 local_data: None,
2219 attachments: None,
2220 fields: None,
2221 password_history: None,
2222 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2223 deleted_date: None,
2224 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2225 archived_date: None,
2226 data: None,
2227 };
2228
2229 let result = cipher.populate_cipher_types();
2230 assert!(matches!(
2231 result,
2232 Err(VaultParseError::MissingField(MissingFieldError("data")))
2233 ));
2234 }
2235
2236 #[test]
2237 fn test_populate_cipher_types_with_invalid_json() {
2238 let mut cipher = Cipher {
2239 id: Some(TEST_UUID.parse().unwrap()),
2240 organization_id: None,
2241 folder_id: None,
2242 collection_ids: vec![],
2243 key: None,
2244 name: TEST_CIPHER_NAME.parse().unwrap(),
2245 notes: None,
2246 r#type: CipherType::Login,
2247 login: None,
2248 identity: None,
2249 card: None,
2250 secure_note: None,
2251 ssh_key: None,
2252 favorite: false,
2253 reprompt: CipherRepromptType::None,
2254 organization_use_totp: false,
2255 edit: true,
2256 view_password: true,
2257 permissions: None,
2258 local_data: None,
2259 attachments: None,
2260 fields: None,
2261 password_history: None,
2262 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2263 deleted_date: None,
2264 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2265 archived_date: None,
2266 data: Some("invalid json".to_string()),
2267 };
2268
2269 let result = cipher.populate_cipher_types();
2270
2271 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2272 }
2273
2274 #[test]
2275 fn test_decrypt_cipher_with_mixed_attachments() {
2276 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2277 let key_store = create_test_crypto_with_user_key(user_key);
2278
2279 let mut ctx = key_store.context();
2281 let valid1 = "valid_file_1.txt"
2282 .encrypt(&mut ctx, SymmetricKeyId::User)
2283 .unwrap();
2284 let valid2 = "valid_file_2.txt"
2285 .encrypt(&mut ctx, SymmetricKeyId::User)
2286 .unwrap();
2287
2288 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
2290 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
2291 let mut wrong_ctx = wrong_key_store.context();
2292 let corrupted = "corrupted_file.txt"
2293 .encrypt(&mut wrong_ctx, SymmetricKeyId::User)
2294 .unwrap();
2295
2296 let cipher = Cipher {
2297 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2298 organization_id: None,
2299 folder_id: None,
2300 collection_ids: vec![],
2301 key: None,
2302 name: TEST_CIPHER_NAME.parse().unwrap(),
2303 notes: None,
2304 r#type: CipherType::Login,
2305 login: None,
2306 identity: None,
2307 card: None,
2308 secure_note: None,
2309 ssh_key: None,
2310 favorite: false,
2311 reprompt: CipherRepromptType::None,
2312 organization_use_totp: false,
2313 edit: true,
2314 permissions: None,
2315 view_password: true,
2316 local_data: None,
2317 attachments: Some(vec![
2318 attachment::Attachment {
2320 id: Some("valid-attachment".to_string()),
2321 url: Some("https://example.com/valid".to_string()),
2322 size: Some("100".to_string()),
2323 size_name: Some("100 Bytes".to_string()),
2324 file_name: Some(valid1),
2325 key: None,
2326 },
2327 attachment::Attachment {
2329 id: Some("corrupted-attachment".to_string()),
2330 url: Some("https://example.com/corrupted".to_string()),
2331 size: Some("200".to_string()),
2332 size_name: Some("200 Bytes".to_string()),
2333 file_name: Some(corrupted),
2334 key: None,
2335 },
2336 attachment::Attachment {
2338 id: Some("valid-attachment-2".to_string()),
2339 url: Some("https://example.com/valid2".to_string()),
2340 size: Some("150".to_string()),
2341 size_name: Some("150 Bytes".to_string()),
2342 file_name: Some(valid2),
2343 key: None,
2344 },
2345 ]),
2346 fields: None,
2347 password_history: None,
2348 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2349 deleted_date: None,
2350 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2351 archived_date: None,
2352 data: None,
2353 };
2354
2355 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2356
2357 assert!(view.attachments.is_some());
2359 let successes = view.attachments.as_ref().unwrap();
2360 assert_eq!(successes.len(), 2);
2361 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
2362 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
2363
2364 assert!(view.attachment_decryption_failures.is_some());
2366 let failures = view.attachment_decryption_failures.as_ref().unwrap();
2367 assert_eq!(failures.len(), 1);
2368 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
2369 assert_eq!(failures[0].file_name, None);
2370 }
2371}