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 #[cfg(feature = "wasm")]
434 pub notes: Option<String>,
435 #[cfg(feature = "wasm")]
438 pub fields: Option<Vec<field::FieldListView>>,
439 #[cfg(feature = "wasm")]
441 pub attachment_names: Option<Vec<String>>,
442}
443
444#[derive(Serialize, Deserialize, Debug)]
450#[serde(rename_all = "camelCase", deny_unknown_fields)]
451#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
452#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
453pub struct DecryptCipherListResult {
454 pub successes: Vec<CipherListView>,
456 pub failures: Vec<Cipher>,
458}
459
460#[derive(Serialize, Deserialize, Debug)]
466#[serde(rename_all = "camelCase", deny_unknown_fields)]
467#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
468pub struct DecryptCipherResult {
469 pub successes: Vec<CipherView>,
471 pub failures: Vec<Cipher>,
473}
474
475impl CipherListView {
476 pub(crate) fn get_totp_key(
477 self,
478 ctx: &mut KeyStoreContext<KeyIds>,
479 ) -> Result<Option<String>, CryptoError> {
480 let key = self.key_identifier();
481 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
482
483 let totp = match self.r#type {
484 CipherListViewType::Login(LoginListView { totp, .. }) => {
485 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
486 }
487 _ => None,
488 };
489
490 Ok(totp)
491 }
492}
493
494impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
495 fn encrypt_composite(
496 &self,
497 ctx: &mut KeyStoreContext<KeyIds>,
498 key: SymmetricKeyId,
499 ) -> Result<Cipher, CryptoError> {
500 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
501
502 let mut cipher_view = self.clone();
503 cipher_view.generate_checksums();
504
505 Ok(Cipher {
506 id: cipher_view.id,
507 organization_id: cipher_view.organization_id,
508 folder_id: cipher_view.folder_id,
509 collection_ids: cipher_view.collection_ids,
510 key: cipher_view.key,
511 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
512 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
513 r#type: cipher_view.r#type,
514 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
515 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
516 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
517 secure_note: cipher_view
518 .secure_note
519 .encrypt_composite(ctx, ciphers_key)?,
520 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
521 favorite: cipher_view.favorite,
522 reprompt: cipher_view.reprompt,
523 organization_use_totp: cipher_view.organization_use_totp,
524 edit: cipher_view.edit,
525 view_password: cipher_view.view_password,
526 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
527 attachments: cipher_view
528 .attachments
529 .encrypt_composite(ctx, ciphers_key)?,
530 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
531 password_history: cipher_view
532 .password_history
533 .encrypt_composite(ctx, ciphers_key)?,
534 creation_date: cipher_view.creation_date,
535 deleted_date: cipher_view.deleted_date,
536 revision_date: cipher_view.revision_date,
537 permissions: cipher_view.permissions,
538 archived_date: cipher_view.archived_date,
539 data: None, })
541 }
542}
543
544impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
545 #[instrument(err, skip_all, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
546 fn decrypt(
547 &self,
548 ctx: &mut KeyStoreContext<KeyIds>,
549 key: SymmetricKeyId,
550 ) -> Result<CipherView, CryptoError> {
551 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
552
553 let (attachments, attachment_decryption_failures) =
555 attachment::decrypt_attachments_with_failures(
556 self.attachments.as_deref().unwrap_or_default(),
557 ctx,
558 ciphers_key,
559 );
560
561 let mut cipher = CipherView {
562 id: self.id,
563 organization_id: self.organization_id,
564 folder_id: self.folder_id,
565 collection_ids: self.collection_ids.clone(),
566 key: self.key.clone(),
567 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
568 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
569 r#type: self.r#type,
570 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
571 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
572 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
573 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
574 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
575 favorite: self.favorite,
576 reprompt: self.reprompt,
577 organization_use_totp: self.organization_use_totp,
578 edit: self.edit,
579 permissions: self.permissions,
580 view_password: self.view_password,
581 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
582 attachments: Some(attachments),
583 attachment_decryption_failures: Some(attachment_decryption_failures),
584 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
585 password_history: self
586 .password_history
587 .decrypt(ctx, ciphers_key)
588 .ok()
589 .flatten(),
590 creation_date: self.creation_date,
591 deleted_date: self.deleted_date,
592 revision_date: self.revision_date,
593 archived_date: self.archived_date,
594 };
595
596 if cipher.key.is_some()
599 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
600 {
601 cipher.remove_invalid_checksums();
602 }
603
604 Ok(cipher)
605 }
606}
607
608impl Cipher {
609 #[instrument(err, skip_all)]
620 pub(super) fn decrypt_cipher_key(
621 ctx: &mut KeyStoreContext<KeyIds>,
622 key: SymmetricKeyId,
623 ciphers_key: &Option<EncString>,
624 ) -> Result<SymmetricKeyId, CryptoError> {
625 match ciphers_key {
626 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
627 None => Ok(key),
628 }
629 }
630
631 fn get_kind(&self) -> Option<&dyn CipherKind> {
633 match self.r#type {
634 CipherType::Login => self.login.as_ref().map(|v| v as _),
635 CipherType::Card => self.card.as_ref().map(|v| v as _),
636 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
637 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
638 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
639 }
640 }
641
642 fn decrypt_subtitle(
644 &self,
645 ctx: &mut KeyStoreContext<KeyIds>,
646 key: SymmetricKeyId,
647 ) -> Result<String, CryptoError> {
648 self.get_kind()
649 .map(|sub| sub.decrypt_subtitle(ctx, key))
650 .unwrap_or_else(|| Ok(String::new()))
651 }
652
653 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
656 self.get_kind()
657 .map(|kind| kind.get_copyable_fields(Some(self)))
658 .unwrap_or_default()
659 }
660
661 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
666 let data = self
667 .data
668 .as_ref()
669 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
670
671 match &self.r#type {
672 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
673 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
674 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
675 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
676 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
677 }
678 Ok(())
679 }
680
681 pub(crate) fn soft_delete(&mut self) {
683 self.deleted_date = Some(Utc::now());
684 }
685}
686impl CipherView {
687 #[allow(missing_docs)]
688 pub fn generate_cipher_key(
689 &mut self,
690 ctx: &mut KeyStoreContext<KeyIds>,
691 key: SymmetricKeyId,
692 ) -> Result<(), CryptoError> {
693 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
694
695 let new_key = ctx.generate_symmetric_key();
696
697 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
698 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
699
700 self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
701 Ok(())
702 }
703
704 #[allow(missing_docs)]
705 pub fn generate_checksums(&mut self) {
706 if let Some(l) = self.login.as_mut() {
707 l.generate_checksums();
708 }
709 }
710
711 #[allow(missing_docs)]
712 pub fn remove_invalid_checksums(&mut self) {
713 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
714 uris.retain(|u| u.is_checksum_valid());
715 }
716 }
717
718 fn reencrypt_attachment_keys(
719 &mut self,
720 ctx: &mut KeyStoreContext<KeyIds>,
721 old_key: SymmetricKeyId,
722 new_key: SymmetricKeyId,
723 ) -> Result<(), CryptoError> {
724 if let Some(attachments) = &mut self.attachments {
725 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
726 }
727 Ok(())
728 }
729
730 #[allow(missing_docs)]
731 pub fn decrypt_fido2_credentials(
732 &self,
733 ctx: &mut KeyStoreContext<KeyIds>,
734 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
735 let key = self.key_identifier();
736 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
737
738 Ok(self
739 .login
740 .as_ref()
741 .and_then(|l| l.fido2_credentials.as_ref())
742 .map(|f| f.decrypt(ctx, ciphers_key))
743 .transpose()?
744 .unwrap_or_default())
745 }
746
747 fn reencrypt_fido2_credentials(
748 &mut self,
749 ctx: &mut KeyStoreContext<KeyIds>,
750 old_key: SymmetricKeyId,
751 new_key: SymmetricKeyId,
752 ) -> Result<(), CryptoError> {
753 if let Some(login) = self.login.as_mut() {
754 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
755 }
756 Ok(())
757 }
758
759 pub fn move_to_organization(
766 &mut self,
767 ctx: &mut KeyStoreContext<KeyIds>,
768 organization_id: OrganizationId,
769 ) -> Result<(), CipherError> {
770 let new_key = SymmetricKeyId::Organization(organization_id);
771
772 self.reencrypt_cipher_keys(ctx, new_key)?;
773 self.organization_id = Some(organization_id);
774
775 Ok(())
776 }
777
778 pub fn reencrypt_cipher_keys(
783 &mut self,
784 ctx: &mut KeyStoreContext<KeyIds>,
785 new_wrapping_key: SymmetricKeyId,
786 ) -> Result<(), CipherError> {
787 let old_key = self.key_identifier();
788
789 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
791 return Err(CipherError::AttachmentsWithoutKeys);
792 }
793
794 if self.key.is_some() {
796 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
798
799 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
801 } else {
802 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
805 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
806 }
807
808 Ok(())
809 }
810
811 #[allow(missing_docs)]
812 pub fn set_new_fido2_credentials(
813 &mut self,
814 ctx: &mut KeyStoreContext<KeyIds>,
815 creds: Vec<Fido2CredentialFullView>,
816 ) -> Result<(), CipherError> {
817 let key = self.key_identifier();
818
819 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
820
821 require!(self.login.as_mut()).fido2_credentials =
822 Some(creds.encrypt_composite(ctx, ciphers_key)?);
823
824 Ok(())
825 }
826
827 #[allow(missing_docs)]
828 pub fn get_fido2_credentials(
829 &self,
830 ctx: &mut KeyStoreContext<KeyIds>,
831 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
832 let key = self.key_identifier();
833
834 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
835
836 let login = require!(self.login.as_ref());
837 let creds = require!(login.fido2_credentials.as_ref());
838 let res = creds.decrypt(ctx, ciphers_key)?;
839 Ok(res)
840 }
841
842 #[allow(missing_docs)]
843 pub fn decrypt_fido2_private_key(
844 &self,
845 ctx: &mut KeyStoreContext<KeyIds>,
846 ) -> Result<String, CipherError> {
847 let fido2_credential = self.get_fido2_credentials(ctx)?;
848
849 Ok(fido2_credential[0].key_value.clone())
850 }
851
852 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
853 let changes = self
854 .login
855 .as_mut()
856 .map_or(vec![], |login| {
857 login.detect_password_change(&original_cipher.login)
858 })
859 .into_iter()
860 .chain(self.fields.as_deref().map_or(vec![], |fields| {
861 FieldView::detect_hidden_field_changes(
862 fields,
863 original_cipher.fields.as_deref().unwrap_or(&[]),
864 )
865 }))
866 .rev()
867 .chain(original_cipher.password_history.iter().flatten().cloned())
868 .take(MAX_PASSWORD_HISTORY_ENTRIES)
869 .collect();
870 self.password_history = Some(changes)
871 }
872}
873
874impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
875 fn decrypt(
876 &self,
877 ctx: &mut KeyStoreContext<KeyIds>,
878 key: SymmetricKeyId,
879 ) -> Result<CipherListView, CryptoError> {
880 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
881
882 Ok(CipherListView {
883 id: self.id,
884 organization_id: self.organization_id,
885 folder_id: self.folder_id,
886 collection_ids: self.collection_ids.clone(),
887 key: self.key.clone(),
888 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
889 subtitle: self
890 .decrypt_subtitle(ctx, ciphers_key)
891 .ok()
892 .unwrap_or_default(),
893 r#type: match self.r#type {
894 CipherType::Login => {
895 let login = self
896 .login
897 .as_ref()
898 .ok_or(CryptoError::MissingField("login"))?;
899 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
900 }
901 CipherType::SecureNote => CipherListViewType::SecureNote,
902 CipherType::Card => {
903 let card = self
904 .card
905 .as_ref()
906 .ok_or(CryptoError::MissingField("card"))?;
907 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
908 }
909 CipherType::Identity => CipherListViewType::Identity,
910 CipherType::SshKey => CipherListViewType::SshKey,
911 },
912 favorite: self.favorite,
913 reprompt: self.reprompt,
914 organization_use_totp: self.organization_use_totp,
915 edit: self.edit,
916 permissions: self.permissions,
917 view_password: self.view_password,
918 attachments: self
919 .attachments
920 .as_ref()
921 .map(|a| a.len() as u32)
922 .unwrap_or(0),
923 has_old_attachments: self
924 .attachments
925 .as_ref()
926 .map(|a| a.iter().any(|att| att.key.is_none()))
927 .unwrap_or(false),
928 creation_date: self.creation_date,
929 deleted_date: self.deleted_date,
930 revision_date: self.revision_date,
931 copyable_fields: self.get_copyable_fields(),
932 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
933 archived_date: self.archived_date,
934 #[cfg(feature = "wasm")]
935 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
936 #[cfg(feature = "wasm")]
937 fields: self.fields.as_ref().map(|fields| {
938 fields
939 .iter()
940 .filter_map(|f| {
941 f.decrypt(ctx, ciphers_key)
942 .ok()
943 .map(field::FieldListView::from)
944 })
945 .collect()
946 }),
947 #[cfg(feature = "wasm")]
948 attachment_names: self.attachments.as_ref().map(|attachments| {
949 attachments
950 .iter()
951 .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
952 .collect()
953 }),
954 })
955 }
956}
957
958impl IdentifyKey<SymmetricKeyId> for Cipher {
959 fn key_identifier(&self) -> SymmetricKeyId {
960 match self.organization_id {
961 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
962 None => SymmetricKeyId::User,
963 }
964 }
965}
966
967impl IdentifyKey<SymmetricKeyId> for CipherView {
968 fn key_identifier(&self) -> SymmetricKeyId {
969 match self.organization_id {
970 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
971 None => SymmetricKeyId::User,
972 }
973 }
974}
975
976impl IdentifyKey<SymmetricKeyId> for CipherListView {
977 fn key_identifier(&self) -> SymmetricKeyId {
978 match self.organization_id {
979 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
980 None => SymmetricKeyId::User,
981 }
982 }
983}
984
985impl TryFrom<CipherDetailsResponseModel> for Cipher {
986 type Error = VaultParseError;
987
988 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
989 Ok(Self {
990 id: cipher.id.map(CipherId::new),
991 organization_id: cipher.organization_id.map(OrganizationId::new),
992 folder_id: cipher.folder_id.map(FolderId::new),
993 collection_ids: cipher
994 .collection_ids
995 .unwrap_or_default()
996 .into_iter()
997 .map(CollectionId::new)
998 .collect(),
999 name: require!(EncString::try_from_optional(cipher.name)?),
1000 notes: EncString::try_from_optional(cipher.notes)?,
1001 r#type: require!(cipher.r#type).try_into()?,
1002 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1003 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1004 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1005 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1006 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1007 favorite: cipher.favorite.unwrap_or(false),
1008 reprompt: cipher
1009 .reprompt
1010 .map(|r| r.try_into())
1011 .transpose()?
1012 .unwrap_or(CipherRepromptType::None),
1013 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
1014 edit: cipher.edit.unwrap_or(true),
1015 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1016 view_password: cipher.view_password.unwrap_or(true),
1017 local_data: None, attachments: cipher
1019 .attachments
1020 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1021 .transpose()?,
1022 fields: cipher
1023 .fields
1024 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1025 .transpose()?,
1026 password_history: cipher
1027 .password_history
1028 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1029 .transpose()?,
1030 creation_date: require!(cipher.creation_date).parse()?,
1031 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1032 revision_date: require!(cipher.revision_date).parse()?,
1033 key: EncString::try_from_optional(cipher.key)?,
1034 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1035 data: cipher.data,
1036 })
1037 }
1038}
1039
1040impl PartialCipher for CipherDetailsResponseModel {
1041 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1042 Ok(Cipher {
1043 local_data: cipher.and_then(|c| c.local_data),
1044 ..self.try_into()?
1045 })
1046 }
1047}
1048
1049impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
1050 type Error = MissingFieldError;
1051
1052 fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
1053 Ok(match t {
1054 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1055 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1056 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1057 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1058 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1059 bitwarden_api_api::models::CipherType::__Unknown(_) => {
1060 return Err(MissingFieldError("type"));
1061 }
1062 })
1063 }
1064}
1065
1066impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1067 type Error = MissingFieldError;
1068
1069 fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
1070 Ok(match t {
1071 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1072 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1073 bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
1074 return Err(MissingFieldError("reprompt"));
1075 }
1076 })
1077 }
1078}
1079
1080pub(crate) trait PartialCipher {
1084 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1085}
1086
1087impl From<CipherType> for bitwarden_api_api::models::CipherType {
1088 fn from(t: CipherType) -> Self {
1089 match t {
1090 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1091 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1092 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1093 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1094 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1095 }
1096 }
1097}
1098
1099impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1100 fn from(t: CipherRepromptType) -> Self {
1101 match t {
1102 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1103 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1104 }
1105 }
1106}
1107
1108impl TryFrom<CipherResponseModel> for Cipher {
1109 type Error = VaultParseError;
1110
1111 fn try_from(cipher: CipherResponseModel) -> Result<Self, Self::Error> {
1112 Ok(Self {
1113 id: cipher.id.map(CipherId::new),
1114 organization_id: cipher.organization_id.map(OrganizationId::new),
1115 folder_id: cipher.folder_id.map(FolderId::new),
1116 collection_ids: vec![], name: require!(cipher.name).parse()?,
1118 notes: EncString::try_from_optional(cipher.notes)?,
1119 r#type: require!(cipher.r#type).try_into()?,
1120 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1121 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1122 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1123 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1124 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1125 favorite: cipher.favorite.unwrap_or(false),
1126 reprompt: cipher
1127 .reprompt
1128 .map(|r| r.try_into())
1129 .transpose()?
1130 .unwrap_or(CipherRepromptType::None),
1131 organization_use_totp: cipher.organization_use_totp.unwrap_or(false),
1132 edit: cipher.edit.unwrap_or(false),
1133 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1134 view_password: cipher.view_password.unwrap_or(true),
1135 local_data: None, attachments: cipher
1137 .attachments
1138 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1139 .transpose()?,
1140 fields: cipher
1141 .fields
1142 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1143 .transpose()?,
1144 password_history: cipher
1145 .password_history
1146 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1147 .transpose()?,
1148 creation_date: require!(cipher.creation_date).parse()?,
1149 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1150 revision_date: require!(cipher.revision_date).parse()?,
1151 key: EncString::try_from_optional(cipher.key)?,
1152 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1153 data: cipher.data,
1154 })
1155 }
1156}
1157
1158impl PartialCipher for CipherMiniResponseModel {
1159 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1160 let cipher = cipher.as_ref();
1161 Ok(Cipher {
1162 id: self.id.map(CipherId::new),
1163 organization_id: self.organization_id.map(OrganizationId::new),
1164 key: EncString::try_from_optional(self.key)?,
1165 name: require!(EncString::try_from_optional(self.name)?),
1166 notes: EncString::try_from_optional(self.notes)?,
1167 r#type: require!(self.r#type).try_into()?,
1168 login: self.login.map(|l| (*l).try_into()).transpose()?,
1169 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1170 card: self.card.map(|c| (*c).try_into()).transpose()?,
1171 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1172 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1173 reprompt: self
1174 .reprompt
1175 .map(|r| r.try_into())
1176 .transpose()?
1177 .unwrap_or(CipherRepromptType::None),
1178 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1179 attachments: self
1180 .attachments
1181 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1182 .transpose()?,
1183 fields: self
1184 .fields
1185 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1186 .transpose()?,
1187 password_history: self
1188 .password_history
1189 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1190 .transpose()?,
1191 creation_date: require!(self.creation_date)
1192 .parse()
1193 .map_err(Into::<VaultParseError>::into)?,
1194 deleted_date: self
1195 .deleted_date
1196 .map(|d| d.parse())
1197 .transpose()
1198 .map_err(Into::<VaultParseError>::into)?,
1199 revision_date: require!(self.revision_date)
1200 .parse()
1201 .map_err(Into::<VaultParseError>::into)?,
1202 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1203 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1204 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1205 edit: cipher.map_or(Default::default(), |c| c.edit),
1206 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1207 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1208 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1209 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1210 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1211 })
1212 }
1213}
1214
1215impl PartialCipher for CipherMiniDetailsResponseModel {
1216 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1217 let cipher = cipher.as_ref();
1218 Ok(Cipher {
1219 id: self.id.map(CipherId::new),
1220 organization_id: self.organization_id.map(OrganizationId::new),
1221 key: EncString::try_from_optional(self.key)?,
1222 name: require!(EncString::try_from_optional(self.name)?),
1223 notes: EncString::try_from_optional(self.notes)?,
1224 r#type: require!(self.r#type).try_into()?,
1225 login: self.login.map(|l| (*l).try_into()).transpose()?,
1226 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1227 card: self.card.map(|c| (*c).try_into()).transpose()?,
1228 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1229 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1230 reprompt: self
1231 .reprompt
1232 .map(|r| r.try_into())
1233 .transpose()?
1234 .unwrap_or(CipherRepromptType::None),
1235 organization_use_totp: self.organization_use_totp.unwrap_or(true),
1236 attachments: self
1237 .attachments
1238 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1239 .transpose()?,
1240 fields: self
1241 .fields
1242 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1243 .transpose()?,
1244 password_history: self
1245 .password_history
1246 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1247 .transpose()?,
1248 creation_date: require!(self.creation_date)
1249 .parse()
1250 .map_err(Into::<VaultParseError>::into)?,
1251 deleted_date: self
1252 .deleted_date
1253 .map(|d| d.parse())
1254 .transpose()
1255 .map_err(Into::<VaultParseError>::into)?,
1256 revision_date: require!(self.revision_date)
1257 .parse()
1258 .map_err(Into::<VaultParseError>::into)?,
1259 collection_ids: self
1260 .collection_ids
1261 .into_iter()
1262 .flatten()
1263 .map(CollectionId::new)
1264 .collect(),
1265 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1266 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1267 favorite: cipher.map_or(Default::default(), |c| c.favorite),
1268 edit: cipher.map_or(Default::default(), |c| c.edit),
1269 permissions: cipher.map_or(Default::default(), |c| c.permissions),
1270 view_password: cipher.map_or(Default::default(), |c| c.view_password),
1271 data: cipher.map_or(Default::default(), |c| c.data.clone()),
1272 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1273 })
1274 }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279
1280 use attachment::AttachmentView;
1281 use bitwarden_core::key_management::{
1282 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1283 };
1284 use bitwarden_crypto::SymmetricCryptoKey;
1285
1286 use super::*;
1287 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1288
1289 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1291 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1292 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1293 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1294 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1295 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1296 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1297
1298 fn generate_cipher() -> CipherView {
1299 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1300 CipherView {
1301 r#type: CipherType::Login,
1302 login: Some(LoginView {
1303 username: Some("test_username".to_string()),
1304 password: Some("test_password".to_string()),
1305 password_revision_date: None,
1306 uris: None,
1307 totp: None,
1308 autofill_on_page_load: None,
1309 fido2_credentials: None,
1310 }),
1311 id: Some(test_id),
1312 organization_id: None,
1313 folder_id: None,
1314 collection_ids: vec![],
1315 key: None,
1316 name: "My test login".to_string(),
1317 notes: None,
1318 identity: None,
1319 card: None,
1320 secure_note: None,
1321 ssh_key: None,
1322 favorite: false,
1323 reprompt: CipherRepromptType::None,
1324 organization_use_totp: true,
1325 edit: true,
1326 permissions: None,
1327 view_password: true,
1328 local_data: None,
1329 attachments: None,
1330 attachment_decryption_failures: None,
1331 fields: None,
1332 password_history: None,
1333 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1334 deleted_date: None,
1335 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1336 archived_date: None,
1337 }
1338 }
1339
1340 fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
1341 Fido2Credential {
1342 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1343 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1344 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1345 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1346 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1347 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1348 user_handle: None,
1349 user_name: None,
1350 counter: "123".to_string().encrypt(ctx, key).unwrap(),
1351 rp_name: None,
1352 user_display_name: None,
1353 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1354 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1355 }
1356 }
1357
1358 #[test]
1359 fn test_decrypt_cipher_list_view() {
1360 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1361 let key_store = create_test_crypto_with_user_key(key);
1362
1363 let cipher = Cipher {
1364 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1365 organization_id: None,
1366 folder_id: None,
1367 collection_ids: vec![],
1368 key: None,
1369 name: TEST_CIPHER_NAME.parse().unwrap(),
1370 notes: None,
1371 r#type: CipherType::Login,
1372 login: Some(Login {
1373 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1374 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1375 password_revision_date: None,
1376 uris: None,
1377 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1378 autofill_on_page_load: None,
1379 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
1380 }),
1381 identity: None,
1382 card: None,
1383 secure_note: None,
1384 ssh_key: None,
1385 favorite: false,
1386 reprompt: CipherRepromptType::None,
1387 organization_use_totp: false,
1388 edit: true,
1389 permissions: Some(CipherPermissions {
1390 delete: false,
1391 restore: false
1392 }),
1393 view_password: true,
1394 local_data: None,
1395 attachments: None,
1396 fields: None,
1397 password_history: None,
1398 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1399 deleted_date: None,
1400 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1401 archived_date: None,
1402 data: None,
1403 };
1404
1405 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1406
1407 assert_eq!(
1408 view,
1409 CipherListView {
1410 id: cipher.id,
1411 organization_id: cipher.organization_id,
1412 folder_id: cipher.folder_id,
1413 collection_ids: cipher.collection_ids,
1414 key: cipher.key,
1415 name: "My test login".to_string(),
1416 subtitle: "test_username".to_string(),
1417 r#type: CipherListViewType::Login(LoginListView {
1418 fido2_credentials: Some(vec![Fido2CredentialListView {
1419 credential_id: "123".to_string(),
1420 rp_id: "123".to_string(),
1421 user_handle: None,
1422 user_name: None,
1423 user_display_name: None,
1424 counter: "123".to_string(),
1425 }]),
1426 has_fido2: true,
1427 username: Some("test_username".to_string()),
1428 totp: cipher.login.as_ref().unwrap().totp.clone(),
1429 uris: None,
1430 }),
1431 favorite: cipher.favorite,
1432 reprompt: cipher.reprompt,
1433 organization_use_totp: cipher.organization_use_totp,
1434 edit: cipher.edit,
1435 permissions: cipher.permissions,
1436 view_password: cipher.view_password,
1437 attachments: 0,
1438 has_old_attachments: false,
1439 creation_date: cipher.creation_date,
1440 deleted_date: cipher.deleted_date,
1441 revision_date: cipher.revision_date,
1442 copyable_fields: vec![
1443 CopyableCipherFields::LoginUsername,
1444 CopyableCipherFields::LoginPassword,
1445 CopyableCipherFields::LoginTotp
1446 ],
1447 local_data: None,
1448 archived_date: cipher.archived_date,
1449 #[cfg(feature = "wasm")]
1450 notes: None,
1451 #[cfg(feature = "wasm")]
1452 fields: None,
1453 #[cfg(feature = "wasm")]
1454 attachment_names: None,
1455 }
1456 )
1457 }
1458
1459 #[test]
1460 fn test_generate_cipher_key() {
1461 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1462 let key_store = create_test_crypto_with_user_key(key);
1463
1464 let original_cipher = generate_cipher();
1465
1466 let cipher = generate_cipher();
1468 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1469 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1470 assert!(no_key_cipher_dec.key.is_none());
1471 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1472
1473 let mut cipher = generate_cipher();
1474 cipher
1475 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1476 .unwrap();
1477
1478 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1480 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1481 assert!(key_cipher_dec.key.is_some());
1482 assert_eq!(key_cipher_dec.name, original_cipher.name);
1483 }
1484
1485 #[test]
1486 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1487 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1488 let key_store = create_test_crypto_with_user_key(key);
1489
1490 let mut original_cipher = generate_cipher();
1491 {
1492 let mut ctx = key_store.context();
1493 let cipher_key = ctx.generate_symmetric_key();
1494
1495 original_cipher.key = Some(
1496 ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1497 .unwrap(),
1498 );
1499 }
1500
1501 original_cipher
1502 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1503 .unwrap();
1504
1505 let wrapped_key = original_cipher.key.unwrap();
1507 let mut ctx = key_store.context();
1508 ctx.unwrap_symmetric_key(SymmetricKeyId::User, &wrapped_key)
1509 .unwrap();
1510 }
1511
1512 #[test]
1513 fn test_generate_cipher_key_ignores_attachments_without_key() {
1514 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1515 let key_store = create_test_crypto_with_user_key(key);
1516
1517 let mut cipher = generate_cipher();
1518 let attachment = AttachmentView {
1519 id: None,
1520 url: None,
1521 size: None,
1522 size_name: None,
1523 file_name: Some("Attachment test name".into()),
1524 key: None,
1525 #[cfg(feature = "wasm")]
1526 decrypted_key: None,
1527 };
1528 cipher.attachments = Some(vec![attachment]);
1529
1530 cipher
1531 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1532 .unwrap();
1533 assert!(cipher.attachments.unwrap()[0].key.is_none());
1534 }
1535
1536 #[test]
1537 fn test_reencrypt_cipher_key() {
1538 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1539 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1540 let key_store = create_test_crypto_with_user_key(old_key);
1541 let mut ctx = key_store.context_mut();
1542
1543 let mut cipher = generate_cipher();
1544 cipher
1545 .generate_cipher_key(&mut ctx, cipher.key_identifier())
1546 .unwrap();
1547
1548 let new_key_id = ctx.add_local_symmetric_key(new_key);
1550
1551 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
1552
1553 assert!(cipher.key.is_some());
1555 assert!(
1556 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
1557 .is_ok()
1558 );
1559 }
1560
1561 #[test]
1562 fn test_reencrypt_cipher_key_ignores_missing_key() {
1563 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1564 let key_store = create_test_crypto_with_user_key(key);
1565 let mut ctx = key_store.context_mut();
1566 let mut cipher = generate_cipher();
1567
1568 let new_cipher_key = ctx.generate_symmetric_key();
1570 cipher
1571 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
1572 .unwrap();
1573
1574 assert!(cipher.key.is_none());
1576 }
1577
1578 #[test]
1579 fn test_move_user_cipher_to_org() {
1580 let org = OrganizationId::new_v4();
1581 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1582 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1583 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1584
1585 let mut cipher = generate_cipher();
1587 cipher
1588 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1589 .unwrap();
1590
1591 cipher
1592 .move_to_organization(&mut key_store.context(), org)
1593 .unwrap();
1594 assert_eq!(cipher.organization_id, Some(org));
1595
1596 let cipher_enc = key_store.encrypt(cipher).unwrap();
1598 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1599
1600 assert_eq!(cipher_dec.name, "My test login");
1601 }
1602
1603 #[test]
1604 fn test_move_user_cipher_to_org_manually() {
1605 let org = OrganizationId::new_v4();
1606 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1607 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1608 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1609
1610 let mut cipher = generate_cipher();
1612 cipher
1613 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1614 .unwrap();
1615
1616 cipher.organization_id = Some(org);
1617
1618 assert!(key_store.encrypt(cipher).is_err());
1621 }
1622
1623 #[test]
1624 fn test_move_user_cipher_with_attachment_without_key_to_org() {
1625 let org = OrganizationId::new_v4();
1626 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1627 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1628 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1629
1630 let mut cipher = generate_cipher();
1631 let attachment = AttachmentView {
1632 id: None,
1633 url: None,
1634 size: None,
1635 size_name: None,
1636 file_name: Some("Attachment test name".into()),
1637 key: None,
1638 #[cfg(feature = "wasm")]
1639 decrypted_key: None,
1640 };
1641 cipher.attachments = Some(vec![attachment]);
1642
1643 assert!(
1645 cipher
1646 .move_to_organization(&mut key_store.context(), org)
1647 .is_err()
1648 );
1649 }
1650
1651 #[test]
1652 fn test_move_user_cipher_with_attachment_with_key_to_org() {
1653 let org = OrganizationId::new_v4();
1654 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1655 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1656 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1657 let org_key = SymmetricKeyId::Organization(org);
1658
1659 let (attachment_key_enc, attachment_key_val) = {
1661 let mut ctx = key_store.context();
1662 let attachment_key = ctx.generate_symmetric_key();
1663 let attachment_key_enc = ctx
1664 .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1665 .unwrap();
1666 #[allow(deprecated)]
1667 let attachment_key_val = ctx
1668 .dangerous_get_symmetric_key(attachment_key)
1669 .unwrap()
1670 .clone();
1671
1672 (attachment_key_enc, attachment_key_val)
1673 };
1674
1675 let mut cipher = generate_cipher();
1676 let attachment = AttachmentView {
1677 id: None,
1678 url: None,
1679 size: None,
1680 size_name: None,
1681 file_name: Some("Attachment test name".into()),
1682 key: Some(attachment_key_enc),
1683 #[cfg(feature = "wasm")]
1684 decrypted_key: None,
1685 };
1686 cipher.attachments = Some(vec![attachment]);
1687 let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1688 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1689
1690 cipher
1691 .move_to_organization(&mut key_store.context(), org)
1692 .unwrap();
1693
1694 assert!(cipher.key.is_none());
1695
1696 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1699 let mut ctx = key_store.context();
1700 let new_attachment_key_id = ctx
1701 .unwrap_symmetric_key(org_key, &new_attachment_key)
1702 .unwrap();
1703 #[allow(deprecated)]
1704 let new_attachment_key_dec = ctx
1705 .dangerous_get_symmetric_key(new_attachment_key_id)
1706 .unwrap();
1707
1708 assert_eq!(*new_attachment_key_dec, attachment_key_val);
1709
1710 let cred2: Fido2CredentialFullView = cipher
1711 .login
1712 .unwrap()
1713 .fido2_credentials
1714 .unwrap()
1715 .first()
1716 .unwrap()
1717 .decrypt(&mut key_store.context(), org_key)
1718 .unwrap();
1719
1720 assert_eq!(cred2.credential_id, "123");
1721 }
1722
1723 #[test]
1724 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1725 let org = OrganizationId::new_v4();
1726 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1727 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1728 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1729 let org_key = SymmetricKeyId::Organization(org);
1730
1731 let mut ctx = key_store.context();
1732
1733 let cipher_key = ctx.generate_symmetric_key();
1734 let cipher_key_enc = ctx
1735 .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1736 .unwrap();
1737
1738 let attachment_key = ctx.generate_symmetric_key();
1740 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1741
1742 let mut cipher = generate_cipher();
1743 cipher.key = Some(cipher_key_enc);
1744
1745 let attachment = AttachmentView {
1746 id: None,
1747 url: None,
1748 size: None,
1749 size_name: None,
1750 file_name: Some("Attachment test name".into()),
1751 key: Some(attachment_key_enc.clone()),
1752 #[cfg(feature = "wasm")]
1753 decrypted_key: None,
1754 };
1755 cipher.attachments = Some(vec![attachment]);
1756
1757 let cred = generate_fido2(&mut ctx, cipher_key);
1758 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1759
1760 cipher.move_to_organization(&mut ctx, org).unwrap();
1761
1762 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1764 let new_cipher_key_dec = ctx
1765 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
1766 .unwrap();
1767 #[allow(deprecated)]
1768 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1769 #[allow(deprecated)]
1770 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1771
1772 assert_eq!(new_cipher_key_dec, cipher_key_val);
1773
1774 assert_eq!(
1776 cipher.attachments.unwrap()[0]
1777 .key
1778 .as_ref()
1779 .unwrap()
1780 .to_string(),
1781 attachment_key_enc.to_string()
1782 );
1783
1784 let cred2: Fido2Credential = cipher
1785 .login
1786 .unwrap()
1787 .fido2_credentials
1788 .unwrap()
1789 .first()
1790 .unwrap()
1791 .clone();
1792
1793 assert_eq!(
1794 cred2.credential_id.to_string(),
1795 cred.credential_id.to_string()
1796 );
1797 }
1798
1799 #[test]
1800 fn test_decrypt_fido2_private_key() {
1801 let key_store =
1802 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1803 let mut ctx = key_store.context();
1804
1805 let mut cipher_view = generate_cipher();
1806 cipher_view
1807 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1808 .unwrap();
1809
1810 let key_id = cipher_view.key_identifier();
1811 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1812
1813 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1814
1815 cipher_view.login.as_mut().unwrap().fido2_credentials =
1816 Some(vec![fido2_credential.clone()]);
1817
1818 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1819 assert_eq!(decrypted_key_value, "123");
1820 }
1821
1822 #[test]
1823 fn test_password_history_on_password_change() {
1824 use chrono::Utc;
1825
1826 let original_cipher = generate_cipher();
1827 let mut new_cipher = generate_cipher();
1828
1829 if let Some(ref mut login) = new_cipher.login {
1831 login.password = Some("new_password123".to_string());
1832 }
1833
1834 let start = Utc::now();
1835 new_cipher.update_password_history(&original_cipher);
1836 let end = Utc::now();
1837
1838 assert!(new_cipher.password_history.is_some());
1839 let history = new_cipher.password_history.unwrap();
1840 assert_eq!(history.len(), 1);
1841 assert_eq!(history[0].password, "test_password");
1842 assert!(
1843 history[0].last_used_date >= start && history[0].last_used_date <= end,
1844 "last_used_date was not set properly"
1845 );
1846 }
1847
1848 #[test]
1849 fn test_password_history_on_unchanged_password() {
1850 let original_cipher = generate_cipher();
1851 let mut new_cipher = generate_cipher();
1852
1853 new_cipher.update_password_history(&original_cipher);
1854
1855 assert!(
1857 new_cipher.password_history.is_none()
1858 || new_cipher.password_history.as_ref().unwrap().is_empty()
1859 );
1860 }
1861
1862 #[test]
1863 fn test_password_history_is_preserved() {
1864 use chrono::TimeZone;
1865
1866 let mut original_cipher = generate_cipher();
1867 original_cipher.password_history = Some(
1868 (0..4)
1869 .map(|i| PasswordHistoryView {
1870 password: format!("old_password_{}", i),
1871 last_used_date: chrono::Utc
1872 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
1873 .unwrap(),
1874 })
1875 .collect(),
1876 );
1877
1878 let mut new_cipher = generate_cipher();
1879
1880 new_cipher.update_password_history(&original_cipher);
1881
1882 assert!(new_cipher.password_history.is_some());
1883 let history = new_cipher.password_history.unwrap();
1884 assert_eq!(history.len(), 4);
1885
1886 assert_eq!(history[0].password, "old_password_0");
1887 assert_eq!(
1888 history[0].last_used_date,
1889 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
1890 );
1891 assert_eq!(history[1].password, "old_password_1");
1892 assert_eq!(
1893 history[1].last_used_date,
1894 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
1895 );
1896 assert_eq!(history[2].password, "old_password_2");
1897 assert_eq!(
1898 history[2].last_used_date,
1899 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
1900 );
1901 assert_eq!(history[3].password, "old_password_3");
1902 assert_eq!(
1903 history[3].last_used_date,
1904 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
1905 );
1906 }
1907
1908 #[test]
1909 fn test_populate_cipher_types_login_with_valid_data() {
1910 let mut cipher = Cipher {
1911 id: Some(TEST_UUID.parse().unwrap()),
1912 organization_id: None,
1913 folder_id: None,
1914 collection_ids: vec![],
1915 key: None,
1916 name: TEST_CIPHER_NAME.parse().unwrap(),
1917 notes: None,
1918 r#type: CipherType::Login,
1919 login: None,
1920 identity: None,
1921 card: None,
1922 secure_note: None,
1923 ssh_key: None,
1924 favorite: false,
1925 reprompt: CipherRepromptType::None,
1926 organization_use_totp: false,
1927 edit: true,
1928 view_password: true,
1929 permissions: None,
1930 local_data: None,
1931 attachments: None,
1932 fields: None,
1933 password_history: None,
1934 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1935 deleted_date: None,
1936 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1937 archived_date: None,
1938 data: Some(format!(
1939 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1940 TEST_ENC_STRING_1, TEST_ENC_STRING_2
1941 )),
1942 };
1943
1944 cipher
1945 .populate_cipher_types()
1946 .expect("populate_cipher_types failed");
1947
1948 assert!(cipher.login.is_some());
1949 let login = cipher.login.unwrap();
1950 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
1951 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
1952 }
1953
1954 #[test]
1955 fn test_populate_cipher_types_secure_note() {
1956 let mut cipher = Cipher {
1957 id: Some(TEST_UUID.parse().unwrap()),
1958 organization_id: None,
1959 folder_id: None,
1960 collection_ids: vec![],
1961 key: None,
1962 name: TEST_CIPHER_NAME.parse().unwrap(),
1963 notes: None,
1964 r#type: CipherType::SecureNote,
1965 login: None,
1966 identity: None,
1967 card: None,
1968 secure_note: None,
1969 ssh_key: None,
1970 favorite: false,
1971 reprompt: CipherRepromptType::None,
1972 organization_use_totp: false,
1973 edit: true,
1974 view_password: true,
1975 permissions: None,
1976 local_data: None,
1977 attachments: None,
1978 fields: None,
1979 password_history: None,
1980 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1981 deleted_date: None,
1982 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1983 archived_date: None,
1984 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
1985 };
1986
1987 cipher
1988 .populate_cipher_types()
1989 .expect("populate_cipher_types failed");
1990
1991 assert!(cipher.secure_note.is_some());
1992 }
1993
1994 #[test]
1995 fn test_populate_cipher_types_card() {
1996 let mut cipher = Cipher {
1997 id: Some(TEST_UUID.parse().unwrap()),
1998 organization_id: None,
1999 folder_id: None,
2000 collection_ids: vec![],
2001 key: None,
2002 name: TEST_CIPHER_NAME.parse().unwrap(),
2003 notes: None,
2004 r#type: CipherType::Card,
2005 login: None,
2006 identity: None,
2007 card: None,
2008 secure_note: None,
2009 ssh_key: None,
2010 favorite: false,
2011 reprompt: CipherRepromptType::None,
2012 organization_use_totp: false,
2013 edit: true,
2014 view_password: true,
2015 permissions: None,
2016 local_data: None,
2017 attachments: None,
2018 fields: None,
2019 password_history: None,
2020 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2021 deleted_date: None,
2022 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2023 archived_date: None,
2024 data: Some(format!(
2025 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2026 TEST_ENC_STRING_1,
2027 TEST_ENC_STRING_2,
2028 TEST_ENC_STRING_3,
2029 TEST_ENC_STRING_4,
2030 TEST_ENC_STRING_5,
2031 TEST_ENC_STRING_1
2032 )),
2033 };
2034
2035 cipher
2036 .populate_cipher_types()
2037 .expect("populate_cipher_types failed");
2038
2039 assert!(cipher.card.is_some());
2040 let card = cipher.card.unwrap();
2041 assert_eq!(
2042 card.cardholder_name.as_ref().unwrap().to_string(),
2043 TEST_ENC_STRING_1
2044 );
2045 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
2046 assert_eq!(
2047 card.exp_month.as_ref().unwrap().to_string(),
2048 TEST_ENC_STRING_3
2049 );
2050 assert_eq!(
2051 card.exp_year.as_ref().unwrap().to_string(),
2052 TEST_ENC_STRING_4
2053 );
2054 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
2055 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
2056 }
2057
2058 #[test]
2059 fn test_populate_cipher_types_identity() {
2060 let mut cipher = Cipher {
2061 id: Some(TEST_UUID.parse().unwrap()),
2062 organization_id: None,
2063 folder_id: None,
2064 collection_ids: vec![],
2065 key: None,
2066 name: TEST_CIPHER_NAME.parse().unwrap(),
2067 notes: None,
2068 r#type: CipherType::Identity,
2069 login: None,
2070 identity: None,
2071 card: None,
2072 secure_note: None,
2073 ssh_key: None,
2074 favorite: false,
2075 reprompt: CipherRepromptType::None,
2076 organization_use_totp: false,
2077 edit: true,
2078 view_password: true,
2079 permissions: None,
2080 local_data: None,
2081 attachments: None,
2082 fields: None,
2083 password_history: None,
2084 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2085 deleted_date: None,
2086 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2087 archived_date: None,
2088 data: Some(format!(
2089 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2090 TEST_ENC_STRING_1,
2091 TEST_ENC_STRING_2,
2092 TEST_ENC_STRING_3,
2093 TEST_ENC_STRING_4,
2094 TEST_ENC_STRING_5,
2095 TEST_ENC_STRING_1,
2096 TEST_ENC_STRING_2,
2097 TEST_ENC_STRING_3,
2098 TEST_ENC_STRING_4,
2099 TEST_ENC_STRING_5
2100 )),
2101 };
2102
2103 cipher
2104 .populate_cipher_types()
2105 .expect("populate_cipher_types failed");
2106
2107 assert!(cipher.identity.is_some());
2108 let identity = cipher.identity.unwrap();
2109 assert_eq!(
2110 identity.first_name.as_ref().unwrap().to_string(),
2111 TEST_ENC_STRING_1
2112 );
2113 assert_eq!(
2114 identity.last_name.as_ref().unwrap().to_string(),
2115 TEST_ENC_STRING_2
2116 );
2117 assert_eq!(
2118 identity.email.as_ref().unwrap().to_string(),
2119 TEST_ENC_STRING_3
2120 );
2121 assert_eq!(
2122 identity.phone.as_ref().unwrap().to_string(),
2123 TEST_ENC_STRING_4
2124 );
2125 assert_eq!(
2126 identity.company.as_ref().unwrap().to_string(),
2127 TEST_ENC_STRING_5
2128 );
2129 assert_eq!(
2130 identity.address1.as_ref().unwrap().to_string(),
2131 TEST_ENC_STRING_1
2132 );
2133 assert_eq!(
2134 identity.city.as_ref().unwrap().to_string(),
2135 TEST_ENC_STRING_2
2136 );
2137 assert_eq!(
2138 identity.state.as_ref().unwrap().to_string(),
2139 TEST_ENC_STRING_3
2140 );
2141 assert_eq!(
2142 identity.postal_code.as_ref().unwrap().to_string(),
2143 TEST_ENC_STRING_4
2144 );
2145 assert_eq!(
2146 identity.country.as_ref().unwrap().to_string(),
2147 TEST_ENC_STRING_5
2148 );
2149 }
2150
2151 #[test]
2152
2153 fn test_password_history_with_hidden_fields() {
2154 let mut original_cipher = generate_cipher();
2155 original_cipher.fields = Some(vec![FieldView {
2156 name: Some("Secret Key".to_string()),
2157 value: Some("old_secret_value".to_string()),
2158 r#type: crate::FieldType::Hidden,
2159 linked_id: None,
2160 }]);
2161
2162 let mut new_cipher = generate_cipher();
2163 new_cipher.fields = Some(vec![FieldView {
2164 name: Some("Secret Key".to_string()),
2165 value: Some("new_secret_value".to_string()),
2166 r#type: crate::FieldType::Hidden,
2167 linked_id: None,
2168 }]);
2169
2170 new_cipher.update_password_history(&original_cipher);
2171
2172 assert!(new_cipher.password_history.is_some());
2173 let history = new_cipher.password_history.unwrap();
2174 assert_eq!(history.len(), 1);
2175 assert_eq!(history[0].password, "Secret Key: old_secret_value");
2176 }
2177
2178 #[test]
2179 fn test_password_history_length_limit() {
2180 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
2181
2182 let mut original_cipher = generate_cipher();
2183 original_cipher.password_history = Some(
2184 (0..10)
2185 .map(|i| PasswordHistoryView {
2186 password: format!("old_password_{}", i),
2187 last_used_date: chrono::Utc::now(),
2188 })
2189 .collect(),
2190 );
2191
2192 let mut new_cipher = original_cipher.clone();
2193 if let Some(ref mut login) = new_cipher.login {
2195 login.password = Some("brand_new_password".to_string());
2196 }
2197
2198 new_cipher.update_password_history(&original_cipher);
2199
2200 assert!(new_cipher.password_history.is_some());
2201 let history = new_cipher.password_history.unwrap();
2202
2203 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
2205
2206 assert_eq!(history[0].password, "test_password");
2208 assert_eq!(history[1].password, "old_password_0");
2210 assert_eq!(history[2].password, "old_password_1");
2211 assert_eq!(history[3].password, "old_password_2");
2212 assert_eq!(history[4].password, "old_password_3");
2213 }
2214
2215 #[test]
2216 fn test_populate_cipher_types_ssh_key() {
2217 let mut cipher = Cipher {
2218 id: Some(TEST_UUID.parse().unwrap()),
2219 organization_id: None,
2220 folder_id: None,
2221 collection_ids: vec![],
2222 key: None,
2223 name: TEST_CIPHER_NAME.parse().unwrap(),
2224 notes: None,
2225 r#type: CipherType::SshKey,
2226 login: None,
2227 identity: None,
2228 card: None,
2229 secure_note: None,
2230 ssh_key: None,
2231 favorite: false,
2232 reprompt: CipherRepromptType::None,
2233 organization_use_totp: false,
2234 edit: true,
2235 view_password: true,
2236 permissions: None,
2237 local_data: None,
2238 attachments: None,
2239 fields: None,
2240 password_history: None,
2241 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2242 deleted_date: None,
2243 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2244 archived_date: None,
2245 data: Some(format!(
2246 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2247 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2248 )),
2249 };
2250
2251 cipher
2252 .populate_cipher_types()
2253 .expect("populate_cipher_types failed");
2254
2255 assert!(cipher.ssh_key.is_some());
2256 let ssh_key = cipher.ssh_key.unwrap();
2257 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2258 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2259 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2260 }
2261
2262 #[test]
2263 fn test_populate_cipher_types_with_null_data() {
2264 let mut cipher = Cipher {
2265 id: Some(TEST_UUID.parse().unwrap()),
2266 organization_id: None,
2267 folder_id: None,
2268 collection_ids: vec![],
2269 key: None,
2270 name: TEST_CIPHER_NAME.parse().unwrap(),
2271 notes: None,
2272 r#type: CipherType::Login,
2273 login: None,
2274 identity: None,
2275 card: None,
2276 secure_note: None,
2277 ssh_key: None,
2278 favorite: false,
2279 reprompt: CipherRepromptType::None,
2280 organization_use_totp: false,
2281 edit: true,
2282 view_password: true,
2283 permissions: None,
2284 local_data: None,
2285 attachments: None,
2286 fields: None,
2287 password_history: None,
2288 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2289 deleted_date: None,
2290 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2291 archived_date: None,
2292 data: None,
2293 };
2294
2295 let result = cipher.populate_cipher_types();
2296 assert!(matches!(
2297 result,
2298 Err(VaultParseError::MissingField(MissingFieldError("data")))
2299 ));
2300 }
2301
2302 #[test]
2303 fn test_populate_cipher_types_with_invalid_json() {
2304 let mut cipher = Cipher {
2305 id: Some(TEST_UUID.parse().unwrap()),
2306 organization_id: None,
2307 folder_id: None,
2308 collection_ids: vec![],
2309 key: None,
2310 name: TEST_CIPHER_NAME.parse().unwrap(),
2311 notes: None,
2312 r#type: CipherType::Login,
2313 login: None,
2314 identity: None,
2315 card: None,
2316 secure_note: None,
2317 ssh_key: None,
2318 favorite: false,
2319 reprompt: CipherRepromptType::None,
2320 organization_use_totp: false,
2321 edit: true,
2322 view_password: true,
2323 permissions: None,
2324 local_data: None,
2325 attachments: None,
2326 fields: None,
2327 password_history: None,
2328 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2329 deleted_date: None,
2330 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2331 archived_date: None,
2332 data: Some("invalid json".to_string()),
2333 };
2334
2335 let result = cipher.populate_cipher_types();
2336
2337 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2338 }
2339
2340 #[test]
2341 fn test_decrypt_cipher_with_mixed_attachments() {
2342 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2343 let key_store = create_test_crypto_with_user_key(user_key);
2344
2345 let mut ctx = key_store.context();
2347 let valid1 = "valid_file_1.txt"
2348 .encrypt(&mut ctx, SymmetricKeyId::User)
2349 .unwrap();
2350 let valid2 = "valid_file_2.txt"
2351 .encrypt(&mut ctx, SymmetricKeyId::User)
2352 .unwrap();
2353
2354 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
2356 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
2357 let mut wrong_ctx = wrong_key_store.context();
2358 let corrupted = "corrupted_file.txt"
2359 .encrypt(&mut wrong_ctx, SymmetricKeyId::User)
2360 .unwrap();
2361
2362 let cipher = Cipher {
2363 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2364 organization_id: None,
2365 folder_id: None,
2366 collection_ids: vec![],
2367 key: None,
2368 name: TEST_CIPHER_NAME.parse().unwrap(),
2369 notes: None,
2370 r#type: CipherType::Login,
2371 login: None,
2372 identity: None,
2373 card: None,
2374 secure_note: None,
2375 ssh_key: None,
2376 favorite: false,
2377 reprompt: CipherRepromptType::None,
2378 organization_use_totp: false,
2379 edit: true,
2380 permissions: None,
2381 view_password: true,
2382 local_data: None,
2383 attachments: Some(vec![
2384 attachment::Attachment {
2386 id: Some("valid-attachment".to_string()),
2387 url: Some("https://example.com/valid".to_string()),
2388 size: Some("100".to_string()),
2389 size_name: Some("100 Bytes".to_string()),
2390 file_name: Some(valid1),
2391 key: None,
2392 },
2393 attachment::Attachment {
2395 id: Some("corrupted-attachment".to_string()),
2396 url: Some("https://example.com/corrupted".to_string()),
2397 size: Some("200".to_string()),
2398 size_name: Some("200 Bytes".to_string()),
2399 file_name: Some(corrupted),
2400 key: None,
2401 },
2402 attachment::Attachment {
2404 id: Some("valid-attachment-2".to_string()),
2405 url: Some("https://example.com/valid2".to_string()),
2406 size: Some("150".to_string()),
2407 size_name: Some("150 Bytes".to_string()),
2408 file_name: Some(valid2),
2409 key: None,
2410 },
2411 ]),
2412 fields: None,
2413 password_history: None,
2414 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2415 deleted_date: None,
2416 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2417 archived_date: None,
2418 data: None,
2419 };
2420
2421 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2422
2423 assert!(view.attachments.is_some());
2425 let successes = view.attachments.as_ref().unwrap();
2426 assert_eq!(successes.len(), 2);
2427 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
2428 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
2429
2430 assert!(view.attachment_decryption_failures.is_some());
2432 let failures = view.attachment_decryption_failures.as_ref().unwrap();
2433 assert_eq!(failures.len(), 1);
2434 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
2435 assert_eq!(failures[0].file_name, None);
2436 }
2437}