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