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