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