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