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