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