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