diff --git a/crates/bitwarden/src/admin_console/mod.rs b/crates/bitwarden/src/admin_console/mod.rs new file mode 100644 index 000000000..e553456cf --- /dev/null +++ b/crates/bitwarden/src/admin_console/mod.rs @@ -0,0 +1,3 @@ +mod policy; + +pub use policy::Policy; diff --git a/crates/bitwarden/src/admin_console/policy.rs b/crates/bitwarden/src/admin_console/policy.rs new file mode 100644 index 000000000..1b4acc310 --- /dev/null +++ b/crates/bitwarden/src/admin_console/policy.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use bitwarden_api_api::models::PolicyResponseModel; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use uuid::Uuid; + +use crate::error::{Error, Result}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct Policy { + id: Uuid, + organization_id: Uuid, + r#type: PolicyType, + data: Option>, + enabled: bool, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, JsonSchema)] +#[repr(u8)] +pub enum PolicyType { + TwoFactorAuthentication = 0, // Requires users to have 2fa enabled + MasterPassword = 1, // Sets minimum requirements for master password complexity + PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases + SingleOrg = 3, // Allows users to only be apart of one organization + RequireSso = 4, // Requires users to authenticate with SSO + PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items + DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends + SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends + ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow + MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout + DisablePersonalVaultExport = 10, // Disable personal vault export + ActivateAutofill = 11, // Activates autofill with page load on the browser extension +} + +impl TryFrom for Policy { + type Error = Error; + + fn try_from(policy: PolicyResponseModel) -> Result { + Ok(Self { + id: policy.id.ok_or(Error::MissingFields)?, + organization_id: policy.organization_id.ok_or(Error::MissingFields)?, + r#type: policy.r#type.ok_or(Error::MissingFields)?.into(), + data: policy.data, + enabled: policy.enabled.ok_or(Error::MissingFields)?, + }) + } +} + +impl From for PolicyType { + fn from(policy_type: bitwarden_api_api::models::PolicyType) -> Self { + match policy_type { + bitwarden_api_api::models::PolicyType::Variant0 => PolicyType::TwoFactorAuthentication, + bitwarden_api_api::models::PolicyType::Variant1 => PolicyType::MasterPassword, + bitwarden_api_api::models::PolicyType::Variant2 => PolicyType::PasswordGenerator, + bitwarden_api_api::models::PolicyType::Variant3 => PolicyType::SingleOrg, + bitwarden_api_api::models::PolicyType::Variant4 => PolicyType::RequireSso, + bitwarden_api_api::models::PolicyType::Variant5 => PolicyType::PersonalOwnership, + bitwarden_api_api::models::PolicyType::Variant6 => PolicyType::DisableSend, + bitwarden_api_api::models::PolicyType::Variant7 => PolicyType::SendOptions, + bitwarden_api_api::models::PolicyType::Variant8 => PolicyType::ResetPassword, + bitwarden_api_api::models::PolicyType::Variant9 => PolicyType::MaximumVaultTimeout, + bitwarden_api_api::models::PolicyType::Variant10 => { + PolicyType::DisablePersonalVaultExport + } + bitwarden_api_api::models::PolicyType::Variant11 => PolicyType::ActivateAutofill, + } + } +} diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index 09f877c50..3004d2dea 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -4,14 +4,13 @@ use aes::cipher::{generic_array::GenericArray, typenum::U32}; use base64::Engine; use serde::{de::Visitor, Deserialize}; +use super::{KeyDecryptable, KeyEncryptable, LocateKey}; use crate::{ crypto::{decrypt_aes256_hmac, SymmetricCryptoKey}, error::{CryptoError, EncStringParseError, Error, Result}, util::BASE64_ENGINE, }; -use super::{KeyDecryptable, KeyEncryptable, LocateKey}; - /// # Encrypted string primitive /// /// [EncString] is a Bitwarden specific primitive that represents an encrypted string. They are @@ -165,6 +164,12 @@ impl FromStr for EncString { } impl EncString { + /// Synthetic sugar for mapping `Option` to `Result>` + #[cfg(feature = "internal")] + pub(crate) fn try_from_optional(s: Option) -> Result, Error> { + s.map(|s| s.parse()).transpose() + } + #[cfg(feature = "mobile")] pub(crate) fn from_buffer(buf: &[u8]) -> Result { if buf.is_empty() { @@ -397,11 +402,22 @@ impl KeyDecryptable for EncString { } } +/// Usually we wouldn't want to expose EncStrings in the API or the schemas. +/// But during the transition phase we will expose endpoints using the EncString type. +impl schemars::JsonSchema for crate::crypto::EncString { + fn schema_name() -> String { + "EncString".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + gen.subschema_for::() + } +} + #[cfg(test)] mod tests { - use crate::crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; - use super::EncString; + use crate::crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}; #[test] fn test_enc_string_roundtrip() { diff --git a/crates/bitwarden/src/lib.rs b/crates/bitwarden/src/lib.rs index 20e36d237..0017ecff9 100644 --- a/crates/bitwarden/src/lib.rs +++ b/crates/bitwarden/src/lib.rs @@ -51,6 +51,8 @@ #[cfg(feature = "mobile")] uniffi::setup_scaffolding!(); +#[cfg(feature = "internal")] +pub mod admin_console; pub mod auth; pub mod client; pub mod crypto; @@ -66,7 +68,7 @@ pub mod tool; #[cfg(feature = "mobile")] pub(crate) mod uniffi_support; mod util; -#[cfg(feature = "mobile")] +#[cfg(feature = "internal")] pub mod vault; pub mod wordlist; diff --git a/crates/bitwarden/src/mobile/mod.rs b/crates/bitwarden/src/mobile/mod.rs index fe3083aa8..e0ba50d00 100644 --- a/crates/bitwarden/src/mobile/mod.rs +++ b/crates/bitwarden/src/mobile/mod.rs @@ -8,15 +8,3 @@ mod client_kdf; pub use client_crypto::ClientCrypto; pub use client_kdf::ClientKdf; - -// Usually we wouldn't want to expose EncStrings in the API or the schemas, -// but we need them in the mobile API, so define it here to limit the scope -impl schemars::JsonSchema for crate::crypto::EncString { - fn schema_name() -> String { - "EncString".to_string() - } - - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - gen.subschema_for::() - } -} diff --git a/crates/bitwarden/src/platform/domain.rs b/crates/bitwarden/src/platform/domain.rs new file mode 100644 index 000000000..c903dd5a5 --- /dev/null +++ b/crates/bitwarden/src/platform/domain.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::error::{Error, Result}; + +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct GlobalDomains { + pub r#type: i32, + pub domains: Vec, + pub excluded: bool, +} + +impl TryFrom for GlobalDomains { + type Error = Error; + + fn try_from(global_domains: bitwarden_api_api::models::GlobalDomains) -> Result { + Ok(Self { + r#type: global_domains.r#type.ok_or(Error::MissingFields)?, + domains: global_domains.domains.ok_or(Error::MissingFields)?, + excluded: global_domains.excluded.ok_or(Error::MissingFields)?, + }) + } +} diff --git a/crates/bitwarden/src/platform/mod.rs b/crates/bitwarden/src/platform/mod.rs index 0224e8d3a..0e234f00b 100644 --- a/crates/bitwarden/src/platform/mod.rs +++ b/crates/bitwarden/src/platform/mod.rs @@ -1,3 +1,4 @@ +mod domain; mod generate_fingerprint; mod get_user_api_key; mod secret_verification_request; diff --git a/crates/bitwarden/src/platform/sync.rs b/crates/bitwarden/src/platform/sync.rs index fd33e4343..870b88ab9 100644 --- a/crates/bitwarden/src/platform/sync.rs +++ b/crates/bitwarden/src/platform/sync.rs @@ -1,14 +1,16 @@ use bitwarden_api_api::models::{ - CipherDetailsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, - SyncResponseModel, + DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use super::domain::GlobalDomains; use crate::{ + admin_console::Policy, client::{encryption_settings::EncryptionSettings, Client}, error::{Error, Result}, + vault::{Cipher, Collection, Folder}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -59,15 +61,23 @@ pub struct ProfileOrganizationResponse { #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CipherDetailsResponse {} +pub struct DomainResponse { + pub equivalent_domains: Vec>, + pub global_equivalent_domains: Vec, +} #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SyncResponse { /// Data about the user, including their encryption keys and the organizations they are a part of pub profile: ProfileResponse, - /// List of ciphers accesible by the user - pub ciphers: Vec, + pub folders: Vec, + pub collections: Vec, + /// List of ciphers accessible by the user + pub ciphers: Vec, + pub domains: Option, + pub policies: Vec, + pub sends: Vec, } impl SyncResponse { @@ -78,22 +88,27 @@ impl SyncResponse { let profile = *response.profile.ok_or(Error::MissingFields)?; let ciphers = response.ciphers.ok_or(Error::MissingFields)?; + fn try_into_iter(iter: In) -> Result + where + In: IntoIterator, + InItem: TryInto, + Out: FromIterator, + { + iter.into_iter().map(|i| i.try_into()).collect() + } + Ok(SyncResponse { profile: ProfileResponse::process_response(profile, enc)?, - ciphers: ciphers - .into_iter() - .map(CipherDetailsResponse::process_response) - .collect::>()?, + folders: try_into_iter(response.folders.ok_or(Error::MissingFields)?)?, + collections: try_into_iter(response.collections.ok_or(Error::MissingFields)?)?, + ciphers: try_into_iter(ciphers)?, + domains: response.domains.map(|d| (*d).try_into()).transpose()?, + policies: try_into_iter(response.policies.ok_or(Error::MissingFields)?)?, + sends: try_into_iter(response.sends.ok_or(Error::MissingFields)?)?, }) } } -impl CipherDetailsResponse { - fn process_response(_response: CipherDetailsResponseModel) -> Result { - Ok(CipherDetailsResponse {}) - } -} - impl ProfileOrganizationResponse { fn process_response( response: ProfileOrganizationResponseModel, @@ -124,3 +139,18 @@ impl ProfileResponse { }) } } + +impl TryFrom for DomainResponse { + type Error = Error; + fn try_from(value: DomainsResponseModel) -> Result { + Ok(Self { + equivalent_domains: value.equivalent_domains.unwrap_or_default(), + global_equivalent_domains: value + .global_equivalent_domains + .unwrap_or_default() + .into_iter() + .map(|s| s.try_into()) + .collect::>>()?, + }) + } +} diff --git a/crates/bitwarden/src/vault/cipher/attachment.rs b/crates/bitwarden/src/vault/cipher/attachment.rs index 101c8f4bc..ace4937da 100644 --- a/crates/bitwarden/src/vault/cipher/attachment.rs +++ b/crates/bitwarden/src/vault/cipher/attachment.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -28,7 +28,7 @@ pub struct AttachmentView { pub size: Option, pub size_name: Option, pub file_name: Option, - pub key: Option, + pub key: Option>, // TODO: Should be made into SymmetricCryptoKey } impl KeyEncryptable for AttachmentView { @@ -39,7 +39,7 @@ impl KeyEncryptable for AttachmentView { size: self.size, size_name: self.size_name, file_name: self.file_name.encrypt_with_key(key)?, - key: self.key.encrypt_with_key(key)?, + key: self.key.as_deref().encrypt_with_key(key)?, }) } } @@ -56,3 +56,18 @@ impl KeyDecryptable for Attachment { }) } } + +impl TryFrom for Attachment { + type Error = Error; + + fn try_from(attachment: bitwarden_api_api::models::AttachmentResponseModel) -> Result { + Ok(Self { + id: attachment.id, + url: attachment.url, + size: attachment.size, + size_name: attachment.size_name, + file_name: EncString::try_from_optional(attachment.file_name)?, + key: EncString::try_from_optional(attachment.key)?, + }) + } +} diff --git a/crates/bitwarden/src/vault/cipher/card.rs b/crates/bitwarden/src/vault/cipher/card.rs index 1545ad171..6f96041e9 100644 --- a/crates/bitwarden/src/vault/cipher/card.rs +++ b/crates/bitwarden/src/vault/cipher/card.rs @@ -1,9 +1,10 @@ +use bitwarden_api_api::models::CipherCardModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -55,3 +56,18 @@ impl KeyDecryptable for Card { }) } } + +impl TryFrom for Card { + type Error = Error; + + fn try_from(card: CipherCardModel) -> Result { + Ok(Self { + cardholder_name: EncString::try_from_optional(card.cardholder_name)?, + exp_month: EncString::try_from_optional(card.exp_month)?, + exp_year: EncString::try_from_optional(card.exp_year)?, + code: EncString::try_from_optional(card.code)?, + brand: EncString::try_from_optional(card.brand)?, + number: EncString::try_from_optional(card.number)?, + }) + } +} diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index b95a2f090..e44a3757a 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::CipherDetailsResponseModel; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -12,7 +13,7 @@ use super::{ use crate::{ client::encryption_settings::EncryptionSettings, crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, vault::password_history, }; @@ -332,3 +333,68 @@ impl LocateKey for CipherView { enc.get_key(&self.organization_id) } } + +impl TryFrom for Cipher { + type Error = Error; + + fn try_from(cipher: CipherDetailsResponseModel) -> Result { + Ok(Self { + id: cipher.id, + organization_id: cipher.organization_id, + folder_id: cipher.folder_id, + collection_ids: cipher.collection_ids.unwrap_or_default(), + name: EncString::try_from_optional(cipher.name)?.ok_or(Error::MissingFields)?, + notes: EncString::try_from_optional(cipher.notes)?, + r#type: cipher.r#type.ok_or(Error::MissingFields)?.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()?, + 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), + 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: cipher.creation_date.ok_or(Error::MissingFields)?.parse()?, + deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?, + revision_date: cipher.revision_date.ok_or(Error::MissingFields)?.parse()?, + key: EncString::try_from_optional(cipher.key)?, + }) + } +} + +impl From for CipherType { + fn from(t: bitwarden_api_api::models::CipherType) -> Self { + match t { + bitwarden_api_api::models::CipherType::Variant1 => CipherType::Login, + bitwarden_api_api::models::CipherType::Variant2 => CipherType::SecureNote, + bitwarden_api_api::models::CipherType::Variant3 => CipherType::Card, + bitwarden_api_api::models::CipherType::Variant4 => CipherType::Identity, + } + } +} + +impl From for CipherRepromptType { + fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self { + match t { + bitwarden_api_api::models::CipherRepromptType::Variant0 => CipherRepromptType::None, + bitwarden_api_api::models::CipherRepromptType::Variant1 => CipherRepromptType::Password, + } + } +} diff --git a/crates/bitwarden/src/vault/cipher/field.rs b/crates/bitwarden/src/vault/cipher/field.rs index 13f7dc9bb..dee71872b 100644 --- a/crates/bitwarden/src/vault/cipher/field.rs +++ b/crates/bitwarden/src/vault/cipher/field.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::CipherFieldModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -5,7 +6,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::linked_id::LinkedIdType; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] @@ -61,3 +62,30 @@ impl KeyDecryptable for Field { }) } } + +impl TryFrom for Field { + type Error = Error; + + fn try_from(model: CipherFieldModel) -> Result { + Ok(Self { + name: EncString::try_from_optional(model.name)?, + value: EncString::try_from_optional(model.value)?, + r#type: model.r#type.map(|t| t.into()).ok_or(Error::MissingFields)?, + linked_id: model + .linked_id + .map(|id| (id as u32).try_into()) + .transpose()?, + }) + } +} + +impl From for FieldType { + fn from(model: bitwarden_api_api::models::FieldType) -> Self { + match model { + bitwarden_api_api::models::FieldType::Variant0 => FieldType::Text, + bitwarden_api_api::models::FieldType::Variant1 => FieldType::Hidden, + bitwarden_api_api::models::FieldType::Variant2 => FieldType::Boolean, + bitwarden_api_api::models::FieldType::Variant3 => FieldType::Linked, + } + } +} diff --git a/crates/bitwarden/src/vault/cipher/identity.rs b/crates/bitwarden/src/vault/cipher/identity.rs index d40991866..922d2aee6 100644 --- a/crates/bitwarden/src/vault/cipher/identity.rs +++ b/crates/bitwarden/src/vault/cipher/identity.rs @@ -1,9 +1,10 @@ +use bitwarden_api_api::models::CipherIdentityModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -103,3 +104,30 @@ impl KeyDecryptable for Identity { }) } } + +impl TryFrom for Identity { + type Error = Error; + + fn try_from(identity: CipherIdentityModel) -> Result { + Ok(Self { + title: EncString::try_from_optional(identity.title)?, + first_name: EncString::try_from_optional(identity.first_name)?, + middle_name: EncString::try_from_optional(identity.middle_name)?, + last_name: EncString::try_from_optional(identity.last_name)?, + address1: EncString::try_from_optional(identity.address1)?, + address2: EncString::try_from_optional(identity.address2)?, + address3: EncString::try_from_optional(identity.address3)?, + city: EncString::try_from_optional(identity.city)?, + state: EncString::try_from_optional(identity.state)?, + postal_code: EncString::try_from_optional(identity.postal_code)?, + country: EncString::try_from_optional(identity.country)?, + company: EncString::try_from_optional(identity.company)?, + email: EncString::try_from_optional(identity.email)?, + phone: EncString::try_from_optional(identity.phone)?, + ssn: EncString::try_from_optional(identity.ssn)?, + username: EncString::try_from_optional(identity.username)?, + passport_number: EncString::try_from_optional(identity.passport_number)?, + license_number: EncString::try_from_optional(identity.license_number)?, + }) + } +} diff --git a/crates/bitwarden/src/vault/cipher/linked_id.rs b/crates/bitwarden/src/vault/cipher/linked_id.rs index 692d59b77..6fb676dfe 100644 --- a/crates/bitwarden/src/vault/cipher/linked_id.rs +++ b/crates/bitwarden/src/vault/cipher/linked_id.rs @@ -10,6 +10,7 @@ pub enum LinkedIdType { Identity(IdentityLinkedIdType), } +use crate::error::{Error, Result}; #[cfg(feature = "mobile")] use crate::UniffiCustomTypeConverter; #[cfg(feature = "mobile")] @@ -73,6 +74,43 @@ pub enum IdentityLinkedIdType { FullName = 418, } +impl TryFrom for LinkedIdType { + type Error = Error; + + fn try_from(value: u32) -> Result { + match value { + 100 => Ok(LinkedIdType::Login(LoginLinkedIdType::Username)), + 101 => Ok(LinkedIdType::Login(LoginLinkedIdType::Password)), + 300 => Ok(LinkedIdType::Card(CardLinkedIdType::CardholderName)), + 301 => Ok(LinkedIdType::Card(CardLinkedIdType::ExpMonth)), + 302 => Ok(LinkedIdType::Card(CardLinkedIdType::ExpYear)), + 303 => Ok(LinkedIdType::Card(CardLinkedIdType::Code)), + 304 => Ok(LinkedIdType::Card(CardLinkedIdType::Brand)), + 305 => Ok(LinkedIdType::Card(CardLinkedIdType::Number)), + 400 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Title)), + 401 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::MiddleName)), + 402 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Address1)), + 403 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Address2)), + 404 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Address3)), + 405 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::City)), + 406 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::State)), + 407 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::PostalCode)), + 408 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Country)), + 409 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Company)), + 410 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Email)), + 411 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Phone)), + 412 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Ssn)), + 413 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::Username)), + 414 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::PassportNumber)), + 415 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::LicenseNumber)), + 416 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::FirstName)), + 417 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::LastName)), + 418 => Ok(LinkedIdType::Identity(IdentityLinkedIdType::FullName)), + _ => Err(Error::MissingFields), + } + } +} + #[cfg(test)] mod tests { diff --git a/crates/bitwarden/src/vault/cipher/login.rs b/crates/bitwarden/src/vault/cipher/login.rs index 7a8e18411..da71f963f 100644 --- a/crates/bitwarden/src/vault/cipher/login.rs +++ b/crates/bitwarden/src/vault/cipher/login.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -5,7 +6,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] @@ -106,3 +107,48 @@ impl KeyDecryptable for Login { }) } } + +impl TryFrom for Login { + type Error = Error; + + fn try_from(login: CipherLoginModel) -> Result { + Ok(Self { + username: EncString::try_from_optional(login.username)?, + password: EncString::try_from_optional(login.password)?, + password_revision_date: login + .password_revision_date + .map(|d| d.parse()) + .transpose()?, + uris: login + .uris + .map(|v| v.into_iter().map(|u| u.try_into()).collect()) + .transpose()?, + totp: EncString::try_from_optional(login.totp)?, + autofill_on_page_load: login.autofill_on_page_load, + }) + } +} + +impl TryFrom for LoginUri { + type Error = Error; + + fn try_from(uri: CipherLoginUriModel) -> Result { + Ok(Self { + uri: EncString::try_from_optional(uri.uri)?, + r#match: uri.r#match.map(|m| m.into()), + }) + } +} + +impl From for UriMatchType { + fn from(value: bitwarden_api_api::models::UriMatchType) -> Self { + match value { + bitwarden_api_api::models::UriMatchType::Variant0 => Self::Domain, + bitwarden_api_api::models::UriMatchType::Variant1 => Self::Host, + bitwarden_api_api::models::UriMatchType::Variant2 => Self::StartsWith, + bitwarden_api_api::models::UriMatchType::Variant3 => Self::Exact, + bitwarden_api_api::models::UriMatchType::Variant4 => Self::RegularExpression, + bitwarden_api_api::models::UriMatchType::Variant5 => Self::Never, + } + } +} diff --git a/crates/bitwarden/src/vault/cipher/secure_note.rs b/crates/bitwarden/src/vault/cipher/secure_note.rs index 0c7b4c799..5fbb81811 100644 --- a/crates/bitwarden/src/vault/cipher/secure_note.rs +++ b/crates/bitwarden/src/vault/cipher/secure_note.rs @@ -1,10 +1,11 @@ +use bitwarden_api_api::models::CipherSecureNoteModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{ crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, JsonSchema)] @@ -43,3 +44,21 @@ impl KeyDecryptable for SecureNote { }) } } + +impl TryFrom for SecureNote { + type Error = Error; + + fn try_from(model: CipherSecureNoteModel) -> Result { + Ok(Self { + r#type: model.r#type.map(|t| t.into()).ok_or(Error::MissingFields)?, + }) + } +} + +impl From for SecureNoteType { + fn from(model: bitwarden_api_api::models::SecureNoteType) -> Self { + match model { + bitwarden_api_api::models::SecureNoteType::Variant0 => SecureNoteType::Generic, + } + } +} diff --git a/crates/bitwarden/src/vault/collection.rs b/crates/bitwarden/src/vault/collection.rs index 58492ef17..11983dca5 100644 --- a/crates/bitwarden/src/vault/collection.rs +++ b/crates/bitwarden/src/vault/collection.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::CollectionDetailsResponseModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -5,7 +6,7 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, crypto::{EncString, KeyDecryptable, LocateKey, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -59,3 +60,18 @@ impl KeyDecryptable for Collection { }) } } + +impl TryFrom for Collection { + type Error = Error; + + fn try_from(collection: CollectionDetailsResponseModel) -> Result { + Ok(Collection { + id: collection.id.ok_or(Error::MissingFields)?, + organization_id: collection.organization_id.ok_or(Error::MissingFields)?, + name: collection.name.ok_or(Error::MissingFields)?.parse()?, + external_id: collection.external_id, + hide_passwords: collection.hide_passwords.unwrap_or(false), + read_only: collection.read_only.unwrap_or(false), + }) + } +} diff --git a/crates/bitwarden/src/vault/folder.rs b/crates/bitwarden/src/vault/folder.rs index f9ae06055..d04bf3110 100644 --- a/crates/bitwarden/src/vault/folder.rs +++ b/crates/bitwarden/src/vault/folder.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::FolderResponseModel; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -5,7 +6,7 @@ use uuid::Uuid; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -47,3 +48,15 @@ impl KeyDecryptable for Folder { }) } } + +impl TryFrom for Folder { + type Error = Error; + + fn try_from(folder: FolderResponseModel) -> Result { + Ok(Folder { + id: folder.id.ok_or(Error::MissingFields)?, + name: EncString::try_from_optional(folder.name)?.ok_or(Error::MissingFields)?, + revision_date: folder.revision_date.ok_or(Error::MissingFields)?.parse()?, + }) + } +} diff --git a/crates/bitwarden/src/vault/mod.rs b/crates/bitwarden/src/vault/mod.rs index ca9b12c1c..6ac43a4d0 100644 --- a/crates/bitwarden/src/vault/mod.rs +++ b/crates/bitwarden/src/vault/mod.rs @@ -3,6 +3,7 @@ mod collection; mod folder; mod password_history; mod send; +#[cfg(feature = "mobile")] mod totp; pub use cipher::{Cipher, CipherListView, CipherView}; @@ -10,5 +11,7 @@ pub use collection::{Collection, CollectionView}; pub use folder::{Folder, FolderView}; pub use password_history::{PasswordHistory, PasswordHistoryView}; pub use send::{Send, SendListView, SendView}; +#[cfg(feature = "mobile")] pub(crate) use totp::generate_totp; +#[cfg(feature = "mobile")] pub use totp::TotpResponse; diff --git a/crates/bitwarden/src/vault/password_history.rs b/crates/bitwarden/src/vault/password_history.rs index da6a4b19e..786c7a8f2 100644 --- a/crates/bitwarden/src/vault/password_history.rs +++ b/crates/bitwarden/src/vault/password_history.rs @@ -1,10 +1,11 @@ +use bitwarden_api_api::models::CipherPasswordHistoryModel; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ crypto::{EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey}, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -42,3 +43,14 @@ impl KeyDecryptable for PasswordHistory { }) } } + +impl TryFrom for PasswordHistory { + type Error = Error; + + fn try_from(model: CipherPasswordHistoryModel) -> Result { + Ok(Self { + password: model.password.parse()?, + last_used_date: model.last_used_date.parse()?, + }) + } +} diff --git a/crates/bitwarden/src/vault/send.rs b/crates/bitwarden/src/vault/send.rs index 3aec03639..255f6b9fd 100644 --- a/crates/bitwarden/src/vault/send.rs +++ b/crates/bitwarden/src/vault/send.rs @@ -1,3 +1,4 @@ +use bitwarden_api_api::models::{SendFileModel, SendResponseModel, SendTextModel}; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,7 +10,7 @@ use crate::{ derive_shareable_key, EncString, KeyDecryptable, KeyEncryptable, LocateKey, SymmetricCryptoKey, }, - error::Result, + error::{Error, Result}, }; #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -264,6 +265,64 @@ impl KeyEncryptable for SendView { } } +impl TryFrom for Send { + type Error = Error; + + fn try_from(send: SendResponseModel) -> Result { + Ok(Send { + id: send.id.ok_or(Error::MissingFields)?, + access_id: send.access_id.ok_or(Error::MissingFields)?, + name: send.name.ok_or(Error::MissingFields)?.parse()?, + notes: EncString::try_from_optional(send.notes)?, + key: send.key.ok_or(Error::MissingFields)?.parse()?, + password: send.password, + r#type: send.r#type.ok_or(Error::MissingFields)?.into(), + file: send.file.map(|f| (*f).try_into()).transpose()?, + text: send.text.map(|t| (*t).try_into()).transpose()?, + max_access_count: send.max_access_count.map(|s| s as u32), + access_count: send.access_count.ok_or(Error::MissingFields)? as u32, + disabled: send.disabled.unwrap_or(false), + hide_email: send.hide_email.unwrap_or(false), + revision_date: send.revision_date.ok_or(Error::MissingFields)?.parse()?, + deletion_date: send.deletion_date.ok_or(Error::MissingFields)?.parse()?, + expiration_date: send.expiration_date.map(|s| s.parse()).transpose()?, + }) + } +} + +impl From for SendType { + fn from(t: bitwarden_api_api::models::SendType) -> Self { + match t { + bitwarden_api_api::models::SendType::Variant0 => SendType::Text, + bitwarden_api_api::models::SendType::Variant1 => SendType::File, + } + } +} + +impl TryFrom for SendFile { + type Error = Error; + + fn try_from(file: SendFileModel) -> Result { + Ok(SendFile { + id: file.id.ok_or(Error::MissingFields)?, + file_name: file.file_name.ok_or(Error::MissingFields)?.parse()?, + size: file.size.ok_or(Error::MissingFields)?.to_string(), + size_name: file.size_name.ok_or(Error::MissingFields)?, + }) + } +} + +impl TryFrom for SendText { + type Error = Error; + + fn try_from(text: SendTextModel) -> Result { + Ok(SendText { + text: EncString::try_from_optional(text.text)?, + hidden: text.hidden.unwrap_or(false), + }) + } +} + #[cfg(test)] mod tests { use super::Send; diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 1fcd15414..53b9b609e 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -3,6 +3,7 @@ use bitwarden::{ ApiKeyLoginRequest, PasswordLoginRequest, TwoFactorEmailRequest, TwoFactorProvider, TwoFactorRequest, }, + platform::SyncRequest, Client, }; use bitwarden_cli::text_prompt_when_none; @@ -80,6 +81,13 @@ pub(crate) async fn login_password(mut client: Client, email: Option) -> debug!("{:?}", result); } + let res = client + .sync(&SyncRequest { + exclude_subdomains: Some(true), + }) + .await?; + info!("{:#?}", res); + Ok(()) }