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