diff --git a/crates/bitwarden-vault/src/cipher/card.rs b/crates/bitwarden-vault/src/cipher/card.rs index 4b622a80f..bb1260960 100644 --- a/crates/bitwarden-vault/src/cipher/card.rs +++ b/crates/bitwarden-vault/src/cipher/card.rs @@ -127,6 +127,19 @@ impl TryFrom for Card { } } +impl From for bitwarden_api_api::models::CipherCardModel { + fn from(card: Card) -> Self { + Self { + cardholder_name: card.cardholder_name.map(|n| n.to_string()), + brand: card.brand.map(|b| b.to_string()), + number: card.number.map(|n| n.to_string()), + exp_month: card.exp_month.map(|m| m.to_string()), + exp_year: card.exp_year.map(|y| y.to_string()), + code: card.code.map(|c| c.to_string()), + } + } +} + impl CipherKind for Card { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 392eea61f..70896c611 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -1,4 +1,10 @@ -use bitwarden_api_api::models::CipherDetailsResponseModel; +use bitwarden_api_api::{ + apis::ciphers_api::{PutShareError, PutShareManyError}, + models::{ + CipherDetailsResponseModel, CipherRequestModel, CipherResponseModel, + CipherWithIdRequestModel, + }, +}; use bitwarden_collections::collection::CollectionId; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, @@ -9,8 +15,9 @@ use bitwarden_crypto::{ PrimitiveEncryptable, }; use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::RepositoryError; use bitwarden_uuid::uuid_newtype; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use thiserror::Error; @@ -45,8 +52,20 @@ pub enum CipherError { Crypto(#[from] CryptoError), #[error(transparent)] Encrypt(#[from] EncryptError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + // #[error(transparent)] + // Api #[error("This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation")] AttachmentsWithoutKeys, + #[error("This cipher cannot be moved to the specified organization")] + OrganizationAlreadySet, + #[error(transparent)] + PutShare(#[from] bitwarden_api_api::apis::Error), + #[error(transparent)] + PutShareMany(#[from] bitwarden_api_api::apis::Error), + #[error(transparent)] + Repository(#[from] RepositoryError), } /// Helper trait for operations on cipher types. @@ -85,6 +104,15 @@ pub enum CipherRepromptType { Password = 1, } +impl From for bitwarden_api_api::models::CipherRepromptType { + fn from(t: CipherRepromptType) -> Self { + match t { + CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None, + CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password, + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -97,6 +125,148 @@ pub struct EncryptionContext { pub cipher: Cipher, } +impl TryFrom for CipherWithIdRequestModel { + type Error = CipherError; + fn try_from( + EncryptionContext { + cipher, + encrypted_for, + }: EncryptionContext, + ) -> Result { + Ok(Self { + id: require!(cipher.id).into(), + encrypted_for: Some(encrypted_for.into()), + r#type: Some(cipher.r#type.into()), + organization_id: cipher.organization_id.map(|o| o.to_string()), + folder_id: cipher.folder_id.as_ref().map(ToString::to_string), + favorite: cipher.favorite.into(), + reprompt: Some(cipher.reprompt.into()), + key: cipher.key.map(|k| k.to_string()), + name: cipher.name.to_string(), + notes: cipher.notes.map(|n| n.to_string()), + fields: Some( + cipher + .fields + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), + password_history: Some( + cipher + .password_history + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), + attachments: None, + attachments2: Some( + cipher + .attachments + .into_iter() + .flatten() + .filter_map(|a| { + a.id.map(|id| { + ( + id, + bitwarden_api_api::models::CipherAttachmentModel { + file_name: a.file_name.map(|n| n.to_string()), + key: a.key.map(|k| k.to_string()), + }, + ) + }) + }) + .collect(), + ), + login: cipher.login.map(|l| Box::new(l.into())), + card: cipher.card.map(|c| Box::new(c.into())), + identity: cipher.identity.map(|i| Box::new(i.into())), + secure_note: cipher.secure_note.map(|s| Box::new(s.into())), + ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())), + data: None, // TODO: Consume this instead of the individual fields above. + last_known_revision_date: Some( + cipher + .revision_date + .to_rfc3339_opts(SecondsFormat::Millis, true), + ), + archived_date: cipher + .archived_date + .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)), + }) + } +} + +impl From for CipherRequestModel { + fn from( + EncryptionContext { + cipher, + encrypted_for, + }: EncryptionContext, + ) -> Self { + Self { + encrypted_for: Some(encrypted_for.into()), + r#type: Some(cipher.r#type.into()), + organization_id: cipher.organization_id.map(|o| o.to_string()), + folder_id: cipher.folder_id.as_ref().map(ToString::to_string), + favorite: cipher.favorite.into(), + reprompt: Some(cipher.reprompt.into()), + key: cipher.key.map(|k| k.to_string()), + name: cipher.name.to_string(), + notes: cipher.notes.map(|n| n.to_string()), + fields: Some( + cipher + .fields + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), + password_history: Some( + cipher + .password_history + .into_iter() + .flatten() + .map(Into::into) + .collect(), + ), + attachments: None, + attachments2: Some( + cipher + .attachments + .into_iter() + .flatten() + .filter_map(|a| { + a.id.map(|id| { + ( + id, + bitwarden_api_api::models::CipherAttachmentModel { + file_name: a.file_name.map(|n| n.to_string()), + key: a.key.map(|k| k.to_string()), + }, + ) + }) + }) + .collect(), + ), + login: cipher.login.map(|l| Box::new(l.into())), + card: cipher.card.map(|c| Box::new(c.into())), + identity: cipher.identity.map(|i| Box::new(i.into())), + secure_note: cipher.secure_note.map(|s| Box::new(s.into())), + ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())), + data: None, // TODO: Consume this instead of the individual fields above. + last_known_revision_date: Some( + cipher + .revision_date + .to_rfc3339_opts(SecondsFormat::Millis, true), + ), + archived_date: cipher + .archived_date + .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)), + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -692,6 +862,24 @@ impl Decryptable for Cipher { } } +#[cfg(feature = "wasm")] +impl wasm_bindgen::__rt::VectorIntoJsValue for Cipher { + fn vector_into_jsvalue( + vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>, + ) -> wasm_bindgen::JsValue { + wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector) + } +} + +#[cfg(feature = "wasm")] +impl wasm_bindgen::__rt::VectorIntoJsValue for CipherView { + fn vector_into_jsvalue( + vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>, + ) -> wasm_bindgen::JsValue { + wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector) + } +} + impl IdentifyKey for Cipher { fn key_identifier(&self) -> SymmetricKeyId { match self.organization_id { @@ -793,6 +981,68 @@ impl From for CipherRepromptType } } +impl From for bitwarden_api_api::models::CipherType { + fn from(t: CipherType) -> Self { + match t { + CipherType::Login => bitwarden_api_api::models::CipherType::Login, + CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote, + CipherType::Card => bitwarden_api_api::models::CipherType::Card, + CipherType::Identity => bitwarden_api_api::models::CipherType::Identity, + CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey, + } + } +} + +impl TryFrom for Cipher { + type Error = VaultParseError; + + fn try_from(cipher: CipherResponseModel) -> Result { + Ok(Self { + id: cipher.id.map(CipherId::new), + organization_id: cipher.organization_id.map(OrganizationId::new), + folder_id: cipher.folder_id.map(FolderId::new), + collection_ids: vec![], // CipherResponseModel doesn't include collection_ids + name: require!(EncString::try_from_optional(cipher.name)?), + notes: EncString::try_from_optional(cipher.notes)?, + r#type: require!(cipher.r#type).into(), + login: cipher.login.map(|l| (*l).try_into()).transpose()?, + identity: cipher.identity.map(|i| (*i).try_into()).transpose()?, + card: cipher.card.map(|c| (*c).try_into()).transpose()?, + secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?, + // TODO: add ssh_key + ssh_key: None, + favorite: cipher.favorite.unwrap_or(false), + reprompt: cipher + .reprompt + .map(|r| r.into()) + .unwrap_or(CipherRepromptType::None), + organization_use_totp: cipher.organization_use_totp.unwrap_or(true), + edit: cipher.edit.unwrap_or(true), + // TODO: add permissions + permissions: None, + view_password: cipher.view_password.unwrap_or(true), + local_data: None, // Not sent from server + attachments: cipher + .attachments + .map(|a| a.into_iter().map(|a| a.try_into()).collect()) + .transpose()?, + fields: cipher + .fields + .map(|f| f.into_iter().map(|f| f.try_into()).collect()) + .transpose()?, + password_history: cipher + .password_history + .map(|p| p.into_iter().map(|p| p.try_into()).collect()) + .transpose()?, + creation_date: require!(cipher.creation_date).parse()?, + deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, + revision_date: require!(cipher.revision_date).parse()?, + key: EncString::try_from_optional(cipher.key)?, + archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?, + }) + } +} + #[cfg(test)] mod tests { diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs similarity index 98% rename from crates/bitwarden-vault/src/cipher/cipher_client.rs rename to crates/bitwarden-vault/src/cipher/cipher_client/mod.rs index edb9e53fd..056f9185c 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs @@ -1,7 +1,10 @@ +use std::sync::Arc; + use bitwarden_core::{key_management::SymmetricKeyId, Client, OrganizationId}; use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey}; #[cfg(feature = "wasm")] use bitwarden_encoding::B64; +use bitwarden_state::repository::{Repository, RepositoryError}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -11,6 +14,8 @@ use crate::{ DecryptError, EncryptError, Fido2CredentialFullView, }; +mod share_cipher; + #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct CiphersClient { @@ -174,8 +179,15 @@ impl CiphersClient { let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?; Ok(decrypted_key) } -} + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self + .client + .platform() + .state() + .get_client_managed::()?) + } +} #[cfg(test)] mod tests { diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs b/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs new file mode 100644 index 000000000..b2ee60da4 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs @@ -0,0 +1,206 @@ +use bitwarden_api_api::models::{ + CipherBulkShareRequestModel, CipherMiniResponseModel, CipherShareRequestModel, +}; +use bitwarden_collections::collection::CollectionId; +use bitwarden_core::{require, MissingFieldError, OrganizationId}; +use bitwarden_crypto::EncString; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::{ + Cipher, CipherError, CipherId, CipherRepromptType, CipherView, CiphersClient, VaultParseError, +}; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl CiphersClient { + fn move_to_collections( + &self, + mut cipher_view: CipherView, + organization_id: OrganizationId, + collection_ids: Vec, + ) -> Result { + let organization_id = &organization_id; + if cipher_view.organization_id.is_some() { + return Err(CipherError::OrganizationAlreadySet); + } + + cipher_view = self.move_to_organization(cipher_view, *organization_id)?; + cipher_view.collection_ids = collection_ids; + Ok(cipher_view) + } + + /// Moves a cipher into an organization and collections. + pub async fn share_cipher( + &self, + mut cipher_view: CipherView, + organization_id: OrganizationId, + collection_ids: Vec, + _original_cipher: Option, + ) -> Result { + cipher_view = + self.move_to_collections(cipher_view, organization_id, collection_ids.clone())?; + + let cipher_id = require!(cipher_view.id).into(); + let encrypted_cipher = self.encrypt(cipher_view)?; + + let req = CipherShareRequestModel::new( + collection_ids + .iter() + .map(::to_string) + .collect(), + encrypted_cipher.into(), + ); + + let api_client = &self + .client + .internal + .get_api_configurations() + .await + .api_client; + + let response = api_client + .ciphers_api() + .put_share(cipher_id, Some(req)) + .await?; + + let new_cipher: Cipher = response.try_into()?; + + self.get_repository()? + .set(cipher_id.to_string(), new_cipher.clone()) + .await?; + + Ok(new_cipher) + } + + #[allow(missing_docs)] + pub async fn share_ciphers_bulk( + &self, + cipher_views: Vec, + organization_id: OrganizationId, + collection_ids: Vec, + ) -> Result, CipherError> { + let encrypted_ciphers = cipher_views + .into_iter() + .map(|cv| self.move_to_collections(cv, organization_id, collection_ids.clone())) + .collect::, _>>()? + .into_iter() + .map(|cv| self.encrypt(cv)) + .collect::, _>>()?; + + let request = CipherBulkShareRequestModel::new( + collection_ids + .iter() + .map(::to_string) + .collect(), + encrypted_ciphers + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + ); + let api_client = &self + .client + .internal + .get_api_configurations() + .await + .api_client; + + let response = api_client + .ciphers_api() + .put_share_many(Some(request)) + .await?; + let results = self + .update_repository_from_bulk_share_response( + response.data.unwrap_or_default(), + collection_ids, + ) + .await?; + Ok(results) + } + + async fn update_repository_from_bulk_share_response( + &self, + ciphers: Vec, + collection_ids: Vec, + ) -> Result, CipherError> { + let repo = self.get_repository()?; + let mut results = Vec::new(); + for cipher_mini in ciphers { + // The server does not return the full Cipher object, so we pull the details from the + // current local version to fill in those missing values. + let orig_cipher = repo + .get(cipher_mini.id.ok_or(MissingFieldError("id"))?.to_string()) + .await?; + + let cipher: Cipher = Cipher { + id: cipher_mini.id.map(CipherId::new), + organization_id: cipher_mini.organization_id.map(OrganizationId::new), + key: EncString::try_from_optional(cipher_mini.key)?, + name: require!(EncString::try_from_optional(cipher_mini.name)?), + notes: EncString::try_from_optional(cipher_mini.notes)?, + r#type: require!(cipher_mini.r#type).into(), + login: cipher_mini.login.map(|l| (*l).try_into()).transpose()?, + identity: cipher_mini.identity.map(|i| (*i).try_into()).transpose()?, + card: cipher_mini.card.map(|c| (*c).try_into()).transpose()?, + secure_note: cipher_mini + .secure_note + .map(|s| (*s).try_into()) + .transpose()?, + ssh_key: cipher_mini.ssh_key.map(|s| (*s).try_into()).transpose()?, + reprompt: cipher_mini + .reprompt + .map(|r| r.into()) + .unwrap_or(CipherRepromptType::None), + organization_use_totp: cipher_mini.organization_use_totp.unwrap_or(true), + attachments: cipher_mini + .attachments + .map(|a| a.into_iter().map(|a| a.try_into()).collect()) + .transpose()?, + fields: cipher_mini + .fields + .map(|f| f.into_iter().map(|f| f.try_into()).collect()) + .transpose()?, + password_history: cipher_mini + .password_history + .map(|p| p.into_iter().map(|p| p.try_into()).collect()) + .transpose()?, + creation_date: require!(cipher_mini.creation_date) + .parse() + .map_err(Into::::into)?, + deleted_date: cipher_mini + .deleted_date + .map(|d| d.parse()) + .transpose() + .map_err(Into::::into)?, + revision_date: require!(cipher_mini.revision_date) + .parse() + .map_err(Into::::into)?, + archived_date: cipher_mini + .archived_date + .map(|d| d.parse()) + .transpose() + .map_err(Into::::into)?, + edit: orig_cipher.as_ref().map(|c| c.edit).unwrap_or_default(), + favorite: orig_cipher.as_ref().map(|c| c.favorite).unwrap_or_default(), + folder_id: orig_cipher + .as_ref() + .map(|c| c.folder_id) + .unwrap_or_default(), + permissions: orig_cipher + .as_ref() + .map(|c| c.permissions) + .unwrap_or_default(), + view_password: orig_cipher + .as_ref() + .map(|c| c.view_password) + .unwrap_or_default(), + local_data: orig_cipher.map(|c| c.local_data).unwrap_or_default(), + collection_ids: collection_ids.clone(), /* Should we have confirmation from the + * server that these were set?? */ + }; + repo.set(require!(cipher.id).to_string(), cipher.clone()) + .await?; + results.push(cipher) + } + Ok(results) + } +} diff --git a/crates/bitwarden-vault/src/cipher/field.rs b/crates/bitwarden-vault/src/cipher/field.rs index 9811e68b2..f1f826920 100644 --- a/crates/bitwarden-vault/src/cipher/field.rs +++ b/crates/bitwarden-vault/src/cipher/field.rs @@ -129,6 +129,28 @@ impl From for FieldType { } } +impl From for bitwarden_api_api::models::CipherFieldModel { + fn from(field: Field) -> Self { + Self { + name: field.name.map(|n| n.to_string()), + value: field.value.map(|v| v.to_string()), + r#type: Some(field.r#type.into()), + linked_id: field.linked_id.map(|id| u32::from(id) as i32), + } + } +} + +impl From for bitwarden_api_api::models::FieldType { + fn from(field_type: FieldType) -> Self { + match field_type { + FieldType::Text => bitwarden_api_api::models::FieldType::Text, + FieldType::Hidden => bitwarden_api_api::models::FieldType::Hidden, + FieldType::Boolean => bitwarden_api_api::models::FieldType::Boolean, + FieldType::Linked => bitwarden_api_api::models::FieldType::Linked, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bitwarden-vault/src/cipher/identity.rs b/crates/bitwarden-vault/src/cipher/identity.rs index 1484b3f44..6edd9cde8 100644 --- a/crates/bitwarden-vault/src/cipher/identity.rs +++ b/crates/bitwarden-vault/src/cipher/identity.rs @@ -147,6 +147,31 @@ impl TryFrom for Identity { } } +impl From for bitwarden_api_api::models::CipherIdentityModel { + fn from(identity: Identity) -> Self { + Self { + title: identity.title.map(|t| t.to_string()), + first_name: identity.first_name.map(|n| n.to_string()), + middle_name: identity.middle_name.map(|n| n.to_string()), + last_name: identity.last_name.map(|n| n.to_string()), + address1: identity.address1.map(|a| a.to_string()), + address2: identity.address2.map(|a| a.to_string()), + address3: identity.address3.map(|a| a.to_string()), + city: identity.city.map(|c| c.to_string()), + state: identity.state.map(|s| s.to_string()), + postal_code: identity.postal_code.map(|p| p.to_string()), + country: identity.country.map(|c| c.to_string()), + company: identity.company.map(|c| c.to_string()), + email: identity.email.map(|e| e.to_string()), + phone: identity.phone.map(|p| p.to_string()), + ssn: identity.ssn.map(|s| s.to_string()), + username: identity.username.map(|u| u.to_string()), + passport_number: identity.passport_number.map(|p| p.to_string()), + license_number: identity.license_number.map(|l| l.to_string()), + } + } +} + impl CipherKind for Identity { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 0bb0f29cf..0f5a164d6 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -84,7 +84,7 @@ impl LoginUriView { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -296,7 +296,7 @@ pub struct Login { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -562,6 +562,70 @@ impl TryFrom for Fido2Cre } } +impl From for bitwarden_api_api::models::CipherLoginUriModel { + fn from(uri: LoginUri) -> Self { + bitwarden_api_api::models::CipherLoginUriModel { + uri: uri.uri.map(|u| u.to_string()), + uri_checksum: uri.uri_checksum.map(|c| c.to_string()), + r#match: uri.r#match.map(|m| m.into()), + } + } +} + +impl From for bitwarden_api_api::models::UriMatchType { + fn from(match_type: UriMatchType) -> Self { + match match_type { + UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain, + UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host, + UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith, + UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact, + UriMatchType::RegularExpression => { + bitwarden_api_api::models::UriMatchType::RegularExpression + } + UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never, + } + } +} + +impl From for bitwarden_api_api::models::CipherFido2CredentialModel { + fn from(cred: Fido2Credential) -> Self { + bitwarden_api_api::models::CipherFido2CredentialModel { + credential_id: Some(cred.credential_id.to_string()), + key_type: Some(cred.key_type.to_string()), + key_algorithm: Some(cred.key_algorithm.to_string()), + key_curve: Some(cred.key_curve.to_string()), + key_value: Some(cred.key_value.to_string()), + rp_id: Some(cred.rp_id.to_string()), + user_handle: cred.user_handle.map(|h| h.to_string()), + user_name: cred.user_name.map(|n| n.to_string()), + counter: Some(cred.counter.to_string()), + rp_name: cred.rp_name.map(|n| n.to_string()), + user_display_name: cred.user_display_name.map(|n| n.to_string()), + discoverable: Some(cred.discoverable.to_string()), + creation_date: cred.creation_date.to_rfc3339(), + } + } +} + +impl From for bitwarden_api_api::models::CipherLoginModel { + fn from(login: Login) -> Self { + bitwarden_api_api::models::CipherLoginModel { + uri: None, + uris: login + .uris + .map(|u| u.into_iter().map(|u| u.into()).collect()), + username: login.username.map(|u| u.to_string()), + password: login.password.map(|p| p.to_string()), + password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()), + totp: login.totp.map(|t| t.to_string()), + autofill_on_page_load: login.autofill_on_page_load, + fido2_credentials: login + .fido2_credentials + .map(|c| c.into_iter().map(|c| c.into()).collect()), + } + } +} + impl CipherKind for Login { fn decrypt_subtitle( &self, diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 39fe85361..38b96b60e 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -5,7 +5,10 @@ pub(crate) mod card; pub(crate) mod cipher; pub(crate) mod cipher_client; pub(crate) mod cipher_permissions; +// pub(crate) mod create; +// pub(crate) mod edit; pub(crate) mod field; +// pub(crate) mod get_list; pub(crate) mod identity; pub(crate) mod linked_id; pub(crate) mod local_data; diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 268b71971..cc14bafcf 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -84,6 +84,22 @@ impl From for SecureNoteType { } } +impl From for bitwarden_api_api::models::SecureNoteType { + fn from(model: SecureNoteType) -> Self { + match model { + SecureNoteType::Generic => bitwarden_api_api::models::SecureNoteType::Generic, + } + } +} + +impl From for CipherSecureNoteModel { + fn from(model: SecureNote) -> Self { + Self { + r#type: Some(model.r#type.into()), + } + } +} + impl CipherKind for SecureNote { fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec { [cipher diff --git a/crates/bitwarden-vault/src/cipher/ssh_key.rs b/crates/bitwarden-vault/src/cipher/ssh_key.rs index e56da0033..2345953fc 100644 --- a/crates/bitwarden-vault/src/cipher/ssh_key.rs +++ b/crates/bitwarden-vault/src/cipher/ssh_key.rs @@ -95,6 +95,16 @@ impl TryFrom for SshKey { } } +impl From for CipherSshKeyModel { + fn from(ssh_key: SshKey) -> Self { + Self { + private_key: Some(ssh_key.private_key.to_string()), + public_key: Some(ssh_key.public_key.to_string()), + key_fingerprint: Some(ssh_key.fingerprint.to_string()), + } + } +} + #[cfg(test)] mod tests { use bitwarden_core::key_management::create_test_crypto_with_user_key; diff --git a/crates/bitwarden-vault/src/password_history.rs b/crates/bitwarden-vault/src/password_history.rs index 23df58daf..222f1de16 100644 --- a/crates/bitwarden-vault/src/password_history.rs +++ b/crates/bitwarden-vault/src/password_history.rs @@ -21,6 +21,15 @@ pub struct PasswordHistory { last_used_date: DateTime, } +impl From for CipherPasswordHistoryModel { + fn from(history: PasswordHistory) -> Self { + Self { + password: history.password.to_string(), + last_used_date: history.last_used_date.to_rfc3339(), + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)]