1use bitwarden_api_api::models::CipherDetailsResponseModel;
2use bitwarden_collections::collection::CollectionId;
3use bitwarden_core::{
4 key_management::{KeyIds, SymmetricKeyId},
5 require, MissingFieldError, OrganizationId, UserId, VaultLockedError,
6};
7use bitwarden_crypto::{
8 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
9 PrimitiveEncryptable,
10};
11use bitwarden_error::bitwarden_error;
12use bitwarden_uuid::uuid_newtype;
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use serde_repr::{Deserialize_repr, Serialize_repr};
16use thiserror::Error;
17#[cfg(feature = "wasm")]
18use tsify::Tsify;
19#[cfg(feature = "wasm")]
20use wasm_bindgen::prelude::wasm_bindgen;
21
22use super::{
23 attachment, card,
24 card::CardListView,
25 cipher_permissions::CipherPermissions,
26 field, identity,
27 local_data::{LocalData, LocalDataView},
28 login::LoginListView,
29 secure_note, ssh_key,
30};
31use crate::{
32 password_history, EncryptError, Fido2CredentialFullView, Fido2CredentialView, FolderId, Login,
33 LoginView, VaultParseError,
34};
35
36uuid_newtype!(pub CipherId);
37
38#[allow(missing_docs)]
39#[bitwarden_error(flat)]
40#[derive(Debug, Error)]
41pub enum CipherError {
42 #[error(transparent)]
43 MissingFieldError(#[from] MissingFieldError),
44 #[error(transparent)]
45 VaultLocked(#[from] VaultLockedError),
46 #[error(transparent)]
47 CryptoError(#[from] CryptoError),
48 #[error(transparent)]
49 EncryptError(#[from] EncryptError),
50 #[error("This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation")]
51 AttachmentsWithoutKeys,
52}
53
54pub(super) trait CipherKind {
56 fn decrypt_subtitle(
58 &self,
59 ctx: &mut KeyStoreContext<KeyIds>,
60 key: SymmetricKeyId,
61 ) -> Result<String, CryptoError>;
62
63 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
65}
66
67#[allow(missing_docs)]
68#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
69#[repr(u8)]
70#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
71#[cfg_attr(feature = "wasm", wasm_bindgen)]
72pub enum CipherType {
73 Login = 1,
74 SecureNote = 2,
75 Card = 3,
76 Identity = 4,
77 SshKey = 5,
78}
79
80#[allow(missing_docs)]
81#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
82#[repr(u8)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
84#[cfg_attr(feature = "wasm", wasm_bindgen)]
85pub enum CipherRepromptType {
86 None = 0,
87 Password = 1,
88}
89
90#[allow(missing_docs)]
91#[derive(Serialize, Deserialize, Debug, Clone)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
94#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
95pub struct EncryptionContext {
96 pub encrypted_for: UserId,
99 pub cipher: Cipher,
100}
101
102#[allow(missing_docs)]
103#[derive(Serialize, Deserialize, Debug, Clone)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
106#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
107pub struct Cipher {
108 pub id: Option<CipherId>,
109 pub organization_id: Option<OrganizationId>,
110 pub folder_id: Option<FolderId>,
111 pub collection_ids: Vec<CollectionId>,
112
113 pub key: Option<EncString>,
116
117 pub name: EncString,
118 pub notes: Option<EncString>,
119
120 pub r#type: CipherType,
121 pub login: Option<Login>,
122 pub identity: Option<identity::Identity>,
123 pub card: Option<card::Card>,
124 pub secure_note: Option<secure_note::SecureNote>,
125 pub ssh_key: Option<ssh_key::SshKey>,
126
127 pub favorite: bool,
128 pub reprompt: CipherRepromptType,
129 pub organization_use_totp: bool,
130 pub edit: bool,
131 pub permissions: Option<CipherPermissions>,
132 pub view_password: bool,
133 pub local_data: Option<LocalData>,
134
135 pub attachments: Option<Vec<attachment::Attachment>>,
136 pub fields: Option<Vec<field::Field>>,
137 pub password_history: Option<Vec<password_history::PasswordHistory>>,
138
139 pub creation_date: DateTime<Utc>,
140 pub deleted_date: Option<DateTime<Utc>>,
141 pub revision_date: DateTime<Utc>,
142}
143
144bitwarden_state::register_repository_item!(Cipher, "Cipher");
145
146#[allow(missing_docs)]
147#[derive(Serialize, Deserialize, Debug, Clone)]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
150#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
151pub struct CipherView {
152 pub id: Option<CipherId>,
153 pub organization_id: Option<OrganizationId>,
154 pub folder_id: Option<FolderId>,
155 pub collection_ids: Vec<CollectionId>,
156
157 pub key: Option<EncString>,
159
160 pub name: String,
161 pub notes: Option<String>,
162
163 pub r#type: CipherType,
164 pub login: Option<LoginView>,
165 pub identity: Option<identity::IdentityView>,
166 pub card: Option<card::CardView>,
167 pub secure_note: Option<secure_note::SecureNoteView>,
168 pub ssh_key: Option<ssh_key::SshKeyView>,
169
170 pub favorite: bool,
171 pub reprompt: CipherRepromptType,
172 pub organization_use_totp: bool,
173 pub edit: bool,
174 pub permissions: Option<CipherPermissions>,
175 pub view_password: bool,
176 pub local_data: Option<LocalDataView>,
177
178 pub attachments: Option<Vec<attachment::AttachmentView>>,
179 pub fields: Option<Vec<field::FieldView>>,
180 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
181
182 pub creation_date: DateTime<Utc>,
183 pub deleted_date: Option<DateTime<Utc>>,
184 pub revision_date: DateTime<Utc>,
185}
186
187#[allow(missing_docs)]
188#[derive(Serialize, Deserialize, Debug, PartialEq)]
189#[serde(rename_all = "camelCase", deny_unknown_fields)]
190#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
191#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
192pub enum CipherListViewType {
193 Login(LoginListView),
194 SecureNote,
195 Card(CardListView),
196 Identity,
197 SshKey,
198}
199
200#[derive(Serialize, Deserialize, Debug, PartialEq)]
202#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
203#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
204pub enum CopyableCipherFields {
205 LoginUsername,
206 LoginPassword,
207 LoginTotp,
208 CardNumber,
209 CardSecurityCode,
210 IdentityUsername,
211 IdentityEmail,
212 IdentityPhone,
213 IdentityAddress,
214 SshKey,
215 SecureNotes,
216}
217
218#[allow(missing_docs)]
219#[derive(Serialize, Deserialize, Debug, PartialEq)]
220#[serde(rename_all = "camelCase", deny_unknown_fields)]
221#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
222#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
223pub struct CipherListView {
224 pub id: Option<CipherId>,
225 pub organization_id: Option<OrganizationId>,
226 pub folder_id: Option<FolderId>,
227 pub collection_ids: Vec<CollectionId>,
228
229 pub key: Option<EncString>,
231
232 pub name: String,
233 pub subtitle: String,
234
235 pub r#type: CipherListViewType,
236
237 pub favorite: bool,
238 pub reprompt: CipherRepromptType,
239 pub organization_use_totp: bool,
240 pub edit: bool,
241 pub permissions: Option<CipherPermissions>,
242
243 pub view_password: bool,
244
245 pub attachments: u32,
247 pub has_old_attachments: bool,
249
250 pub creation_date: DateTime<Utc>,
251 pub deleted_date: Option<DateTime<Utc>>,
252 pub revision_date: DateTime<Utc>,
253
254 pub copyable_fields: Vec<CopyableCipherFields>,
256
257 pub local_data: Option<LocalDataView>,
258}
259
260#[derive(Serialize, Deserialize, Debug)]
266#[serde(rename_all = "camelCase", deny_unknown_fields)]
267#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
268#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
269pub struct DecryptCipherListResult {
270 pub successes: Vec<CipherListView>,
272 pub failures: Vec<Cipher>,
274}
275
276impl CipherListView {
277 pub(crate) fn get_totp_key(
278 self,
279 ctx: &mut KeyStoreContext<KeyIds>,
280 ) -> Result<Option<String>, CryptoError> {
281 let key = self.key_identifier();
282 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
283
284 let totp = match self.r#type {
285 CipherListViewType::Login(LoginListView { totp, .. }) => {
286 totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
287 }
288 _ => None,
289 };
290
291 Ok(totp)
292 }
293}
294
295impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
296 fn encrypt_composite(
297 &self,
298 ctx: &mut KeyStoreContext<KeyIds>,
299 key: SymmetricKeyId,
300 ) -> Result<Cipher, CryptoError> {
301 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
302
303 let mut cipher_view = self.clone();
304 cipher_view.generate_checksums();
305
306 Ok(Cipher {
307 id: cipher_view.id,
308 organization_id: cipher_view.organization_id,
309 folder_id: cipher_view.folder_id,
310 collection_ids: cipher_view.collection_ids,
311 key: cipher_view.key,
312 name: cipher_view.name.encrypt(ctx, ciphers_key)?,
313 notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
314 r#type: cipher_view.r#type,
315 login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
316 identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
317 card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
318 secure_note: cipher_view
319 .secure_note
320 .encrypt_composite(ctx, ciphers_key)?,
321 ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
322 favorite: cipher_view.favorite,
323 reprompt: cipher_view.reprompt,
324 organization_use_totp: cipher_view.organization_use_totp,
325 edit: cipher_view.edit,
326 view_password: cipher_view.view_password,
327 local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
328 attachments: cipher_view
329 .attachments
330 .encrypt_composite(ctx, ciphers_key)?,
331 fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
332 password_history: cipher_view
333 .password_history
334 .encrypt_composite(ctx, ciphers_key)?,
335 creation_date: cipher_view.creation_date,
336 deleted_date: cipher_view.deleted_date,
337 revision_date: cipher_view.revision_date,
338 permissions: cipher_view.permissions,
339 })
340 }
341}
342
343impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
344 fn decrypt(
345 &self,
346 ctx: &mut KeyStoreContext<KeyIds>,
347 key: SymmetricKeyId,
348 ) -> Result<CipherView, CryptoError> {
349 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
350
351 let mut cipher = CipherView {
352 id: self.id,
353 organization_id: self.organization_id,
354 folder_id: self.folder_id,
355 collection_ids: self.collection_ids.clone(),
356 key: self.key.clone(),
357 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
358 notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
359 r#type: self.r#type,
360 login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
361 identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
362 card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
363 secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
364 ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
365 favorite: self.favorite,
366 reprompt: self.reprompt,
367 organization_use_totp: self.organization_use_totp,
368 edit: self.edit,
369 permissions: self.permissions,
370 view_password: self.view_password,
371 local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
372 attachments: self.attachments.decrypt(ctx, ciphers_key).ok().flatten(),
373 fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
374 password_history: self
375 .password_history
376 .decrypt(ctx, ciphers_key)
377 .ok()
378 .flatten(),
379 creation_date: self.creation_date,
380 deleted_date: self.deleted_date,
381 revision_date: self.revision_date,
382 };
383
384 if cipher.key.is_some() {
386 cipher.remove_invalid_checksums();
387 }
388
389 Ok(cipher)
390 }
391}
392
393impl Cipher {
394 pub(super) fn decrypt_cipher_key(
405 ctx: &mut KeyStoreContext<KeyIds>,
406 key: SymmetricKeyId,
407 ciphers_key: &Option<EncString>,
408 ) -> Result<SymmetricKeyId, CryptoError> {
409 const CIPHER_KEY: SymmetricKeyId = SymmetricKeyId::Local("cipher_key");
410 match ciphers_key {
411 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, CIPHER_KEY, ciphers_key),
412 None => Ok(key),
413 }
414 }
415
416 fn get_kind(&self) -> Option<&dyn CipherKind> {
418 match self.r#type {
419 CipherType::Login => self.login.as_ref().map(|v| v as _),
420 CipherType::Card => self.card.as_ref().map(|v| v as _),
421 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
422 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
423 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
424 }
425 }
426
427 fn decrypt_subtitle(
429 &self,
430 ctx: &mut KeyStoreContext<KeyIds>,
431 key: SymmetricKeyId,
432 ) -> Result<String, CryptoError> {
433 self.get_kind()
434 .map(|sub| sub.decrypt_subtitle(ctx, key))
435 .unwrap_or_else(|| Ok(String::new()))
436 }
437
438 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
441 self.get_kind()
442 .map(|kind| kind.get_copyable_fields(Some(self)))
443 .unwrap_or_default()
444 }
445}
446
447impl CipherView {
448 #[allow(missing_docs)]
449 pub fn generate_cipher_key(
450 &mut self,
451 ctx: &mut KeyStoreContext<KeyIds>,
452 key: SymmetricKeyId,
453 ) -> Result<(), CryptoError> {
454 let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
455
456 const NEW_KEY: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key");
457
458 let new_key = ctx.generate_symmetric_key(NEW_KEY)?;
459
460 self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
461 self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
462
463 self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
464 Ok(())
465 }
466
467 #[allow(missing_docs)]
468 pub fn generate_checksums(&mut self) {
469 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
470 for uri in uris {
471 uri.generate_checksum();
472 }
473 }
474 }
475
476 #[allow(missing_docs)]
477 pub fn remove_invalid_checksums(&mut self) {
478 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
479 uris.retain(|u| u.is_checksum_valid());
480 }
481 }
482
483 fn reencrypt_attachment_keys(
484 &mut self,
485 ctx: &mut KeyStoreContext<KeyIds>,
486 old_key: SymmetricKeyId,
487 new_key: SymmetricKeyId,
488 ) -> Result<(), CryptoError> {
489 if let Some(attachments) = &mut self.attachments {
490 for attachment in attachments {
491 if let Some(attachment_key) = &mut attachment.key {
492 let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key");
493 ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?;
494 *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?;
495 }
496 }
497 }
498 Ok(())
499 }
500
501 #[allow(missing_docs)]
502 pub fn decrypt_fido2_credentials(
503 &self,
504 ctx: &mut KeyStoreContext<KeyIds>,
505 ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
506 let key = self.key_identifier();
507 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
508
509 Ok(self
510 .login
511 .as_ref()
512 .and_then(|l| l.fido2_credentials.as_ref())
513 .map(|f| f.decrypt(ctx, ciphers_key))
514 .transpose()?
515 .unwrap_or_default())
516 }
517
518 fn reencrypt_fido2_credentials(
519 &mut self,
520 ctx: &mut KeyStoreContext<KeyIds>,
521 old_key: SymmetricKeyId,
522 new_key: SymmetricKeyId,
523 ) -> Result<(), CryptoError> {
524 if let Some(login) = self.login.as_mut() {
525 if let Some(fido2_credentials) = &mut login.fido2_credentials {
526 let dec_fido2_credentials: Vec<Fido2CredentialFullView> =
527 fido2_credentials.decrypt(ctx, old_key)?;
528 *fido2_credentials = dec_fido2_credentials.encrypt_composite(ctx, new_key)?;
529 }
530 }
531 Ok(())
532 }
533
534 pub fn move_to_organization(
541 &mut self,
542 ctx: &mut KeyStoreContext<KeyIds>,
543 organization_id: OrganizationId,
544 ) -> Result<(), CipherError> {
545 let new_key = SymmetricKeyId::Organization(organization_id);
546
547 self.reencrypt_cipher_keys(ctx, new_key)?;
548 self.organization_id = Some(organization_id);
549
550 Ok(())
551 }
552
553 pub fn reencrypt_cipher_keys(
558 &mut self,
559 ctx: &mut KeyStoreContext<KeyIds>,
560 new_wrapping_key: SymmetricKeyId,
561 ) -> Result<(), CipherError> {
562 let old_key = self.key_identifier();
563
564 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
566 return Err(CipherError::AttachmentsWithoutKeys);
567 }
568
569 if self.key.is_some() {
571 let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
573
574 self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
576 } else {
577 self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
580 self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
581 }
582
583 Ok(())
584 }
585
586 #[allow(missing_docs)]
587 pub fn set_new_fido2_credentials(
588 &mut self,
589 ctx: &mut KeyStoreContext<KeyIds>,
590 creds: Vec<Fido2CredentialFullView>,
591 ) -> Result<(), CipherError> {
592 let key = self.key_identifier();
593
594 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
595
596 require!(self.login.as_mut()).fido2_credentials =
597 Some(creds.encrypt_composite(ctx, ciphers_key)?);
598
599 Ok(())
600 }
601
602 #[allow(missing_docs)]
603 pub fn get_fido2_credentials(
604 &self,
605 ctx: &mut KeyStoreContext<KeyIds>,
606 ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
607 let key = self.key_identifier();
608
609 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
610
611 let login = require!(self.login.as_ref());
612 let creds = require!(login.fido2_credentials.as_ref());
613 let res = creds.decrypt(ctx, ciphers_key)?;
614 Ok(res)
615 }
616
617 #[allow(missing_docs)]
618 pub fn decrypt_fido2_private_key(
619 &self,
620 ctx: &mut KeyStoreContext<KeyIds>,
621 ) -> Result<String, CipherError> {
622 let fido2_credential = self.get_fido2_credentials(ctx)?;
623
624 Ok(fido2_credential[0].key_value.clone())
625 }
626}
627
628impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
629 fn decrypt(
630 &self,
631 ctx: &mut KeyStoreContext<KeyIds>,
632 key: SymmetricKeyId,
633 ) -> Result<CipherListView, CryptoError> {
634 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
635
636 Ok(CipherListView {
637 id: self.id,
638 organization_id: self.organization_id,
639 folder_id: self.folder_id,
640 collection_ids: self.collection_ids.clone(),
641 key: self.key.clone(),
642 name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
643 subtitle: self
644 .decrypt_subtitle(ctx, ciphers_key)
645 .ok()
646 .unwrap_or_default(),
647 r#type: match self.r#type {
648 CipherType::Login => {
649 let login = self
650 .login
651 .as_ref()
652 .ok_or(CryptoError::MissingField("login"))?;
653 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
654 }
655 CipherType::SecureNote => CipherListViewType::SecureNote,
656 CipherType::Card => {
657 let card = self
658 .card
659 .as_ref()
660 .ok_or(CryptoError::MissingField("card"))?;
661 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
662 }
663 CipherType::Identity => CipherListViewType::Identity,
664 CipherType::SshKey => CipherListViewType::SshKey,
665 },
666 favorite: self.favorite,
667 reprompt: self.reprompt,
668 organization_use_totp: self.organization_use_totp,
669 edit: self.edit,
670 permissions: self.permissions,
671 view_password: self.view_password,
672 attachments: self
673 .attachments
674 .as_ref()
675 .map(|a| a.len() as u32)
676 .unwrap_or(0),
677 has_old_attachments: self
678 .attachments
679 .as_ref()
680 .map(|a| a.iter().any(|att| att.key.is_none()))
681 .unwrap_or(false),
682 creation_date: self.creation_date,
683 deleted_date: self.deleted_date,
684 revision_date: self.revision_date,
685 copyable_fields: self.get_copyable_fields(),
686 local_data: self.local_data.decrypt(ctx, ciphers_key)?,
687 })
688 }
689}
690
691impl IdentifyKey<SymmetricKeyId> for Cipher {
692 fn key_identifier(&self) -> SymmetricKeyId {
693 match self.organization_id {
694 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
695 None => SymmetricKeyId::User,
696 }
697 }
698}
699
700impl IdentifyKey<SymmetricKeyId> for CipherView {
701 fn key_identifier(&self) -> SymmetricKeyId {
702 match self.organization_id {
703 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
704 None => SymmetricKeyId::User,
705 }
706 }
707}
708
709impl IdentifyKey<SymmetricKeyId> for CipherListView {
710 fn key_identifier(&self) -> SymmetricKeyId {
711 match self.organization_id {
712 Some(organization_id) => SymmetricKeyId::Organization(organization_id),
713 None => SymmetricKeyId::User,
714 }
715 }
716}
717
718impl TryFrom<CipherDetailsResponseModel> for Cipher {
719 type Error = VaultParseError;
720
721 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
722 Ok(Self {
723 id: cipher.id.map(CipherId::new),
724 organization_id: cipher.organization_id.map(OrganizationId::new),
725 folder_id: cipher.folder_id.map(FolderId::new),
726 collection_ids: cipher
727 .collection_ids
728 .unwrap_or_default()
729 .into_iter()
730 .map(CollectionId::new)
731 .collect(),
732 name: require!(EncString::try_from_optional(cipher.name)?),
733 notes: EncString::try_from_optional(cipher.notes)?,
734 r#type: require!(cipher.r#type).into(),
735 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
736 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
737 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
738 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
739 ssh_key: None,
741 favorite: cipher.favorite.unwrap_or(false),
742 reprompt: cipher
743 .reprompt
744 .map(|r| r.into())
745 .unwrap_or(CipherRepromptType::None),
746 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
747 edit: cipher.edit.unwrap_or(true),
748 permissions: None,
750 view_password: cipher.view_password.unwrap_or(true),
751 local_data: None, attachments: cipher
753 .attachments
754 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
755 .transpose()?,
756 fields: cipher
757 .fields
758 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
759 .transpose()?,
760 password_history: cipher
761 .password_history
762 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
763 .transpose()?,
764 creation_date: require!(cipher.creation_date).parse()?,
765 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
766 revision_date: require!(cipher.revision_date).parse()?,
767 key: EncString::try_from_optional(cipher.key)?,
768 })
769 }
770}
771
772impl From<bitwarden_api_api::models::CipherType> for CipherType {
773 fn from(t: bitwarden_api_api::models::CipherType) -> Self {
774 match t {
775 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
776 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
777 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
778 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
779 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
780 }
781 }
782}
783
784impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
785 fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
786 match t {
787 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
788 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
789 }
790 }
791}
792
793#[cfg(test)]
794mod tests {
795
796 use attachment::AttachmentView;
797 use bitwarden_core::key_management::{
798 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
799 };
800 use bitwarden_crypto::SymmetricCryptoKey;
801
802 use super::*;
803 use crate::{login::Fido2CredentialListView, Fido2Credential};
804
805 fn generate_cipher() -> CipherView {
806 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
807 CipherView {
808 r#type: CipherType::Login,
809 login: Some(LoginView {
810 username: Some("test_username".to_string()),
811 password: Some("test_password".to_string()),
812 password_revision_date: None,
813 uris: None,
814 totp: None,
815 autofill_on_page_load: None,
816 fido2_credentials: None,
817 }),
818 id: Some(test_id),
819 organization_id: None,
820 folder_id: None,
821 collection_ids: vec![],
822 key: None,
823 name: "My test login".to_string(),
824 notes: None,
825 identity: None,
826 card: None,
827 secure_note: None,
828 ssh_key: None,
829 favorite: false,
830 reprompt: CipherRepromptType::None,
831 organization_use_totp: true,
832 edit: true,
833 permissions: None,
834 view_password: true,
835 local_data: None,
836 attachments: None,
837 fields: None,
838 password_history: None,
839 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
840 deleted_date: None,
841 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
842 }
843 }
844
845 fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
846 Fido2Credential {
847 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
848 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
849 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
850 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
851 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
852 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
853 user_handle: None,
854 user_name: None,
855 counter: "123".to_string().encrypt(ctx, key).unwrap(),
856 rp_name: None,
857 user_display_name: None,
858 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
859 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
860 }
861 }
862
863 #[test]
864 fn test_decrypt_cipher_list_view() {
865 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
866 let key_store = create_test_crypto_with_user_key(key);
867
868 let cipher = Cipher {
869 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
870 organization_id: None,
871 folder_id: None,
872 collection_ids: vec![],
873 key: None,
874 name: "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=".parse().unwrap(),
875 notes: None,
876 r#type: CipherType::Login,
877 login: Some(Login {
878 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
879 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
880 password_revision_date: None,
881 uris: None,
882 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
883 autofill_on_page_load: None,
884 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
885 }),
886 identity: None,
887 card: None,
888 secure_note: None,
889 ssh_key: None,
890 favorite: false,
891 reprompt: CipherRepromptType::None,
892 organization_use_totp: false,
893 edit: true,
894 permissions: Some(CipherPermissions {
895 delete: false,
896 restore: false
897 }),
898 view_password: true,
899 local_data: None,
900 attachments: None,
901 fields: None,
902 password_history: None,
903 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
904 deleted_date: None,
905 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
906 };
907
908 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
909
910 assert_eq!(
911 view,
912 CipherListView {
913 id: cipher.id,
914 organization_id: cipher.organization_id,
915 folder_id: cipher.folder_id,
916 collection_ids: cipher.collection_ids,
917 key: cipher.key,
918 name: "My test login".to_string(),
919 subtitle: "test_username".to_string(),
920 r#type: CipherListViewType::Login(LoginListView {
921 fido2_credentials: Some(vec![Fido2CredentialListView {
922 credential_id: "123".to_string(),
923 rp_id: "123".to_string(),
924 user_handle: None,
925 user_name: None,
926 user_display_name: None,
927 }]),
928 has_fido2: true,
929 username: Some("test_username".to_string()),
930 totp: cipher.login.as_ref().unwrap().totp.clone(),
931 uris: None,
932 }),
933 favorite: cipher.favorite,
934 reprompt: cipher.reprompt,
935 organization_use_totp: cipher.organization_use_totp,
936 edit: cipher.edit,
937 permissions: cipher.permissions,
938 view_password: cipher.view_password,
939 attachments: 0,
940 has_old_attachments: false,
941 creation_date: cipher.creation_date,
942 deleted_date: cipher.deleted_date,
943 revision_date: cipher.revision_date,
944 copyable_fields: vec![
945 CopyableCipherFields::LoginUsername,
946 CopyableCipherFields::LoginPassword,
947 CopyableCipherFields::LoginTotp
948 ],
949 local_data: None,
950 }
951 )
952 }
953
954 #[test]
955 fn test_generate_cipher_key() {
956 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
957 let key_store = create_test_crypto_with_user_key(key);
958
959 let original_cipher = generate_cipher();
960
961 let cipher = generate_cipher();
963 let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
964 let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
965 assert!(no_key_cipher_dec.key.is_none());
966 assert_eq!(no_key_cipher_dec.name, original_cipher.name);
967
968 let mut cipher = generate_cipher();
969 cipher
970 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
971 .unwrap();
972
973 let key_cipher_enc = key_store.encrypt(cipher).unwrap();
975 let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
976 assert!(key_cipher_dec.key.is_some());
977 assert_eq!(key_cipher_dec.name, original_cipher.name);
978 }
979
980 #[test]
981 fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
982 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
983 let key_store = create_test_crypto_with_user_key(key);
984
985 let mut original_cipher = generate_cipher();
986 {
987 const CIPHER_KEY: SymmetricKeyId = SymmetricKeyId::Local("test_cipher_key");
988 let mut ctx = key_store.context();
989 let cipher_key = ctx.generate_symmetric_key(CIPHER_KEY).unwrap();
990
991 original_cipher.key = Some(
992 ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
993 .unwrap(),
994 );
995 }
996
997 original_cipher
998 .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
999 .unwrap();
1000
1001 let wrapped_key = original_cipher.key.unwrap();
1003 let mut ctx = key_store.context();
1004 ctx.unwrap_symmetric_key(
1005 SymmetricKeyId::User,
1006 SymmetricKeyId::Local("test_cipher_key"),
1007 &wrapped_key,
1008 )
1009 .unwrap();
1010 }
1011
1012 #[test]
1013 fn test_generate_cipher_key_ignores_attachments_without_key() {
1014 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1015 let key_store = create_test_crypto_with_user_key(key);
1016
1017 let mut cipher = generate_cipher();
1018 let attachment = AttachmentView {
1019 id: None,
1020 url: None,
1021 size: None,
1022 size_name: None,
1023 file_name: Some("Attachment test name".into()),
1024 key: None,
1025 decrypted_key: None,
1026 };
1027 cipher.attachments = Some(vec![attachment]);
1028
1029 cipher
1030 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1031 .unwrap();
1032 assert!(cipher.attachments.unwrap()[0].key.is_none());
1033 }
1034
1035 #[test]
1036 fn test_reencrypt_cipher_key() {
1037 let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1038 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1039 let key_store = create_test_crypto_with_user_key(old_key);
1040 let mut ctx = key_store.context_mut();
1041
1042 let mut cipher = generate_cipher();
1043 cipher
1044 .generate_cipher_key(&mut ctx, cipher.key_identifier())
1045 .unwrap();
1046
1047 let new_key_id: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key");
1049 #[allow(deprecated)]
1050 ctx.set_symmetric_key(new_key_id, new_key).unwrap();
1051
1052 cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
1053
1054 assert!(cipher.key.is_some());
1056 assert!(ctx
1057 .unwrap_symmetric_key(new_key_id, new_key_id, &cipher.key.unwrap())
1058 .is_ok());
1059 }
1060
1061 #[test]
1062 fn test_reencrypt_cipher_key_ignores_missing_key() {
1063 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1064 let key_store = create_test_crypto_with_user_key(key);
1065 let mut ctx = key_store.context_mut();
1066 let mut cipher = generate_cipher();
1067
1068 cipher
1070 .reencrypt_cipher_keys(&mut ctx, SymmetricKeyId::Local("new_cipher_key"))
1071 .unwrap();
1072
1073 assert!(cipher.key.is_none());
1075 }
1076
1077 #[test]
1078 fn test_move_user_cipher_to_org() {
1079 let org = OrganizationId::new_v4();
1080 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1081 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1082 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1083
1084 let mut cipher = generate_cipher();
1086 cipher
1087 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1088 .unwrap();
1089
1090 cipher
1091 .move_to_organization(&mut key_store.context(), org)
1092 .unwrap();
1093 assert_eq!(cipher.organization_id, Some(org));
1094
1095 let cipher_enc = key_store.encrypt(cipher).unwrap();
1097 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1098
1099 assert_eq!(cipher_dec.name, "My test login");
1100 }
1101
1102 #[test]
1103 fn test_move_user_cipher_to_org_manually() {
1104 let org = OrganizationId::new_v4();
1105 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1106 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1107 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1108
1109 let mut cipher = generate_cipher();
1111 cipher
1112 .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1113 .unwrap();
1114
1115 cipher.organization_id = Some(org);
1116
1117 assert!(key_store.encrypt(cipher).is_err());
1120 }
1121
1122 #[test]
1123 fn test_move_user_cipher_with_attachment_without_key_to_org() {
1124 let org = OrganizationId::new_v4();
1125 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1126 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1127 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1128
1129 let mut cipher = generate_cipher();
1130 let attachment = AttachmentView {
1131 id: None,
1132 url: None,
1133 size: None,
1134 size_name: None,
1135 file_name: Some("Attachment test name".into()),
1136 key: None,
1137 decrypted_key: None,
1138 };
1139 cipher.attachments = Some(vec![attachment]);
1140
1141 assert!(cipher
1143 .move_to_organization(&mut key_store.context(), org)
1144 .is_err());
1145 }
1146
1147 #[test]
1148 fn test_move_user_cipher_with_attachment_with_key_to_org() {
1149 let org = OrganizationId::new_v4();
1150 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1151 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1152 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1153 let org_key = SymmetricKeyId::Organization(org);
1154
1155 let (attachment_key_enc, attachment_key_val) = {
1157 let mut ctx = key_store.context();
1158 let attachment_key = ctx
1159 .generate_symmetric_key(SymmetricKeyId::Local("test_attachment_key"))
1160 .unwrap();
1161 let attachment_key_enc = ctx
1162 .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1163 .unwrap();
1164 #[allow(deprecated)]
1165 let attachment_key_val = ctx
1166 .dangerous_get_symmetric_key(attachment_key)
1167 .unwrap()
1168 .clone();
1169
1170 (attachment_key_enc, attachment_key_val)
1171 };
1172
1173 let mut cipher = generate_cipher();
1174 let attachment = AttachmentView {
1175 id: None,
1176 url: None,
1177 size: None,
1178 size_name: None,
1179 file_name: Some("Attachment test name".into()),
1180 key: Some(attachment_key_enc),
1181 decrypted_key: None,
1182 };
1183 cipher.attachments = Some(vec![attachment]);
1184 let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1185 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1186
1187 cipher
1188 .move_to_organization(&mut key_store.context(), org)
1189 .unwrap();
1190
1191 assert!(cipher.key.is_none());
1192
1193 let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1196 let mut ctx = key_store.context();
1197 let new_attachment_key_id = ctx
1198 .unwrap_symmetric_key(
1199 org_key,
1200 SymmetricKeyId::Local("test_attachment_key"),
1201 &new_attachment_key,
1202 )
1203 .unwrap();
1204 #[allow(deprecated)]
1205 let new_attachment_key_dec = ctx
1206 .dangerous_get_symmetric_key(new_attachment_key_id)
1207 .unwrap();
1208
1209 assert_eq!(*new_attachment_key_dec, attachment_key_val);
1210
1211 let cred2: Fido2CredentialFullView = cipher
1212 .login
1213 .unwrap()
1214 .fido2_credentials
1215 .unwrap()
1216 .first()
1217 .unwrap()
1218 .decrypt(&mut key_store.context(), org_key)
1219 .unwrap();
1220
1221 assert_eq!(cred2.credential_id, "123");
1222 }
1223
1224 #[test]
1225 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1226 let org = OrganizationId::new_v4();
1227 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1228 let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1229 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1230 let org_key = SymmetricKeyId::Organization(org);
1231
1232 let mut ctx = key_store.context();
1233
1234 let cipher_key = ctx
1235 .generate_symmetric_key(SymmetricKeyId::Local("test_cipher_key"))
1236 .unwrap();
1237 let cipher_key_enc = ctx
1238 .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1239 .unwrap();
1240
1241 let attachment_key = ctx
1243 .generate_symmetric_key(SymmetricKeyId::Local("test_attachment_key"))
1244 .unwrap();
1245 let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1246
1247 let mut cipher = generate_cipher();
1248 cipher.key = Some(cipher_key_enc);
1249
1250 let attachment = AttachmentView {
1251 id: None,
1252 url: None,
1253 size: None,
1254 size_name: None,
1255 file_name: Some("Attachment test name".into()),
1256 key: Some(attachment_key_enc.clone()),
1257 decrypted_key: None,
1258 };
1259 cipher.attachments = Some(vec![attachment]);
1260
1261 let cred = generate_fido2(&mut ctx, cipher_key);
1262 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1263
1264 cipher.move_to_organization(&mut ctx, org).unwrap();
1265
1266 let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1268 let new_cipher_key_dec = ctx
1269 .unwrap_symmetric_key(
1270 org_key,
1271 SymmetricKeyId::Local("test_cipher_key"),
1272 &wrapped_new_cipher_key,
1273 )
1274 .unwrap();
1275 #[allow(deprecated)]
1276 let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1277 #[allow(deprecated)]
1278 let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1279
1280 assert_eq!(new_cipher_key_dec, cipher_key_val);
1281
1282 assert_eq!(
1284 cipher.attachments.unwrap()[0]
1285 .key
1286 .as_ref()
1287 .unwrap()
1288 .to_string(),
1289 attachment_key_enc.to_string()
1290 );
1291
1292 let cred2: Fido2Credential = cipher
1293 .login
1294 .unwrap()
1295 .fido2_credentials
1296 .unwrap()
1297 .first()
1298 .unwrap()
1299 .clone();
1300
1301 assert_eq!(
1302 cred2.credential_id.to_string(),
1303 cred.credential_id.to_string()
1304 );
1305 }
1306
1307 #[test]
1308 fn test_decrypt_fido2_private_key() {
1309 let key_store =
1310 create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1311 let mut ctx = key_store.context();
1312
1313 let mut cipher_view = generate_cipher();
1314 cipher_view
1315 .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1316 .unwrap();
1317
1318 let key_id = cipher_view.key_identifier();
1319 let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1320
1321 let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1322
1323 cipher_view.login.as_mut().unwrap().fido2_credentials =
1324 Some(vec![fido2_credential.clone()]);
1325
1326 let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1327 assert_eq!(decrypted_key_value, "123");
1328 }
1329}