diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 1c68a6cf1..b4a55b22c 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "bitwarden-api-api" -description = "Api bindings for the Bitwarden API." categories = ["api-bindings"] version.workspace = true @@ -13,14 +12,10 @@ license-file.workspace = true keywords.workspace = true [dependencies] -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_repr = { workspace = true } -serde_with = { version = ">=3.8, <4", default-features = false, features = [ - "base64", - "std", - "macros", -] } -url = ">=2.5, <3" -uuid = { workspace = true } +serde = { version = "^1.0", features = ["derive"] } +serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } +serde_json = "^1.0" +serde_repr = "^0.1" +url = "^2.5" +uuid = { version = "^1.8", features = ["serde", "v4"] } +reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } diff --git a/crates/bitwarden-api-api/src/apis/accounts_api.rs b/crates/bitwarden-api-api/src/apis/accounts_api.rs index 921cf2d03..d6bb6151c 100644 --- a/crates/bitwarden-api-api/src/apis/accounts_api.rs +++ b/crates/bitwarden-api-api/src/apis/accounts_api.rs @@ -364,6 +364,7 @@ pub async fn accounts_api_key( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Auth/Controllers/AccountsController.cs#L435`] pub async fn accounts_delete( configuration: &configuration::Configuration, secret_verification_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/auth_requests_api.rs b/crates/bitwarden-api-api/src/apis/auth_requests_api.rs index 3adec9e26..342c6ef25 100644 --- a/crates/bitwarden-api-api/src/apis/auth_requests_api.rs +++ b/crates/bitwarden-api-api/src/apis/auth_requests_api.rs @@ -256,6 +256,7 @@ pub async fn auth_requests_get_response( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Auth/Controllers/AuthRequestsController.cs#L83`] pub async fn auth_requests_post( configuration: &configuration::Configuration, auth_request_create_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/ciphers_api.rs b/crates/bitwarden-api-api/src/apis/ciphers_api.rs index 5b3ba071a..874ff7733 100644 --- a/crates/bitwarden-api-api/src/apis/ciphers_api.rs +++ b/crates/bitwarden-api-api/src/apis/ciphers_api.rs @@ -759,6 +759,7 @@ pub async fn ciphers_delete_many_admin( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Vault/Controllers/CiphersController.cs#L129`] pub async fn ciphers_get( configuration: &configuration::Configuration, id: uuid::Uuid, diff --git a/crates/bitwarden-api-api/src/apis/devices_api.rs b/crates/bitwarden-api-api/src/apis/devices_api.rs index 81962e61e..09ca5c103 100644 --- a/crates/bitwarden-api-api/src/apis/devices_api.rs +++ b/crates/bitwarden-api-api/src/apis/devices_api.rs @@ -507,6 +507,7 @@ pub async fn devices_get_device_keys( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Controllers/DevicesController.cs#L93`] pub async fn devices_post( configuration: &configuration::Configuration, device_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/folders_api.rs b/crates/bitwarden-api-api/src/apis/folders_api.rs index 93a7cb460..4da7eb642 100644 --- a/crates/bitwarden-api-api/src/apis/folders_api.rs +++ b/crates/bitwarden-api-api/src/apis/folders_api.rs @@ -144,6 +144,7 @@ pub async fn folders_delete_all( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Vault/Controllers/FoldersController.cs#L49`] pub async fn folders_get( configuration: &configuration::Configuration, id: &str, @@ -236,6 +237,7 @@ pub async fn folders_get_all( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Vault/Controllers/FoldersController.cs#L58`] pub async fn folders_post( configuration: &configuration::Configuration, folder_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/installations_api.rs b/crates/bitwarden-api-api/src/apis/installations_api.rs index b440fcc0b..e5265cd2d 100644 --- a/crates/bitwarden-api-api/src/apis/installations_api.rs +++ b/crates/bitwarden-api-api/src/apis/installations_api.rs @@ -78,6 +78,7 @@ pub async fn installations_get( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Platform/Installations/Controllers/InstallationsController.cs#L46`] pub async fn installations_post( configuration: &configuration::Configuration, installation_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/organizations_api.rs b/crates/bitwarden-api-api/src/apis/organizations_api.rs index e54b53351..a13d513fd 100644 --- a/crates/bitwarden-api-api/src/apis/organizations_api.rs +++ b/crates/bitwarden-api-api/src/apis/organizations_api.rs @@ -1002,6 +1002,7 @@ pub async fn organizations_leave( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L198`] pub async fn organizations_post( configuration: &configuration::Configuration, organization_create_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/plans_api.rs b/crates/bitwarden-api-api/src/apis/plans_api.rs index 7fccf0bac..df80c9936 100644 --- a/crates/bitwarden-api-api/src/apis/plans_api.rs +++ b/crates/bitwarden-api-api/src/apis/plans_api.rs @@ -21,6 +21,7 @@ pub enum PlansGetError { UnknownValue(serde_json::Value), } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Controllers/PlansController.cs#L16`] pub async fn plans_get( configuration: &configuration::Configuration, ) -> Result> { diff --git a/crates/bitwarden-api-api/src/apis/provider_billing_v_next_api.rs b/crates/bitwarden-api-api/src/apis/provider_billing_v_next_api.rs index 354ce7966..ac566dd68 100644 --- a/crates/bitwarden-api-api/src/apis/provider_billing_v_next_api.rs +++ b/crates/bitwarden-api-api/src/apis/provider_billing_v_next_api.rs @@ -1053,3 +1053,144 @@ pub async fn provider_billing_v_next_update_payment_method( })) } } + +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs#L114`] +pub async fn providers_provider_id_billing_vnext_warnings_get( + configuration: &configuration::Configuration, + provider_id: &str, + id: Option, + name: Option<&str>, + business_name: Option<&str>, + business_address1: Option<&str>, + business_address2: Option<&str>, + business_address3: Option<&str>, + business_country: Option<&str>, + business_tax_number: Option<&str>, + billing_email: Option<&str>, + billing_phone: Option<&str>, + status: Option, + use_events: Option, + r#type: Option, + enabled: Option, + creation_date: Option, + revision_date: Option, + gateway: Option, + gateway_customer_id: Option<&str>, + gateway_subscription_id: Option<&str>, + discount_id: Option<&str>, +) -> Result<(), Error> { + // add a prefix to parameters to efficiently prevent name collisions + let p_provider_id = provider_id; + let p_id = id; + let p_name = name; + let p_business_name = business_name; + let p_business_address1 = business_address1; + let p_business_address2 = business_address2; + let p_business_address3 = business_address3; + let p_business_country = business_country; + let p_business_tax_number = business_tax_number; + let p_billing_email = billing_email; + let p_billing_phone = billing_phone; + let p_status = status; + let p_use_events = use_events; + let p_type = r#type; + let p_enabled = enabled; + let p_creation_date = creation_date; + let p_revision_date = revision_date; + let p_gateway = gateway; + let p_gateway_customer_id = gateway_customer_id; + let p_gateway_subscription_id = gateway_subscription_id; + let p_discount_id = discount_id; + + let uri_str = format!( + "{}/providers/{providerId}/billing/vnext/warnings", + configuration.base_path, + providerId = crate::apis::urlencode(p_provider_id) + ); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref param_value) = p_id { + req_builder = req_builder.query(&[("id", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_name { + req_builder = req_builder.query(&[("name", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_name { + req_builder = req_builder.query(&[("businessName", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_address1 { + req_builder = req_builder.query(&[("businessAddress1", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_address2 { + req_builder = req_builder.query(&[("businessAddress2", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_address3 { + req_builder = req_builder.query(&[("businessAddress3", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_country { + req_builder = req_builder.query(&[("businessCountry", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_business_tax_number { + req_builder = req_builder.query(&[("businessTaxNumber", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_billing_email { + req_builder = req_builder.query(&[("billingEmail", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_billing_phone { + req_builder = req_builder.query(&[("billingPhone", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_status { + req_builder = req_builder.query(&[("status", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_use_events { + req_builder = req_builder.query(&[("useEvents", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_type { + req_builder = req_builder.query(&[("type", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_enabled { + req_builder = req_builder.query(&[("enabled", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_creation_date { + req_builder = req_builder.query(&[("creationDate", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_revision_date { + req_builder = req_builder.query(&[("revisionDate", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_gateway { + req_builder = req_builder.query(&[("gateway", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_gateway_customer_id { + req_builder = req_builder.query(&[("gatewayCustomerId", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_gateway_subscription_id { + req_builder = req_builder.query(&[("gatewaySubscriptionId", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_discount_id { + req_builder = req_builder.query(&[("discountId", ¶m_value.to_string())]); + } + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.oauth_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + if !status.is_client_error() && !status.is_server_error() { + Ok(()) + } else { + let content = resp.text().await?; + let entity: Option = + serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} diff --git a/crates/bitwarden-api-api/src/apis/sends_api.rs b/crates/bitwarden-api-api/src/apis/sends_api.rs index d7e69da9a..92d516804 100644 --- a/crates/bitwarden-api-api/src/apis/sends_api.rs +++ b/crates/bitwarden-api-api/src/apis/sends_api.rs @@ -355,6 +355,7 @@ pub async fn sends_get_send_file_download_data( } } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Tools/Controllers/SendsController.cs#L205`] pub async fn sends_post( configuration: &configuration::Configuration, send_request_model: Option, diff --git a/crates/bitwarden-api-api/src/apis/sync_api.rs b/crates/bitwarden-api-api/src/apis/sync_api.rs index 4cf9f4d73..065878843 100644 --- a/crates/bitwarden-api-api/src/apis/sync_api.rs +++ b/crates/bitwarden-api-api/src/apis/sync_api.rs @@ -21,6 +21,7 @@ pub enum SyncGetError { UnknownValue(serde_json::Value), } +/// This operation is defined on: [`https://github.com/bitwarden/server/blob/22420f595f2f50dd2fc0061743841285258aed22/src/Api/Vault/Controllers/SyncController.cs#L80`] pub async fn sync_get( configuration: &configuration::Configuration, exclude_domains: Option, diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 022225451..ec616cec1 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "bitwarden-api-identity" -description = "Api bindings for the Bitwarden Identity API." categories = ["api-bindings"] version.workspace = true @@ -13,14 +12,10 @@ license-file.workspace = true keywords.workspace = true [dependencies] -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_repr = { workspace = true } -serde_with = { version = ">=3.8, <4", default-features = false, features = [ - "base64", - "std", - "macros", -] } -url = ">=2.5, <3" -uuid = { workspace = true } +serde = { version = "^1.0", features = ["derive"] } +serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } +serde_json = "^1.0" +serde_repr = "^0.1" +url = "^2.5" +uuid = { version = "^1.8", features = ["serde", "v4"] } +reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } diff --git a/crates/bitwarden-vault/src/cipher/card.rs b/crates/bitwarden-vault/src/cipher/card.rs index 4b622a80f..5c70cb15a 100644 --- a/crates/bitwarden-vault/src/cipher/card.rs +++ b/crates/bitwarden-vault/src/cipher/card.rs @@ -12,7 +12,7 @@ use super::cipher::CipherKind; use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError}; #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Card { diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 392eea61f..1b586f5b6 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -47,6 +47,15 @@ pub enum CipherError { Encrypt(#[from] EncryptError), #[error("This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation")] AttachmentsWithoutKeys, + // POC - Cipher versioning + #[error("Unsupported cipher version {0}")] + UnsupportedCipherVersion(u32), + #[error("Migration failed: {0}")] + MigrationFailed(String), + #[error(transparent)] + Chrono(#[from] chrono::ParseError), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), } /// Helper trait for operations on cipher types. @@ -445,6 +454,29 @@ impl Cipher { .map(|kind| kind.get_copyable_fields(Some(self))) .unwrap_or_default() } + + /// Extracts and sets the CipherType-specific fields from the opaque `data` field. + /// + /// This replaces the values provided by the API in the `login`, `secure_note`, `card`, + /// `identity`, and `ssh_key` fields, relying instead on client-side parsing of the + /// `data` field. + pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> { + let data = self + .data + .as_ref() + .ok_or(VaultParseError::MissingFieldError(MissingFieldError( + "data", + )))?; + + match &self.r#type { + crate::CipherType::Login => self.login = serde_json::from_str(data)?, + crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?, + crate::CipherType::Card => self.card = serde_json::from_str(data)?, + crate::CipherType::Identity => self.identity = serde_json::from_str(data)?, + crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?, + } + Ok(()) + } } impl CipherView { @@ -875,7 +907,7 @@ mod tests { folder_id: None, collection_ids: vec![], key: None, - name: "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=".parse().unwrap(), + name: TEST_CIPHER_NAME.parse().unwrap(), notes: None, r#type: CipherType::Login, login: Some(Login { @@ -1333,4 +1365,376 @@ mod tests { let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap(); assert_eq!(decrypted_key_value, "123"); } + + // Test constants for encrypted strings + const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk="; + const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo="; + const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0="; + const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4="; + const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk="; + const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0="; + const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69"; + + #[test] + fn test_populate_cipher_types_login_with_valid_data() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::Login, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some(format!( + r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#, + TEST_ENC_STRING_1, TEST_ENC_STRING_2 + )), + }; + + cipher + .populate_cipher_types() + .expect("populate_cipher_types failed"); + + assert!(cipher.login.is_some()); + let login = cipher.login.unwrap(); + assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1); + assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2); + } + + #[test] + fn test_populate_cipher_types_secure_note() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::SecureNote, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()), + }; + + cipher + .populate_cipher_types() + .expect("populate_cipher_types failed"); + + assert!(cipher.secure_note.is_some()); + } + + #[test] + fn test_populate_cipher_types_card() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::Card, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some(format!( + r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#, + TEST_ENC_STRING_1, + TEST_ENC_STRING_2, + TEST_ENC_STRING_3, + TEST_ENC_STRING_4, + TEST_ENC_STRING_5, + TEST_ENC_STRING_1 + )), + }; + + cipher + .populate_cipher_types() + .expect("populate_cipher_types failed"); + + assert!(cipher.card.is_some()); + let card = cipher.card.unwrap(); + assert_eq!( + card.cardholder_name.as_ref().unwrap().to_string(), + TEST_ENC_STRING_1 + ); + assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2); + assert_eq!( + card.exp_month.as_ref().unwrap().to_string(), + TEST_ENC_STRING_3 + ); + assert_eq!( + card.exp_year.as_ref().unwrap().to_string(), + TEST_ENC_STRING_4 + ); + assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5); + assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1); + } + + #[test] + fn test_populate_cipher_types_identity() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::Identity, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some(format!( + r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#, + TEST_ENC_STRING_1, + TEST_ENC_STRING_2, + TEST_ENC_STRING_3, + TEST_ENC_STRING_4, + TEST_ENC_STRING_5, + TEST_ENC_STRING_1, + TEST_ENC_STRING_2, + TEST_ENC_STRING_3, + TEST_ENC_STRING_4, + TEST_ENC_STRING_5 + )), + }; + + cipher + .populate_cipher_types() + .expect("populate_cipher_types failed"); + + assert!(cipher.identity.is_some()); + let identity = cipher.identity.unwrap(); + assert_eq!( + identity.first_name.as_ref().unwrap().to_string(), + TEST_ENC_STRING_1 + ); + assert_eq!( + identity.last_name.as_ref().unwrap().to_string(), + TEST_ENC_STRING_2 + ); + assert_eq!( + identity.email.as_ref().unwrap().to_string(), + TEST_ENC_STRING_3 + ); + assert_eq!( + identity.phone.as_ref().unwrap().to_string(), + TEST_ENC_STRING_4 + ); + assert_eq!( + identity.company.as_ref().unwrap().to_string(), + TEST_ENC_STRING_5 + ); + assert_eq!( + identity.address1.as_ref().unwrap().to_string(), + TEST_ENC_STRING_1 + ); + assert_eq!( + identity.city.as_ref().unwrap().to_string(), + TEST_ENC_STRING_2 + ); + assert_eq!( + identity.state.as_ref().unwrap().to_string(), + TEST_ENC_STRING_3 + ); + assert_eq!( + identity.postal_code.as_ref().unwrap().to_string(), + TEST_ENC_STRING_4 + ); + assert_eq!( + identity.country.as_ref().unwrap().to_string(), + TEST_ENC_STRING_5 + ); + } + + #[test] + fn test_populate_cipher_types_ssh_key() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::SshKey, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some(format!( + r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#, + TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3 + )), + }; + + cipher + .populate_cipher_types() + .expect("populate_cipher_types failed"); + + assert!(cipher.ssh_key.is_some()); + let ssh_key = cipher.ssh_key.unwrap(); + assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1); + assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2); + assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3); + } + + #[test] + fn test_populate_cipher_types_with_null_data() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::Login, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: None, + }; + + let result = cipher.populate_cipher_types(); + assert!(matches!( + result, + Err(VaultParseError::MissingFieldError(MissingFieldError( + "data" + ))) + )); + } + + #[test] + fn test_populate_cipher_types_with_invalid_json() { + let mut cipher = Cipher { + id: Some(TEST_UUID.parse().unwrap()), + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: TEST_CIPHER_NAME.parse().unwrap(), + notes: None, + r#type: CipherType::Login, + login: None, + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: CipherRepromptType::None, + organization_use_totp: false, + edit: true, + view_password: true, + permissions: None, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + deleted_date: None, + revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), + data: Some("invalid json".to_string()), + }; + + let result = cipher.populate_cipher_types(); + + assert!(matches!(result, Err(VaultParseError::SerdeJson(_)))); + } } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index edb9e53fd..875fbd9cb 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -7,8 +7,10 @@ use wasm_bindgen::prelude::*; use super::EncryptionContext; use crate::{ - cipher::cipher::DecryptCipherListResult, Cipher, CipherError, CipherListView, CipherView, - DecryptError, EncryptError, Fido2CredentialFullView, + cipher::cipher::{DecryptCipherListResult, CURRENT_CIPHER_VERSION}, + migrations::registry::MigrationRegistry, + Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError, + Fido2CredentialFullView, }; #[allow(missing_docs)] @@ -174,6 +176,46 @@ impl CiphersClient { let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?; Ok(decrypted_key) } + + #[allow(missing_docs)] + pub fn migrate(&self, mut ciphers: Vec) -> Result, CipherError> { + let registry = MigrationRegistry::new(); + for cipher in &mut ciphers { + let key = cipher.key_identifier(); + let key_store = self.client.internal.get_key_store(); + let mut ctx = key_store.context(); + let cipher_key = Cipher::decrypt_cipher_key(&mut ctx, key, &cipher.key) + .map_err(CipherError::CryptoError)?; + + if let Some(data_str) = &mut cipher.data { + let mut data_json: serde_json::Value = serde_json::from_str(data_str) + .map_err(|e| CipherError::MigrationFailed(e.to_string()))?; + + let response_version = data_json + .get("version") + .and_then(|v| v.as_u64()) + .unwrap_or(1) as u32; + + if response_version < CURRENT_CIPHER_VERSION { + registry.migrate( + &mut data_json, + response_version, + CURRENT_CIPHER_VERSION, + Some(&mut ctx), + Some(cipher_key), + )?; + + data_json["version"] = serde_json::json!(CURRENT_CIPHER_VERSION); + + *data_str = serde_json::to_string(&data_json) + .map_err(|e| CipherError::MigrationFailed(e.to_string()))?; + } + cipher.populate_cipher_types()?; + } + } + + Ok(ciphers) + } } #[cfg(test)] @@ -288,6 +330,37 @@ mod tests { } } + fn test_cipher_v1() -> Cipher { + let mut cipher = test_cipher(); + cipher.data = Some( + serde_json::to_string(&serde_json::json!({ + "Username": "2.PE7g9afvjh9N57ORdUlCDQ==|d8C4kLo0CYAKfa9Gjp4mqg==|YmgGDxGWXtIzW+TJsjDW3CoS0k+U4NZSAwygzq6zV/0=", + "Password": "2.sGpXvg4a6BPFOPN3ePxZaQ==|ChseXEroqhbB11sBk+hH4Q==|SVz2WMGDvZSJwTivSnCFCCfQmmnuiHHPEgw4gzr09pQ=", + "Uris": [], + "Totp": null, + "version": 1 + })) + .unwrap() + ); + cipher + } + + fn test_cipher_v2() -> Cipher { + let mut cipher = test_cipher(); + cipher.data = Some( + serde_json::to_string(&serde_json::json!({ + "Username": "2.PE7g9afvjh9N57ORdUlCDQ==|d8C4kLo0CYAKfa9Gjp4mqg==|YmgGDxGWXtIzW+TJsjDW3CoS0k+U4NZSAwygzq6zV/0=", + "Password": "2.sGpXvg4a6BPFOPN3ePxZaQ==|ChseXEroqhbB11sBk+hH4Q==|SVz2WMGDvZSJwTivSnCFCCfQmmnuiHHPEgw4gzr09pQ=", + "Uris": [], + "Totp": null, + "SecurityQuestions": [], + "version": CURRENT_CIPHER_VERSION + })) + .unwrap() + ); + cipher + } + #[tokio::test] async fn test_decrypt_list() { let client = Client::init_test_account(test_bitwarden_com_account()).await; @@ -538,4 +611,38 @@ mod tests { Some(DecryptError::Crypto(CryptoError::InvalidMac)) )); } + + #[tokio::test] + async fn test_migrate_core_v1_login_adds_security_questions() { + let client = Client::init_test_account(test_bitwarden_com_account()).await; + + let ciphers = vec![test_cipher_v1(), test_cipher_v2()]; + let migrated = client.vault().ciphers().migrate(ciphers).unwrap(); + + assert_eq!(migrated.len(), 2); + + // Test first cipher (v1 should be migrated to v2) + let first_cipher = &migrated[0]; + if let Some(data_str) = &first_cipher.data { + let data: serde_json::Value = serde_json::from_str(data_str).unwrap(); + // Version should be updated inside the JSON + assert_eq!( + data.get("version").and_then(|v| v.as_u64()), + Some(CURRENT_CIPHER_VERSION as u64) + ); + assert!(data.get("SecurityQuestions").is_some()); + assert_eq!(data["SecurityQuestions"], serde_json::Value::Array(vec![])); + } + + // Test second cipher (v2 should remain unchanged) + let second_cipher = &migrated[1]; + if let Some(data_str) = &second_cipher.data { + let data: serde_json::Value = serde_json::from_str(data_str).unwrap(); + assert_eq!( + data.get("version").and_then(|v| v.as_u64()), + Some(CURRENT_CIPHER_VERSION as u64) + ); + assert!(data.get("SecurityQuestions").is_some()); + } + } } diff --git a/crates/bitwarden-vault/src/cipher/identity.rs b/crates/bitwarden-vault/src/cipher/identity.rs index 1484b3f44..7e93276c7 100644 --- a/crates/bitwarden-vault/src/cipher/identity.rs +++ b/crates/bitwarden-vault/src/cipher/identity.rs @@ -12,7 +12,7 @@ use super::cipher::CipherKind; use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError}; #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Identity { diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 0bb0f29cf..084933e06 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -280,7 +280,7 @@ impl Decryptable for Fido2Crede #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Login { diff --git a/crates/bitwarden-vault/src/cipher/migrations/mod.rs b/crates/bitwarden-vault/src/cipher/migrations/mod.rs new file mode 100644 index 000000000..ee72aba02 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/migrations/mod.rs @@ -0,0 +1,2 @@ +pub mod registry; +pub mod versions; diff --git a/crates/bitwarden-vault/src/cipher/migrations/registry.rs b/crates/bitwarden-vault/src/cipher/migrations/registry.rs new file mode 100644 index 000000000..8834ce5fb --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/migrations/registry.rs @@ -0,0 +1,64 @@ +use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; +use bitwarden_crypto::KeyStoreContext; + +use crate::{ + migrations::versions::{V1ToV2Migration, V2ToV3Migration}, + CipherError, +}; + +pub trait Migration { + fn source_version(&self) -> u32; + fn target_version(&self) -> u32; + fn migrate( + &self, + cipher_data: &mut serde_json::Value, + ctx: Option<&mut KeyStoreContext>, + cipher_key: Option, + ) -> Result<(), CipherError>; // this can be migration error +} + +pub struct MigrationRegistry { + migrations: Vec>, +} + +impl MigrationRegistry { + pub fn new() -> Self { + let mut registry = Self { + migrations: Vec::new(), + }; + + // something like this + registry.register(Box::new(V1ToV2Migration)); + registry.register(Box::new(V2ToV3Migration)); + + registry + } + + pub fn register(&mut self, migration: Box) { + self.migrations.push(migration); + } + + pub fn migrate( + &self, + cipher_data: &mut serde_json::Value, + source_version: u32, + target_version: u32, + mut ctx: Option<&mut KeyStoreContext>, + cipher_key: Option, + ) -> Result<(), CipherError> { + let mut current_version = source_version; + + while current_version < target_version { + let migration = self + .migrations + .iter() + .find(|m| m.source_version() == current_version) + .ok_or(CipherError::UnsupportedCipherVersion(current_version))?; + + migration.migrate(cipher_data, ctx.as_deref_mut(), cipher_key)?; + current_version = migration.target_version(); + } + + Ok(()) + } +} diff --git a/crates/bitwarden-vault/src/cipher/migrations/versions/mod.rs b/crates/bitwarden-vault/src/cipher/migrations/versions/mod.rs new file mode 100644 index 000000000..65d722c71 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/migrations/versions/mod.rs @@ -0,0 +1,5 @@ +pub mod v1_to_v2; +pub mod v2_to_v3; + +pub use v1_to_v2::V1ToV2Migration; +pub use v2_to_v3::V2ToV3Migration; diff --git a/crates/bitwarden-vault/src/cipher/migrations/versions/v1_to_v2.rs b/crates/bitwarden-vault/src/cipher/migrations/versions/v1_to_v2.rs new file mode 100644 index 000000000..01e21133e --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/migrations/versions/v1_to_v2.rs @@ -0,0 +1,54 @@ +use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; +use bitwarden_crypto::KeyStoreContext; + +use crate::migrations::registry::Migration; + +pub struct V1ToV2Migration; + +impl Migration for V1ToV2Migration { + fn source_version(&self) -> u32 { + 1 + } + + fn target_version(&self) -> u32 { + 2 + } + + fn migrate( + &self, + cipher_data: &mut serde_json::Value, + _ctx: Option<&mut KeyStoreContext>, + _cipher_key: Option, + ) -> Result<(), crate::CipherError> { + if let Some(obj) = cipher_data.as_object_mut() { + if !obj.contains_key("SecurityQuestions") { + obj.insert( + "SecurityQuestions".to_string(), + serde_json::Value::Array(vec![]), + ); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_v1_to_v2_adds_security_questions() { + let mut data = serde_json::json!({ + "Username": "2.PE7g9afvjh9N57ORdUlCDQ==|d8C4kLo0CYAKfa9Gjp4mqg==|YmgGDxGWXtIzW+TJsjDW3CoS0k+U4NZSAwygzq6zV/0=", + "Password": "2.sGpXvg4a6BPFOPN3ePxZaQ==|ChseXEroqhbB11sBk+hH4Q==|SVz2WMGDvZSJwTivSnCFCCfQmmnuiHHPEgw4gzr09pQ=", + "Uris": [], + "Totp": null + }); + + V1ToV2Migration.migrate(&mut data, None, None).unwrap(); + + assert!(data.get("SecurityQuestions").is_some()); + assert_eq!(data["SecurityQuestions"], serde_json::json!([])); + } +} diff --git a/crates/bitwarden-vault/src/cipher/migrations/versions/v2_to_v3.rs b/crates/bitwarden-vault/src/cipher/migrations/versions/v2_to_v3.rs new file mode 100644 index 000000000..92b3efb99 --- /dev/null +++ b/crates/bitwarden-vault/src/cipher/migrations/versions/v2_to_v3.rs @@ -0,0 +1,118 @@ +use base64::{prelude::BASE64_STANDARD, Engine}; +use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; +use bitwarden_crypto::{Decryptable, EncString, KeyStoreContext, PrimitiveEncryptable}; + +use crate::{migrations::registry::Migration, CipherError}; + +pub struct V2ToV3Migration; + +impl Migration for V2ToV3Migration { + fn source_version(&self) -> u32 { + 2 + } + + fn target_version(&self) -> u32 { + 3 + } + + fn migrate( + &self, + cipher_data: &mut serde_json::Value, + ctx: Option<&mut KeyStoreContext>, + cipher_key: Option, + ) -> Result<(), CipherError> { + let ctx = + ctx.ok_or_else(|| CipherError::MigrationFailed("Crypto context required".to_string()))?; + + let ciphers_key = cipher_key + .ok_or_else(|| CipherError::MigrationFailed("Cipher key required".to_string()))?; + + if let Some(fido2_credentials) = cipher_data + .get_mut("fido2Credentials") + .and_then(|v| v.as_array_mut()) + { + for fido2_credential in fido2_credentials { + if let Some(credential_id_str) = fido2_credential + .get("credentialId") + .and_then(|v| v.as_str()) + { + let enc_string: EncString = credential_id_str.parse()?; + let dec_credential_id: String = enc_string.decrypt(ctx, ciphers_key)?; + let b64_credential_id = BASE64_STANDARD.encode(&dec_credential_id); + let enc_credential_id: EncString = + b64_credential_id.encrypt(ctx, ciphers_key)?; + + if let Some(obj) = fido2_credential.as_object_mut() { + obj.insert( + "credentialId".to_string(), + serde_json::Value::String(enc_credential_id.to_string()), + ); + obj.insert( + "credentialIdType".to_string(), + serde_json::Value::String("base64".to_owned()), + ); + } + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use bitwarden_core::key_management::create_test_crypto_with_user_key; + use bitwarden_crypto::SymmetricCryptoKey; + + use super::*; + + #[test] + fn test_v2_to_v3_migration_fido2_credentials() { + let key_store = + create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); + let mut ctx = key_store.context(); + + let cipher_key = ctx + .generate_symmetric_key(SymmetricKeyId::Local("test_cipher_key")) + .unwrap(); + + let original_credential_id = "test-credential-id-123"; + let encrypted_credential_id: EncString = original_credential_id + .encrypt(&mut ctx, cipher_key) + .unwrap(); + + let mut data = serde_json::json!({ + "fido2Credentials": [ + { + "credentialId": encrypted_credential_id.to_string(), + "keyType": "public-key", + "rpId": "example.com" + } + ] + }); + + V2ToV3Migration + .migrate(&mut data, Some(&mut ctx), Some(cipher_key)) + .unwrap(); + + println!("Data {:#?}", data["fido2Credentials"]); + + let fido2_creds = data["fido2Credentials"].as_array().unwrap(); + let credential = &fido2_creds[0]; + + assert_eq!(credential["credentialIdType"], "base64"); + + let new_credential_id_str = credential["credentialId"].as_str().unwrap(); + let new_enc_string: EncString = new_credential_id_str.parse().unwrap(); + let decrypted_new_id: String = new_enc_string.decrypt(&mut ctx, cipher_key).unwrap(); + + let expected_base64 = BASE64_STANDARD.encode(original_credential_id); + // The decrypted value should match the expected base64 encoding + assert_eq!(decrypted_new_id, expected_base64); + + // Verifiy other fields remain untouched + assert_eq!(credential["keyType"], "public-key"); + assert_eq!(credential["rpId"], "example.com"); + } +} diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index 39fe85361..dbc9f880b 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod identity; pub(crate) mod linked_id; pub(crate) mod local_data; pub(crate) mod login; +pub(crate) mod migrations; pub(crate) mod secure_note; pub(crate) mod ssh_key; diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 268b71971..797db9a90 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -26,7 +26,7 @@ pub enum SecureNoteType { } #[derive(Clone, Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SecureNote { diff --git a/crates/bitwarden-vault/src/cipher/ssh_key.rs b/crates/bitwarden-vault/src/cipher/ssh_key.rs index e56da0033..f301a1c8a 100644 --- a/crates/bitwarden-vault/src/cipher/ssh_key.rs +++ b/crates/bitwarden-vault/src/cipher/ssh_key.rs @@ -15,7 +15,7 @@ use super::cipher::CipherKind; use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError}; #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SshKey { diff --git a/crates/bitwarden-vault/src/error.rs b/crates/bitwarden-vault/src/error.rs index b713296ef..acfc02d0a 100644 --- a/crates/bitwarden-vault/src/error.rs +++ b/crates/bitwarden-vault/src/error.rs @@ -1,6 +1,8 @@ use bitwarden_error::bitwarden_error; use thiserror::Error; +use crate::CipherError; + /// Generic error type for vault encryption errors. #[allow(missing_docs)] #[bitwarden_error(flat)]