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