1use bitwarden_api_api::{
2 apis::ciphers_api::{PutShareError, PutShareManyError},
3 models::{
4 CipherDetailsResponseModel, CipherRequestModel, CipherResponseModel,
5 CipherWithIdRequestModel,
6 },
7};
8use bitwarden_collections::collection::CollectionId;
9use bitwarden_core::{
10 MissingFieldError, OrganizationId, UserId,
11 key_management::{KeyIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeyId},
12 require,
13};
14use bitwarden_crypto::{
15 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
16 PrimitiveEncryptable,
17};
18use bitwarden_error::bitwarden_error;
19use bitwarden_state::repository::RepositoryError;
20use bitwarden_uuid::uuid_newtype;
21use chrono::{DateTime, SecondsFormat, Utc};
22use serde::{Deserialize, Serialize};
23use serde_repr::{Deserialize_repr, Serialize_repr};
24use thiserror::Error;
25#[cfg(feature = "wasm")]
26use tsify::Tsify;
27#[cfg(feature = "wasm")]
28use wasm_bindgen::prelude::wasm_bindgen;
29
30use super::{
31 attachment, card,
32 card::CardListView,
33 cipher_permissions::CipherPermissions,
34 field, identity,
35 local_data::{LocalData, LocalDataView},
36 login::LoginListView,
37 secure_note, ssh_key,
38};
39use crate::{
40 AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView,
41 FieldView, FolderId, Login, LoginView, VaultParseError,
42 password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
43};
44
45uuid_newtype!(pub CipherId);
46
47#[allow(missing_docs)]
48#[bitwarden_error(flat)]
49#[derive(Debug, Error)]
50pub enum CipherError {
51 #[error(transparent)]
52 MissingField(#[from] MissingFieldError),
53 #[error(transparent)]
54 Crypto(#[from] CryptoError),
55 #[error(transparent)]
56 Decrypt(#[from] DecryptError),
57 #[error(transparent)]
58 Encrypt(#[from] EncryptError),
59 #[error(
60 "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
61 )]
62 AttachmentsWithoutKeys,
63 #[error("This cipher cannot be moved to the specified organization")]
64 OrganizationAlreadySet,
65 #[error(transparent)]
66 PutShare(#[from] bitwarden_api_api::apis::Error<PutShareError>),
67 #[error(transparent)]
68 PutShareMany(#[from] bitwarden_api_api::apis::Error<PutShareManyError>),
69 #[error(transparent)]
70 Repository(#[from] RepositoryError),
71 #[error(transparent)]
72 Chrono(#[from] chrono::ParseError),
73 #[error(transparent)]
74 SerdeJson(#[from] serde_json::Error),
75}
76
77pub(super) trait CipherKind {
79 fn decrypt_subtitle(
81 &self,
82 ctx: &mut KeyStoreContext<KeyIds>,
83 key: SymmetricKeyId,
84 ) -> Result<String, CryptoError>;
85
86 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
88}
89
90#[allow(missing_docs)]
91#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
92#[repr(u8)]
93#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
94#[cfg_attr(feature = "wasm", wasm_bindgen)]
95pub enum CipherType {
96 Login = 1,
97 SecureNote = 2,
98 Card = 3,
99 Identity = 4,
100 SshKey = 5,
101}
102
103#[allow(missing_docs)]
104#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
105#[repr(u8)]
106#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
107#[cfg_attr(feature = "wasm", wasm_bindgen)]
108pub enum CipherRepromptType {
109 #[default]
110 None = 0,
111 Password = 1,
112}
113
114#[allow(missing_docs)]
115#[derive(Serialize, Deserialize, Debug, Clone)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
118#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
119pub struct EncryptionContext {
120 pub encrypted_for: UserId,
123 pub cipher: Cipher,
124}
125
126impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
127 type Error = CipherError;
128 fn try_from(
129 EncryptionContext {
130 cipher,
131 encrypted_for,
132 }: EncryptionContext,
133 ) -> Result<Self, Self::Error> {
134 Ok(Self {
135 id: require!(cipher.id).into(),
136 encrypted_for: Some(encrypted_for.into()),
137 r#type: Some(cipher.r#type.into()),
138 organization_id: cipher.organization_id.map(|o| o.to_string()),
139 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
140 favorite: cipher.favorite.into(),
141 reprompt: Some(cipher.reprompt.into()),
142 key: cipher.key.map(|k| k.to_string()),
143 name: cipher.name.to_string(),
144 notes: cipher.notes.map(|n| n.to_string()),
145 fields: Some(
146 cipher
147 .fields
148 .into_iter()
149 .flatten()
150 .map(Into::into)
151 .collect(),
152 ),
153 password_history: Some(
154 cipher
155 .password_history
156 .into_iter()
157 .flatten()
158 .map(Into::into)
159 .collect(),
160 ),
161 attachments: None,
162 attachments2: Some(
163 cipher
164 .attachments
165 .into_iter()
166 .flatten()
167 .filter_map(|a| {
168 a.id.map(|id| {
169 (
170 id,
171 bitwarden_api_api::models::CipherAttachmentModel {
172 file_name: a.file_name.map(|n| n.to_string()),
173 key: a.key.map(|k| k.to_string()),
174 },
175 )
176 })
177 })
178 .collect(),
179 ),
180 login: cipher.login.map(|l| Box::new(l.into())),
181 card: cipher.card.map(|c| Box::new(c.into())),
182 identity: cipher.identity.map(|i| Box::new(i.into())),
183 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
184 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
185 data: None, last_known_revision_date: Some(
187 cipher
188 .revision_date
189 .to_rfc3339_opts(SecondsFormat::Millis, true),
190 ),
191 archived_date: cipher
192 .archived_date
193 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
194 })
195 }
196}
197
198impl From<EncryptionContext> for CipherRequestModel {
199 fn from(
200 EncryptionContext {
201 cipher,
202 encrypted_for,
203 }: EncryptionContext,
204 ) -> Self {
205 Self {
206 encrypted_for: Some(encrypted_for.into()),
207 r#type: Some(cipher.r#type.into()),
208 organization_id: cipher.organization_id.map(|o| o.to_string()),
209 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
210 favorite: cipher.favorite.into(),
211 reprompt: Some(cipher.reprompt.into()),
212 key: cipher.key.map(|k| k.to_string()),
213 name: cipher.name.to_string(),
214 notes: cipher.notes.map(|n| n.to_string()),
215 fields: Some(
216 cipher
217 .fields
218 .into_iter()
219 .flatten()
220 .map(Into::into)
221 .collect(),
222 ),
223 password_history: Some(
224 cipher
225 .password_history
226 .into_iter()
227 .flatten()
228 .map(Into::into)
229 .collect(),
230 ),
231 attachments: None,
232 attachments2: Some(
233 cipher
234 .attachments
235 .into_iter()
236 .flatten()
237 .filter_map(|a| {
238 a.id.map(|id| {
239 (
240 id,
241 bitwarden_api_api::models::CipherAttachmentModel {
242 file_name: a.file_name.map(|n| n.to_string()),
243 key: a.key.map(|k| k.to_string()),
244 },
245 )
246 })
247 })
248 .collect(),
249 ),
250 login: cipher.login.map(|l| Box::new(l.into())),
251 card: cipher.card.map(|c| Box::new(c.into())),
252 identity: cipher.identity.map(|i| Box::new(i.into())),
253 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
254 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
255 data: None, last_known_revision_date: Some(
257 cipher
258 .revision_date
259 .to_rfc3339_opts(SecondsFormat::Millis, true),
260 ),
261 archived_date: cipher
262 .archived_date
263 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
264 }
265 }
266}
267
268#[allow(missing_docs)]
269#[derive(Serialize, Deserialize, Debug, Clone)]
270#[serde(rename_all = "camelCase", deny_unknown_fields)]
271#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
272#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
273pub struct Cipher {
274 pub id: Option<CipherId>,
275 pub organization_id: Option<OrganizationId>,
276 pub folder_id: Option<FolderId>,
277 pub collection_ids: Vec<CollectionId>,
278 pub key: Option<EncString>,
281
282 pub name: EncString,
283 pub notes: Option<EncString>,
284
285 pub r#type: CipherType,
286 pub login: Option<Login>,
287 pub identity: Option<identity::Identity>,
288 pub card: Option<card::Card>,
289 pub secure_note: Option<secure_note::SecureNote>,
290 pub ssh_key: Option<ssh_key::SshKey>,
291
292 pub favorite: bool,
293 pub reprompt: CipherRepromptType,
294 pub organization_use_totp: bool,
295 pub edit: bool,
296 pub permissions: Option<CipherPermissions>,
297 pub view_password: bool,
298 pub local_data: Option<LocalData>,
299
300 pub attachments: Option<Vec<attachment::Attachment>>,
301 pub fields: Option<Vec<field::Field>>,
302 pub password_history: Option<Vec<password_history::PasswordHistory>>,
303
304 pub creation_date: DateTime<Utc>,
305 pub deleted_date: Option<DateTime<Utc>>,
306 pub revision_date: DateTime<Utc>,
307 pub archived_date: Option<DateTime<Utc>>,
308 pub data: Option<String>,
309}
310
311bitwarden_state::register_repository_item!(Cipher, "Cipher");
312
313#[allow(missing_docs)]
314#[derive(Serialize, Deserialize, Debug, Clone)]
315#[serde(rename_all = "camelCase", deny_unknown_fields)]
316#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
317#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
318pub struct CipherView {
319 pub id: Option<CipherId>,
320 pub organization_id: Option<OrganizationId>,
321 pub folder_id: Option<FolderId>,
322 pub collection_ids: Vec<CollectionId>,
323
324 pub key: Option<EncString>,
326
327 pub name: String,
328 pub notes: Option<String>,
329
330 pub r#type: CipherType,
331 pub login: Option<LoginView>,
332 pub identity: Option<identity::IdentityView>,
333 pub card: Option<card::CardView>,
334 pub secure_note: Option<secure_note::SecureNoteView>,
335 pub ssh_key: Option<ssh_key::SshKeyView>,
336
337 pub favorite: bool,
338 pub reprompt: CipherRepromptType,
339 pub organization_use_totp: bool,
340 pub edit: bool,
341 pub permissions: Option<CipherPermissions>,
342 pub view_password: bool,
343 pub local_data: Option<LocalDataView>,
344
345 pub attachments: Option<Vec<attachment::AttachmentView>>,
346 pub fields: Option<Vec<field::FieldView>>,
347 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
348
349 pub creation_date: DateTime<Utc>,
350 pub deleted_date: Option<DateTime<Utc>>,
351 pub revision_date: DateTime<Utc>,
352 pub archived_date: Option<DateTime<Utc>>,
353}
354
355#[allow(missing_docs)]
356#[derive(Serialize, Deserialize, Debug, PartialEq)]
357#[serde(rename_all = "camelCase", deny_unknown_fields)]
358#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
359#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
360pub enum CipherListViewType {
361 Login(LoginListView),
362 SecureNote,
363 Card(CardListView),
364 Identity,
365 SshKey,
366}
367
368#[derive(Serialize, Deserialize, Debug, PartialEq)]
370#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
371#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
372pub enum CopyableCipherFields {
373 LoginUsername,
374 LoginPassword,
375 LoginTotp,
376 CardNumber,
377 CardSecurityCode,
378 IdentityUsername,
379 IdentityEmail,
380 IdentityPhone,
381 IdentityAddress,
382 SshKey,
383 SecureNotes,
384}
385
386#[allow(missing_docs)]
387#[derive(Serialize, Deserialize, Debug, PartialEq)]
388#[serde(rename_all = "camelCase", deny_unknown_fields)]
389#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
390#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
391pub struct CipherListView {
392 pub id: Option<CipherId>,
393 pub organization_id: Option<OrganizationId>,
394 pub folder_id: Option<FolderId>,
395 pub collection_ids: Vec<CollectionId>,
396
397 pub key: Option<EncString>,
399
400 pub name: String,
401 pub subtitle: String,
402
403 pub r#type: CipherListViewType,
404
405 pub favorite: bool,
406 pub reprompt: CipherRepromptType,
407 pub organization_use_totp: bool,
408 pub edit: bool,
409 pub permissions: Option<CipherPermissions>,
410
411 pub view_password: bool,
412
413 pub attachments: u32,
415 pub has_old_attachments: bool,
417
418 pub creation_date: DateTime<Utc>,
419 pub deleted_date: Option<DateTime<Utc>>,
420 pub revision_date: DateTime<Utc>,
421 pub archived_date: Option<DateTime<Utc>>,
422
423 pub copyable_fields: Vec<CopyableCipherFields>,
425
426 pub local_data: Option<LocalDataView>,
427}
428
429#[derive(Serialize, Deserialize, Debug)]
435#[serde(rename_all = "camelCase", deny_unknown_fields)]
436#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
437#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
438pub struct DecryptCipherListResult {
439 pub successes: Vec<CipherListView>,
441 pub failures: Vec<Cipher>,
443}
444
445impl CipherListView {
446 pub(crate) fn get_totp_key(
447 self,
448 ctx: &mut KeyStoreContext<KeyIds>,
449 ) -> Result<Option<String>, CryptoError> {
450 let key = self.key_identifier();
451 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
452
453 let totp = match self.r#type {
454 CipherListViewType::Login(LoginListView { totp, .. }) => {
455 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
456 }
457 _ => None,
458 };
459
460 Ok(totp)
461 }
462}
463
464impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
465 fn encrypt_composite(
466 &self,
467 ctx: &mut KeyStoreContext<KeyIds>,
468 key: SymmetricKeyId,
469 ) -> Result<Cipher, CryptoError> {
470 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
471
472 let mut cipher_view = self.clone();
473 cipher_view.generate_checksums();
474
475 Ok(Cipher {
476 id: cipher_view.id,
477 organization_id: cipher_view.organization_id,
478 folder_id: cipher_view.folder_id,
479 collection_ids: cipher_view.collection_ids,
480 key: cipher_view.key,
481 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
482 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
483 r#type: cipher_view.r#type,
484 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
485 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
486 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
487 secure_note: cipher_view
488 .secure_note
489 .encrypt_composite(ctx, ciphers_key)?,
490 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
491 favorite: cipher_view.favorite,
492 reprompt: cipher_view.reprompt,
493 organization_use_totp: cipher_view.organization_use_totp,
494 edit: cipher_view.edit,
495 view_password: cipher_view.view_password,
496 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
497 attachments: cipher_view
498 .attachments
499 .encrypt_composite(ctx, ciphers_key)?,
500 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
501 password_history: cipher_view
502 .password_history
503 .encrypt_composite(ctx, ciphers_key)?,
504 creation_date: cipher_view.creation_date,
505 deleted_date: cipher_view.deleted_date,
506 revision_date: cipher_view.revision_date,
507 permissions: cipher_view.permissions,
508 archived_date: cipher_view.archived_date,
509 data: None, })
511 }
512}
513
514impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
515 fn decrypt(
516 &self,
517 ctx: &mut KeyStoreContext<KeyIds>,
518 key: SymmetricKeyId,
519 ) -> Result<CipherView, CryptoError> {
520 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
521
522 let mut cipher = CipherView {
523 id: self.id,
524 organization_id: self.organization_id,
525 folder_id: self.folder_id,
526 collection_ids: self.collection_ids.clone(),
527 key: self.key.clone(),
528 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
529 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
530 r#type: self.r#type,
531 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
532 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
533 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
534 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
535 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
536 favorite: self.favorite,
537 reprompt: self.reprompt,
538 organization_use_totp: self.organization_use_totp,
539 edit: self.edit,
540 permissions: self.permissions,
541 view_password: self.view_password,
542 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
543 attachments: self.attachments.decrypt(ctx, ciphers_key).ok().flatten(),
544 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
545 password_history: self
546 .password_history
547 .decrypt(ctx, ciphers_key)
548 .ok()
549 .flatten(),
550 creation_date: self.creation_date,
551 deleted_date: self.deleted_date,
552 revision_date: self.revision_date,
553 archived_date: self.archived_date,
554 };
555
556 if cipher.key.is_some()
559 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
560 {
561 cipher.remove_invalid_checksums();
562 }
563
564 Ok(cipher)
565 }
566}
567
568impl Cipher {
569 pub(super) fn decrypt_cipher_key(
580 ctx: &mut KeyStoreContext<KeyIds>,
581 key: SymmetricKeyId,
582 ciphers_key: &Option<EncString>,
583 ) -> Result<SymmetricKeyId, CryptoError> {
584 match ciphers_key {
585 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
586 None => Ok(key),
587 }
588 }
589
590 fn get_kind(&self) -> Option<&dyn CipherKind> {
592 match self.r#type {
593 CipherType::Login => self.login.as_ref().map(|v| v as _),
594 CipherType::Card => self.card.as_ref().map(|v| v as _),
595 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
596 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
597 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
598 }
599 }
600
601 fn decrypt_subtitle(
603 &self,
604 ctx: &mut KeyStoreContext<KeyIds>,
605 key: SymmetricKeyId,
606 ) -> Result<String, CryptoError> {
607 self.get_kind()
608 .map(|sub| sub.decrypt_subtitle(ctx, key))
609 .unwrap_or_else(|| Ok(String::new()))
610 }
611
612 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
615 self.get_kind()
616 .map(|kind| kind.get_copyable_fields(Some(self)))
617 .unwrap_or_default()
618 }
619
620 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
625 let data = self
626 .data
627 .as_ref()
628 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
629
630 match &self.r#type {
631 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
632 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
633 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
634 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
635 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
636 }
637 Ok(())
638 }
639}
640impl CipherView {
641 #[allow(missing_docs)]
642 pub fn generate_cipher_key(
643 &mut self,
644 ctx: &mut KeyStoreContext<KeyIds>,
645 key: SymmetricKeyId,
646 ) -> Result<(), CryptoError> {
647 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
648
649 let new_key = ctx.generate_symmetric_key();
650
651 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
652 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
653
654 self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
655 Ok(())
656 }
657
658 #[allow(missing_docs)]
659 pub fn generate_checksums(&mut self) {
660 if let Some(l) = self.login.as_mut() {
661 l.generate_checksums();
662 }
663 }
664
665 #[allow(missing_docs)]
666 pub fn remove_invalid_checksums(&mut self) {
667 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
668 uris.retain(|u| u.is_checksum_valid());
669 }
670 }
671
672 fn reencrypt_attachment_keys(
673 &mut self,
674 ctx: &mut KeyStoreContext<KeyIds>,
675 old_key: SymmetricKeyId,
676 new_key: SymmetricKeyId,
677 ) -> Result<(), CryptoError> {
678 if let Some(attachments) = &mut self.attachments {
679 AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
680 }
681 Ok(())
682 }
683
684 #[allow(missing_docs)]
685 pub fn decrypt_fido2_credentials(
686 &self,
687 ctx: &mut KeyStoreContext<KeyIds>,
688 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
689 let key = self.key_identifier();
690 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
691
692 Ok(self
693 .login
694 .as_ref()
695 .and_then(|l| l.fido2_credentials.as_ref())
696 .map(|f| f.decrypt(ctx, ciphers_key))
697 .transpose()?
698 .unwrap_or_default())
699 }
700
701 fn reencrypt_fido2_credentials(
702 &mut self,
703 ctx: &mut KeyStoreContext<KeyIds>,
704 old_key: SymmetricKeyId,
705 new_key: SymmetricKeyId,
706 ) -> Result<(), CryptoError> {
707 if let Some(login) = self.login.as_mut() {
708 login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
709 }
710 Ok(())
711 }
712
713 pub fn move_to_organization(
720 &mut self,
721 ctx: &mut KeyStoreContext<KeyIds>,
722 organization_id: OrganizationId,
723 ) -> Result<(), CipherError> {
724 let new_key = SymmetricKeyId::Organization(organization_id);
725
726 self.reencrypt_cipher_keys(ctx, new_key)?;
727 self.organization_id = Some(organization_id);
728
729 Ok(())
730 }
731
732 pub fn reencrypt_cipher_keys(
737 &mut self,
738 ctx: &mut KeyStoreContext<KeyIds>,
739 new_wrapping_key: SymmetricKeyId,
740 ) -> Result<(), CipherError> {
741 let old_key = self.key_identifier();
742
743 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
745 return Err(CipherError::AttachmentsWithoutKeys);
746 }
747
748 if self.key.is_some() {
750 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
752
753 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
755 } else {
756 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
759 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
760 }
761
762 Ok(())
763 }
764
765 #[allow(missing_docs)]
766 pub fn set_new_fido2_credentials(
767 &mut self,
768 ctx: &mut KeyStoreContext<KeyIds>,
769 creds: Vec<Fido2CredentialFullView>,
770 ) -> Result<(), CipherError> {
771 let key = self.key_identifier();
772
773 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
774
775 require!(self.login.as_mut()).fido2_credentials =
776 Some(creds.encrypt_composite(ctx, ciphers_key)?);
777
778 Ok(())
779 }
780
781 #[allow(missing_docs)]
782 pub fn get_fido2_credentials(
783 &self,
784 ctx: &mut KeyStoreContext<KeyIds>,
785 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
786 let key = self.key_identifier();
787
788 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
789
790 let login = require!(self.login.as_ref());
791 let creds = require!(login.fido2_credentials.as_ref());
792 let res = creds.decrypt(ctx, ciphers_key)?;
793 Ok(res)
794 }
795
796 #[allow(missing_docs)]
797 pub fn decrypt_fido2_private_key(
798 &self,
799 ctx: &mut KeyStoreContext<KeyIds>,
800 ) -> Result<String, CipherError> {
801 let fido2_credential = self.get_fido2_credentials(ctx)?;
802
803 Ok(fido2_credential[0].key_value.clone())
804 }
805
806 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
807 let changes = self
808 .login
809 .as_mut()
810 .map_or(vec![], |login| {
811 login.detect_password_change(&original_cipher.login)
812 })
813 .into_iter()
814 .chain(self.fields.as_deref().map_or(vec![], |fields| {
815 FieldView::detect_hidden_field_changes(
816 fields,
817 original_cipher.fields.as_deref().unwrap_or(&[]),
818 )
819 }))
820 .rev()
821 .chain(original_cipher.password_history.iter().flatten().cloned())
822 .take(MAX_PASSWORD_HISTORY_ENTRIES)
823 .collect();
824 self.password_history = Some(changes)
825 }
826}
827
828impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
829 fn decrypt(
830 &self,
831 ctx: &mut KeyStoreContext<KeyIds>,
832 key: SymmetricKeyId,
833 ) -> Result<CipherListView, CryptoError> {
834 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
835
836 Ok(CipherListView {
837 id: self.id,
838 organization_id: self.organization_id,
839 folder_id: self.folder_id,
840 collection_ids: self.collection_ids.clone(),
841 key: self.key.clone(),
842 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
843 subtitle: self
844 .decrypt_subtitle(ctx, ciphers_key)
845 .ok()
846 .unwrap_or_default(),
847 r#type: match self.r#type {
848 CipherType::Login => {
849 let login = self
850 .login
851 .as_ref()
852 .ok_or(CryptoError::MissingField("login"))?;
853 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
854 }
855 CipherType::SecureNote => CipherListViewType::SecureNote,
856 CipherType::Card => {
857 let card = self
858 .card
859 .as_ref()
860 .ok_or(CryptoError::MissingField("card"))?;
861 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
862 }
863 CipherType::Identity => CipherListViewType::Identity,
864 CipherType::SshKey => CipherListViewType::SshKey,
865 },
866 favorite: self.favorite,
867 reprompt: self.reprompt,
868 organization_use_totp: self.organization_use_totp,
869 edit: self.edit,
870 permissions: self.permissions,
871 view_password: self.view_password,
872 attachments: self
873 .attachments
874 .as_ref()
875 .map(|a| a.len() as u32)
876 .unwrap_or(0),
877 has_old_attachments: self
878 .attachments
879 .as_ref()
880 .map(|a| a.iter().any(|att| att.key.is_none()))
881 .unwrap_or(false),
882 creation_date: self.creation_date,
883 deleted_date: self.deleted_date,
884 revision_date: self.revision_date,
885 copyable_fields: self.get_copyable_fields(),
886 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
887 archived_date: self.archived_date,
888 })
889 }
890}
891
892impl IdentifyKey<SymmetricKeyId> for Cipher {
893 fn key_identifier(&self) -> SymmetricKeyId {
894 match self.organization_id {
895 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
896 None => SymmetricKeyId::User,
897 }
898 }
899}
900
901impl IdentifyKey<SymmetricKeyId> for CipherView {
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 CipherListView {
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 TryFrom<CipherDetailsResponseModel> for Cipher {
920 type Error = VaultParseError;
921
922 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
923 Ok(Self {
924 id: cipher.id.map(CipherId::new),
925 organization_id: cipher.organization_id.map(OrganizationId::new),
926 folder_id: cipher.folder_id.map(FolderId::new),
927 collection_ids: cipher
928 .collection_ids
929 .unwrap_or_default()
930 .into_iter()
931 .map(CollectionId::new)
932 .collect(),
933 name: require!(EncString::try_from_optional(cipher.name)?),
934 notes: EncString::try_from_optional(cipher.notes)?,
935 r#type: require!(cipher.r#type).into(),
936 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
937 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
938 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
939 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
940 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
941 favorite: cipher.favorite.unwrap_or(false),
942 reprompt: cipher
943 .reprompt
944 .map(|r| r.into())
945 .unwrap_or(CipherRepromptType::None),
946 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
947 edit: cipher.edit.unwrap_or(true),
948 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
949 view_password: cipher.view_password.unwrap_or(true),
950 local_data: None, attachments: cipher
952 .attachments
953 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
954 .transpose()?,
955 fields: cipher
956 .fields
957 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
958 .transpose()?,
959 password_history: cipher
960 .password_history
961 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
962 .transpose()?,
963 creation_date: require!(cipher.creation_date).parse()?,
964 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
965 revision_date: require!(cipher.revision_date).parse()?,
966 key: EncString::try_from_optional(cipher.key)?,
967 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
968 data: cipher.data,
969 })
970 }
971}
972
973impl From<bitwarden_api_api::models::CipherType> for CipherType {
974 fn from(t: bitwarden_api_api::models::CipherType) -> Self {
975 match t {
976 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
977 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
978 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
979 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
980 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
981 }
982 }
983}
984
985impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
986 fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
987 match t {
988 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
989 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
990 }
991 }
992}
993
994impl From<CipherType> for bitwarden_api_api::models::CipherType {
995 fn from(t: CipherType) -> Self {
996 match t {
997 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
998 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
999 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1000 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1001 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1002 }
1003 }
1004}
1005
1006impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1007 fn from(t: CipherRepromptType) -> Self {
1008 match t {
1009 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1010 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1011 }
1012 }
1013}
1014
1015impl TryFrom<CipherResponseModel> for Cipher {
1016 type Error = VaultParseError;
1017
1018 fn try_from(cipher: CipherResponseModel) -> Result<Self, Self::Error> {
1019 Ok(Self {
1020 id: cipher.id.map(CipherId::new),
1021 organization_id: cipher.organization_id.map(OrganizationId::new),
1022 folder_id: cipher.folder_id.map(FolderId::new),
1023 collection_ids: vec![], name: require!(cipher.name).parse()?,
1025 notes: EncString::try_from_optional(cipher.notes)?,
1026 r#type: require!(cipher.r#type).into(),
1027 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1028 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1029 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1030 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1031 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1032 favorite: cipher.favorite.unwrap_or(false),
1033 reprompt: cipher
1034 .reprompt
1035 .map(|r| r.into())
1036 .unwrap_or(CipherRepromptType::None),
1037 organization_use_totp: cipher.organization_use_totp.unwrap_or(false),
1038 edit: cipher.edit.unwrap_or(false),
1039 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1040 view_password: cipher.view_password.unwrap_or(true),
1041 local_data: None, attachments: cipher
1043 .attachments
1044 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1045 .transpose()?,
1046 fields: cipher
1047 .fields
1048 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1049 .transpose()?,
1050 password_history: cipher
1051 .password_history
1052 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1053 .transpose()?,
1054 creation_date: require!(cipher.creation_date).parse()?,
1055 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1056 revision_date: require!(cipher.revision_date).parse()?,
1057 key: EncString::try_from_optional(cipher.key)?,
1058 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1059 data: cipher.data,
1060 })
1061 }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066
1067 use attachment::AttachmentView;
1068 use bitwarden_core::key_management::{
1069 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1070 };
1071 use bitwarden_crypto::SymmetricCryptoKey;
1072
1073 use super::*;
1074 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1075
1076 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1078 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1079 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1080 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1081 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1082 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1083 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1084
1085 fn generate_cipher() -> CipherView {
1086 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1087 CipherView {
1088 r#type: CipherType::Login,
1089 login: Some(LoginView {
1090 username: Some("test_username".to_string()),
1091 password: Some("test_password".to_string()),
1092 password_revision_date: None,
1093 uris: None,
1094 totp: None,
1095 autofill_on_page_load: None,
1096 fido2_credentials: None,
1097 }),
1098 id: Some(test_id),
1099 organization_id: None,
1100 folder_id: None,
1101 collection_ids: vec![],
1102 key: None,
1103 name: "My test login".to_string(),
1104 notes: None,
1105 identity: None,
1106 card: None,
1107 secure_note: None,
1108 ssh_key: None,
1109 favorite: false,
1110 reprompt: CipherRepromptType::None,
1111 organization_use_totp: true,
1112 edit: true,
1113 permissions: None,
1114 view_password: true,
1115 local_data: None,
1116 attachments: None,
1117 fields: None,
1118 password_history: None,
1119 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1120 deleted_date: None,
1121 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1122 archived_date: None,
1123 }
1124 }
1125
1126 fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
1127 Fido2Credential {
1128 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1129 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1130 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1131 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1132 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1133 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1134 user_handle: None,
1135 user_name: None,
1136 counter: "123".to_string().encrypt(ctx, key).unwrap(),
1137 rp_name: None,
1138 user_display_name: None,
1139 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1140 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1141 }
1142 }
1143
1144 #[test]
1145 fn test_decrypt_cipher_list_view() {
1146 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1147 let key_store = create_test_crypto_with_user_key(key);
1148
1149 let cipher = Cipher {
1150 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1151 organization_id: None,
1152 folder_id: None,
1153 collection_ids: vec![],
1154 key: None,
1155 name: TEST_CIPHER_NAME.parse().unwrap(),
1156 notes: None,
1157 r#type: CipherType::Login,
1158 login: Some(Login {
1159 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1160 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1161 password_revision_date: None,
1162 uris: None,
1163 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1164 autofill_on_page_load: None,
1165 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
1166 }),
1167 identity: None,
1168 card: None,
1169 secure_note: None,
1170 ssh_key: None,
1171 favorite: false,
1172 reprompt: CipherRepromptType::None,
1173 organization_use_totp: false,
1174 edit: true,
1175 permissions: Some(CipherPermissions {
1176 delete: false,
1177 restore: false
1178 }),
1179 view_password: true,
1180 local_data: None,
1181 attachments: None,
1182 fields: None,
1183 password_history: None,
1184 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1185 deleted_date: None,
1186 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1187 archived_date: None,
1188 data: None,
1189 };
1190
1191 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1192
1193 assert_eq!(
1194 view,
1195 CipherListView {
1196 id: cipher.id,
1197 organization_id: cipher.organization_id,
1198 folder_id: cipher.folder_id,
1199 collection_ids: cipher.collection_ids,
1200 key: cipher.key,
1201 name: "My test login".to_string(),
1202 subtitle: "test_username".to_string(),
1203 r#type: CipherListViewType::Login(LoginListView {
1204 fido2_credentials: Some(vec![Fido2CredentialListView {
1205 credential_id: "123".to_string(),
1206 rp_id: "123".to_string(),
1207 user_handle: None,
1208 user_name: None,
1209 user_display_name: None,
1210 counter: "123".to_string(),
1211 }]),
1212 has_fido2: true,
1213 username: Some("test_username".to_string()),
1214 totp: cipher.login.as_ref().unwrap().totp.clone(),
1215 uris: None,
1216 }),
1217 favorite: cipher.favorite,
1218 reprompt: cipher.reprompt,
1219 organization_use_totp: cipher.organization_use_totp,
1220 edit: cipher.edit,
1221 permissions: cipher.permissions,
1222 view_password: cipher.view_password,
1223 attachments: 0,
1224 has_old_attachments: false,
1225 creation_date: cipher.creation_date,
1226 deleted_date: cipher.deleted_date,
1227 revision_date: cipher.revision_date,
1228 copyable_fields: vec![
1229 CopyableCipherFields::LoginUsername,
1230 CopyableCipherFields::LoginPassword,
1231 CopyableCipherFields::LoginTotp
1232 ],
1233 local_data: None,
1234 archived_date: cipher.archived_date,
1235 }
1236 )
1237 }
1238
1239 #[test]
1240 fn test_generate_cipher_key() {
1241 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1242 let key_store = create_test_crypto_with_user_key(key);
1243
1244 let original_cipher = generate_cipher();
1245
1246 let cipher = generate_cipher();
1248 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1249 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1250 assert!(no_key_cipher_dec.key.is_none());
1251 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1252
1253 let mut cipher = generate_cipher();
1254 cipher
1255 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1256 .unwrap();
1257
1258 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1260 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1261 assert!(key_cipher_dec.key.is_some());
1262 assert_eq!(key_cipher_dec.name, original_cipher.name);
1263 }
1264
1265 #[test]
1266 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1267 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1268 let key_store = create_test_crypto_with_user_key(key);
1269
1270 let mut original_cipher = generate_cipher();
1271 {
1272 let mut ctx = key_store.context();
1273 let cipher_key = ctx.generate_symmetric_key();
1274
1275 original_cipher.key = Some(
1276 ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1277 .unwrap(),
1278 );
1279 }
1280
1281 original_cipher
1282 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1283 .unwrap();
1284
1285 let wrapped_key = original_cipher.key.unwrap();
1287 let mut ctx = key_store.context();
1288 ctx.unwrap_symmetric_key(SymmetricKeyId::User, &wrapped_key)
1289 .unwrap();
1290 }
1291
1292 #[test]
1293 fn test_generate_cipher_key_ignores_attachments_without_key() {
1294 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1295 let key_store = create_test_crypto_with_user_key(key);
1296
1297 let mut cipher = generate_cipher();
1298 let attachment = AttachmentView {
1299 id: None,
1300 url: None,
1301 size: None,
1302 size_name: None,
1303 file_name: Some("Attachment test name".into()),
1304 key: None,
1305 #[cfg(feature = "wasm")]
1306 decrypted_key: None,
1307 };
1308 cipher.attachments = Some(vec![attachment]);
1309
1310 cipher
1311 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1312 .unwrap();
1313 assert!(cipher.attachments.unwrap()[0].key.is_none());
1314 }
1315
1316 #[test]
1317 fn test_reencrypt_cipher_key() {
1318 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1319 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1320 let key_store = create_test_crypto_with_user_key(old_key);
1321 let mut ctx = key_store.context_mut();
1322
1323 let mut cipher = generate_cipher();
1324 cipher
1325 .generate_cipher_key(&mut ctx, cipher.key_identifier())
1326 .unwrap();
1327
1328 let new_key_id = ctx.add_local_symmetric_key(new_key);
1330
1331 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
1332
1333 assert!(cipher.key.is_some());
1335 assert!(
1336 ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
1337 .is_ok()
1338 );
1339 }
1340
1341 #[test]
1342 fn test_reencrypt_cipher_key_ignores_missing_key() {
1343 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1344 let key_store = create_test_crypto_with_user_key(key);
1345 let mut ctx = key_store.context_mut();
1346 let mut cipher = generate_cipher();
1347
1348 let new_cipher_key = ctx.generate_symmetric_key();
1350 cipher
1351 .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
1352 .unwrap();
1353
1354 assert!(cipher.key.is_none());
1356 }
1357
1358 #[test]
1359 fn test_move_user_cipher_to_org() {
1360 let org = OrganizationId::new_v4();
1361 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1362 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1363 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1364
1365 let mut cipher = generate_cipher();
1367 cipher
1368 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1369 .unwrap();
1370
1371 cipher
1372 .move_to_organization(&mut key_store.context(), org)
1373 .unwrap();
1374 assert_eq!(cipher.organization_id, Some(org));
1375
1376 let cipher_enc = key_store.encrypt(cipher).unwrap();
1378 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1379
1380 assert_eq!(cipher_dec.name, "My test login");
1381 }
1382
1383 #[test]
1384 fn test_move_user_cipher_to_org_manually() {
1385 let org = OrganizationId::new_v4();
1386 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1387 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1388 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1389
1390 let mut cipher = generate_cipher();
1392 cipher
1393 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1394 .unwrap();
1395
1396 cipher.organization_id = Some(org);
1397
1398 assert!(key_store.encrypt(cipher).is_err());
1401 }
1402
1403 #[test]
1404 fn test_move_user_cipher_with_attachment_without_key_to_org() {
1405 let org = OrganizationId::new_v4();
1406 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1407 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1408 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1409
1410 let mut cipher = generate_cipher();
1411 let attachment = AttachmentView {
1412 id: None,
1413 url: None,
1414 size: None,
1415 size_name: None,
1416 file_name: Some("Attachment test name".into()),
1417 key: None,
1418 #[cfg(feature = "wasm")]
1419 decrypted_key: None,
1420 };
1421 cipher.attachments = Some(vec![attachment]);
1422
1423 assert!(
1425 cipher
1426 .move_to_organization(&mut key_store.context(), org)
1427 .is_err()
1428 );
1429 }
1430
1431 #[test]
1432 fn test_move_user_cipher_with_attachment_with_key_to_org() {
1433 let org = OrganizationId::new_v4();
1434 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1435 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1436 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1437 let org_key = SymmetricKeyId::Organization(org);
1438
1439 let (attachment_key_enc, attachment_key_val) = {
1441 let mut ctx = key_store.context();
1442 let attachment_key = ctx.generate_symmetric_key();
1443 let attachment_key_enc = ctx
1444 .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1445 .unwrap();
1446 #[allow(deprecated)]
1447 let attachment_key_val = ctx
1448 .dangerous_get_symmetric_key(attachment_key)
1449 .unwrap()
1450 .clone();
1451
1452 (attachment_key_enc, attachment_key_val)
1453 };
1454
1455 let mut cipher = generate_cipher();
1456 let attachment = AttachmentView {
1457 id: None,
1458 url: None,
1459 size: None,
1460 size_name: None,
1461 file_name: Some("Attachment test name".into()),
1462 key: Some(attachment_key_enc),
1463 #[cfg(feature = "wasm")]
1464 decrypted_key: None,
1465 };
1466 cipher.attachments = Some(vec![attachment]);
1467 let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1468 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1469
1470 cipher
1471 .move_to_organization(&mut key_store.context(), org)
1472 .unwrap();
1473
1474 assert!(cipher.key.is_none());
1475
1476 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1479 let mut ctx = key_store.context();
1480 let new_attachment_key_id = ctx
1481 .unwrap_symmetric_key(org_key, &new_attachment_key)
1482 .unwrap();
1483 #[allow(deprecated)]
1484 let new_attachment_key_dec = ctx
1485 .dangerous_get_symmetric_key(new_attachment_key_id)
1486 .unwrap();
1487
1488 assert_eq!(*new_attachment_key_dec, attachment_key_val);
1489
1490 let cred2: Fido2CredentialFullView = cipher
1491 .login
1492 .unwrap()
1493 .fido2_credentials
1494 .unwrap()
1495 .first()
1496 .unwrap()
1497 .decrypt(&mut key_store.context(), org_key)
1498 .unwrap();
1499
1500 assert_eq!(cred2.credential_id, "123");
1501 }
1502
1503 #[test]
1504 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1505 let org = OrganizationId::new_v4();
1506 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1507 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1508 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1509 let org_key = SymmetricKeyId::Organization(org);
1510
1511 let mut ctx = key_store.context();
1512
1513 let cipher_key = ctx.generate_symmetric_key();
1514 let cipher_key_enc = ctx
1515 .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1516 .unwrap();
1517
1518 let attachment_key = ctx.generate_symmetric_key();
1520 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1521
1522 let mut cipher = generate_cipher();
1523 cipher.key = Some(cipher_key_enc);
1524
1525 let attachment = AttachmentView {
1526 id: None,
1527 url: None,
1528 size: None,
1529 size_name: None,
1530 file_name: Some("Attachment test name".into()),
1531 key: Some(attachment_key_enc.clone()),
1532 #[cfg(feature = "wasm")]
1533 decrypted_key: None,
1534 };
1535 cipher.attachments = Some(vec![attachment]);
1536
1537 let cred = generate_fido2(&mut ctx, cipher_key);
1538 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1539
1540 cipher.move_to_organization(&mut ctx, org).unwrap();
1541
1542 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1544 let new_cipher_key_dec = ctx
1545 .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
1546 .unwrap();
1547 #[allow(deprecated)]
1548 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1549 #[allow(deprecated)]
1550 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1551
1552 assert_eq!(new_cipher_key_dec, cipher_key_val);
1553
1554 assert_eq!(
1556 cipher.attachments.unwrap()[0]
1557 .key
1558 .as_ref()
1559 .unwrap()
1560 .to_string(),
1561 attachment_key_enc.to_string()
1562 );
1563
1564 let cred2: Fido2Credential = cipher
1565 .login
1566 .unwrap()
1567 .fido2_credentials
1568 .unwrap()
1569 .first()
1570 .unwrap()
1571 .clone();
1572
1573 assert_eq!(
1574 cred2.credential_id.to_string(),
1575 cred.credential_id.to_string()
1576 );
1577 }
1578
1579 #[test]
1580 fn test_decrypt_fido2_private_key() {
1581 let key_store =
1582 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1583 let mut ctx = key_store.context();
1584
1585 let mut cipher_view = generate_cipher();
1586 cipher_view
1587 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1588 .unwrap();
1589
1590 let key_id = cipher_view.key_identifier();
1591 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1592
1593 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1594
1595 cipher_view.login.as_mut().unwrap().fido2_credentials =
1596 Some(vec![fido2_credential.clone()]);
1597
1598 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1599 assert_eq!(decrypted_key_value, "123");
1600 }
1601
1602 #[test]
1603 fn test_password_history_on_password_change() {
1604 use chrono::Utc;
1605
1606 let original_cipher = generate_cipher();
1607 let mut new_cipher = generate_cipher();
1608
1609 if let Some(ref mut login) = new_cipher.login {
1611 login.password = Some("new_password123".to_string());
1612 }
1613
1614 let start = Utc::now();
1615 new_cipher.update_password_history(&original_cipher);
1616 let end = Utc::now();
1617
1618 assert!(new_cipher.password_history.is_some());
1619 let history = new_cipher.password_history.unwrap();
1620 assert_eq!(history.len(), 1);
1621 assert_eq!(history[0].password, "test_password");
1622 assert!(
1623 history[0].last_used_date >= start && history[0].last_used_date <= end,
1624 "last_used_date was not set properly"
1625 );
1626 }
1627
1628 #[test]
1629 fn test_password_history_on_unchanged_password() {
1630 let original_cipher = generate_cipher();
1631 let mut new_cipher = generate_cipher();
1632
1633 new_cipher.update_password_history(&original_cipher);
1634
1635 assert!(
1637 new_cipher.password_history.is_none()
1638 || new_cipher.password_history.as_ref().unwrap().is_empty()
1639 );
1640 }
1641
1642 #[test]
1643 fn test_password_history_is_preserved() {
1644 use chrono::TimeZone;
1645
1646 let mut original_cipher = generate_cipher();
1647 original_cipher.password_history = Some(
1648 (0..4)
1649 .map(|i| PasswordHistoryView {
1650 password: format!("old_password_{}", i),
1651 last_used_date: chrono::Utc
1652 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
1653 .unwrap(),
1654 })
1655 .collect(),
1656 );
1657
1658 let mut new_cipher = generate_cipher();
1659
1660 new_cipher.update_password_history(&original_cipher);
1661
1662 assert!(new_cipher.password_history.is_some());
1663 let history = new_cipher.password_history.unwrap();
1664 assert_eq!(history.len(), 4);
1665
1666 assert_eq!(history[0].password, "old_password_0");
1667 assert_eq!(
1668 history[0].last_used_date,
1669 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
1670 );
1671 assert_eq!(history[1].password, "old_password_1");
1672 assert_eq!(
1673 history[1].last_used_date,
1674 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
1675 );
1676 assert_eq!(history[2].password, "old_password_2");
1677 assert_eq!(
1678 history[2].last_used_date,
1679 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
1680 );
1681 assert_eq!(history[3].password, "old_password_3");
1682 assert_eq!(
1683 history[3].last_used_date,
1684 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
1685 );
1686 }
1687
1688 #[test]
1689 fn test_populate_cipher_types_login_with_valid_data() {
1690 let mut cipher = Cipher {
1691 id: Some(TEST_UUID.parse().unwrap()),
1692 organization_id: None,
1693 folder_id: None,
1694 collection_ids: vec![],
1695 key: None,
1696 name: TEST_CIPHER_NAME.parse().unwrap(),
1697 notes: None,
1698 r#type: CipherType::Login,
1699 login: None,
1700 identity: None,
1701 card: None,
1702 secure_note: None,
1703 ssh_key: None,
1704 favorite: false,
1705 reprompt: CipherRepromptType::None,
1706 organization_use_totp: false,
1707 edit: true,
1708 view_password: true,
1709 permissions: None,
1710 local_data: None,
1711 attachments: None,
1712 fields: None,
1713 password_history: None,
1714 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1715 deleted_date: None,
1716 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1717 archived_date: None,
1718 data: Some(format!(
1719 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1720 TEST_ENC_STRING_1, TEST_ENC_STRING_2
1721 )),
1722 };
1723
1724 cipher
1725 .populate_cipher_types()
1726 .expect("populate_cipher_types failed");
1727
1728 assert!(cipher.login.is_some());
1729 let login = cipher.login.unwrap();
1730 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
1731 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
1732 }
1733
1734 #[test]
1735 fn test_populate_cipher_types_secure_note() {
1736 let mut cipher = Cipher {
1737 id: Some(TEST_UUID.parse().unwrap()),
1738 organization_id: None,
1739 folder_id: None,
1740 collection_ids: vec![],
1741 key: None,
1742 name: TEST_CIPHER_NAME.parse().unwrap(),
1743 notes: None,
1744 r#type: CipherType::SecureNote,
1745 login: None,
1746 identity: None,
1747 card: None,
1748 secure_note: None,
1749 ssh_key: None,
1750 favorite: false,
1751 reprompt: CipherRepromptType::None,
1752 organization_use_totp: false,
1753 edit: true,
1754 view_password: true,
1755 permissions: None,
1756 local_data: None,
1757 attachments: None,
1758 fields: None,
1759 password_history: None,
1760 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1761 deleted_date: None,
1762 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1763 archived_date: None,
1764 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
1765 };
1766
1767 cipher
1768 .populate_cipher_types()
1769 .expect("populate_cipher_types failed");
1770
1771 assert!(cipher.secure_note.is_some());
1772 }
1773
1774 #[test]
1775 fn test_populate_cipher_types_card() {
1776 let mut cipher = Cipher {
1777 id: Some(TEST_UUID.parse().unwrap()),
1778 organization_id: None,
1779 folder_id: None,
1780 collection_ids: vec![],
1781 key: None,
1782 name: TEST_CIPHER_NAME.parse().unwrap(),
1783 notes: None,
1784 r#type: CipherType::Card,
1785 login: None,
1786 identity: None,
1787 card: None,
1788 secure_note: None,
1789 ssh_key: None,
1790 favorite: false,
1791 reprompt: CipherRepromptType::None,
1792 organization_use_totp: false,
1793 edit: true,
1794 view_password: true,
1795 permissions: None,
1796 local_data: None,
1797 attachments: None,
1798 fields: None,
1799 password_history: None,
1800 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1801 deleted_date: None,
1802 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1803 archived_date: None,
1804 data: Some(format!(
1805 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1806 TEST_ENC_STRING_1,
1807 TEST_ENC_STRING_2,
1808 TEST_ENC_STRING_3,
1809 TEST_ENC_STRING_4,
1810 TEST_ENC_STRING_5,
1811 TEST_ENC_STRING_1
1812 )),
1813 };
1814
1815 cipher
1816 .populate_cipher_types()
1817 .expect("populate_cipher_types failed");
1818
1819 assert!(cipher.card.is_some());
1820 let card = cipher.card.unwrap();
1821 assert_eq!(
1822 card.cardholder_name.as_ref().unwrap().to_string(),
1823 TEST_ENC_STRING_1
1824 );
1825 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
1826 assert_eq!(
1827 card.exp_month.as_ref().unwrap().to_string(),
1828 TEST_ENC_STRING_3
1829 );
1830 assert_eq!(
1831 card.exp_year.as_ref().unwrap().to_string(),
1832 TEST_ENC_STRING_4
1833 );
1834 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
1835 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
1836 }
1837
1838 #[test]
1839 fn test_populate_cipher_types_identity() {
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::Identity,
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#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
1870 TEST_ENC_STRING_1,
1871 TEST_ENC_STRING_2,
1872 TEST_ENC_STRING_3,
1873 TEST_ENC_STRING_4,
1874 TEST_ENC_STRING_5,
1875 TEST_ENC_STRING_1,
1876 TEST_ENC_STRING_2,
1877 TEST_ENC_STRING_3,
1878 TEST_ENC_STRING_4,
1879 TEST_ENC_STRING_5
1880 )),
1881 };
1882
1883 cipher
1884 .populate_cipher_types()
1885 .expect("populate_cipher_types failed");
1886
1887 assert!(cipher.identity.is_some());
1888 let identity = cipher.identity.unwrap();
1889 assert_eq!(
1890 identity.first_name.as_ref().unwrap().to_string(),
1891 TEST_ENC_STRING_1
1892 );
1893 assert_eq!(
1894 identity.last_name.as_ref().unwrap().to_string(),
1895 TEST_ENC_STRING_2
1896 );
1897 assert_eq!(
1898 identity.email.as_ref().unwrap().to_string(),
1899 TEST_ENC_STRING_3
1900 );
1901 assert_eq!(
1902 identity.phone.as_ref().unwrap().to_string(),
1903 TEST_ENC_STRING_4
1904 );
1905 assert_eq!(
1906 identity.company.as_ref().unwrap().to_string(),
1907 TEST_ENC_STRING_5
1908 );
1909 assert_eq!(
1910 identity.address1.as_ref().unwrap().to_string(),
1911 TEST_ENC_STRING_1
1912 );
1913 assert_eq!(
1914 identity.city.as_ref().unwrap().to_string(),
1915 TEST_ENC_STRING_2
1916 );
1917 assert_eq!(
1918 identity.state.as_ref().unwrap().to_string(),
1919 TEST_ENC_STRING_3
1920 );
1921 assert_eq!(
1922 identity.postal_code.as_ref().unwrap().to_string(),
1923 TEST_ENC_STRING_4
1924 );
1925 assert_eq!(
1926 identity.country.as_ref().unwrap().to_string(),
1927 TEST_ENC_STRING_5
1928 );
1929 }
1930
1931 #[test]
1932
1933 fn test_password_history_with_hidden_fields() {
1934 let mut original_cipher = generate_cipher();
1935 original_cipher.fields = Some(vec![FieldView {
1936 name: Some("Secret Key".to_string()),
1937 value: Some("old_secret_value".to_string()),
1938 r#type: crate::FieldType::Hidden,
1939 linked_id: None,
1940 }]);
1941
1942 let mut new_cipher = generate_cipher();
1943 new_cipher.fields = Some(vec![FieldView {
1944 name: Some("Secret Key".to_string()),
1945 value: Some("new_secret_value".to_string()),
1946 r#type: crate::FieldType::Hidden,
1947 linked_id: None,
1948 }]);
1949
1950 new_cipher.update_password_history(&original_cipher);
1951
1952 assert!(new_cipher.password_history.is_some());
1953 let history = new_cipher.password_history.unwrap();
1954 assert_eq!(history.len(), 1);
1955 assert_eq!(history[0].password, "Secret Key: old_secret_value");
1956 }
1957
1958 #[test]
1959 fn test_password_history_length_limit() {
1960 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
1961
1962 let mut original_cipher = generate_cipher();
1963 original_cipher.password_history = Some(
1964 (0..10)
1965 .map(|i| PasswordHistoryView {
1966 password: format!("old_password_{}", i),
1967 last_used_date: chrono::Utc::now(),
1968 })
1969 .collect(),
1970 );
1971
1972 let mut new_cipher = original_cipher.clone();
1973 if let Some(ref mut login) = new_cipher.login {
1975 login.password = Some("brand_new_password".to_string());
1976 }
1977
1978 new_cipher.update_password_history(&original_cipher);
1979
1980 assert!(new_cipher.password_history.is_some());
1981 let history = new_cipher.password_history.unwrap();
1982
1983 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
1985
1986 assert_eq!(history[0].password, "test_password");
1988 assert_eq!(history[1].password, "old_password_0");
1990 assert_eq!(history[2].password, "old_password_1");
1991 assert_eq!(history[3].password, "old_password_2");
1992 assert_eq!(history[4].password, "old_password_3");
1993 }
1994
1995 #[test]
1996 fn test_populate_cipher_types_ssh_key() {
1997 let mut cipher = Cipher {
1998 id: Some(TEST_UUID.parse().unwrap()),
1999 organization_id: None,
2000 folder_id: None,
2001 collection_ids: vec![],
2002 key: None,
2003 name: TEST_CIPHER_NAME.parse().unwrap(),
2004 notes: None,
2005 r#type: CipherType::SshKey,
2006 login: None,
2007 identity: None,
2008 card: None,
2009 secure_note: None,
2010 ssh_key: None,
2011 favorite: false,
2012 reprompt: CipherRepromptType::None,
2013 organization_use_totp: false,
2014 edit: true,
2015 view_password: true,
2016 permissions: None,
2017 local_data: None,
2018 attachments: None,
2019 fields: None,
2020 password_history: None,
2021 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2022 deleted_date: None,
2023 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2024 archived_date: None,
2025 data: Some(format!(
2026 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2027 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2028 )),
2029 };
2030
2031 cipher
2032 .populate_cipher_types()
2033 .expect("populate_cipher_types failed");
2034
2035 assert!(cipher.ssh_key.is_some());
2036 let ssh_key = cipher.ssh_key.unwrap();
2037 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2038 assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2039 assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2040 }
2041
2042 #[test]
2043 fn test_populate_cipher_types_with_null_data() {
2044 let mut cipher = Cipher {
2045 id: Some(TEST_UUID.parse().unwrap()),
2046 organization_id: None,
2047 folder_id: None,
2048 collection_ids: vec![],
2049 key: None,
2050 name: TEST_CIPHER_NAME.parse().unwrap(),
2051 notes: None,
2052 r#type: CipherType::Login,
2053 login: None,
2054 identity: None,
2055 card: None,
2056 secure_note: None,
2057 ssh_key: None,
2058 favorite: false,
2059 reprompt: CipherRepromptType::None,
2060 organization_use_totp: false,
2061 edit: true,
2062 view_password: true,
2063 permissions: None,
2064 local_data: None,
2065 attachments: None,
2066 fields: None,
2067 password_history: None,
2068 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2069 deleted_date: None,
2070 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2071 archived_date: None,
2072 data: None,
2073 };
2074
2075 let result = cipher.populate_cipher_types();
2076 assert!(matches!(
2077 result,
2078 Err(VaultParseError::MissingField(MissingFieldError("data")))
2079 ));
2080 }
2081
2082 #[test]
2083 fn test_populate_cipher_types_with_invalid_json() {
2084 let mut cipher = Cipher {
2085 id: Some(TEST_UUID.parse().unwrap()),
2086 organization_id: None,
2087 folder_id: None,
2088 collection_ids: vec![],
2089 key: None,
2090 name: TEST_CIPHER_NAME.parse().unwrap(),
2091 notes: None,
2092 r#type: CipherType::Login,
2093 login: None,
2094 identity: None,
2095 card: None,
2096 secure_note: None,
2097 ssh_key: None,
2098 favorite: false,
2099 reprompt: CipherRepromptType::None,
2100 organization_use_totp: false,
2101 edit: true,
2102 view_password: true,
2103 permissions: None,
2104 local_data: None,
2105 attachments: None,
2106 fields: None,
2107 password_history: None,
2108 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2109 deleted_date: None,
2110 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2111 archived_date: None,
2112 data: Some("invalid json".to_string()),
2113 };
2114
2115 let result = cipher.populate_cipher_types();
2116
2117 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2118 }
2119}