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