diff --git a/Cargo.lock b/Cargo.lock index 04f97f266..abfca253f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,8 +326,10 @@ dependencies = [ "serde_json", "serde_repr", "serde_with", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] @@ -765,6 +767,7 @@ version = "0.1.0" dependencies = [ "async-trait", "base64", + "bitwarden-api-api", "bitwarden-auth", "bitwarden-core", "bitwarden-crypto", diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 1c68a6cf1..8266d4348 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -12,6 +12,10 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true +[features] +default = [] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support + [dependencies] reqwest = { workspace = true } serde = { workspace = true } @@ -22,5 +26,7 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ "std", "macros", ] } +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } url = ">=2.5, <3" uuid = { workspace = true } +wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs index 38e02d030..af98f1d00 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs @@ -9,10 +9,17 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct MasterPasswordUnlockKdfResponseModel { #[serde(rename = "kdfType")] pub kdf_type: models::KdfType, diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs index d6ec1c676..a161fa1dd 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs @@ -9,24 +9,31 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct MasterPasswordUnlockResponseModel { #[serde(rename = "kdf")] pub kdf: Box, #[serde(rename = "masterKeyEncryptedUserKey")] - pub master_key_encrypted_user_key: Option, + pub master_key_encrypted_user_key: String, #[serde(rename = "salt")] - pub salt: Option, + pub salt: String, } impl MasterPasswordUnlockResponseModel { pub fn new( kdf: models::MasterPasswordUnlockKdfResponseModel, - master_key_encrypted_user_key: Option, - salt: Option, + master_key_encrypted_user_key: String, + salt: String, ) -> MasterPasswordUnlockResponseModel { MasterPasswordUnlockResponseModel { kdf: Box::new(kdf), diff --git a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs index 94ed72a69..b53e730ed 100644 --- a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs +++ b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs @@ -9,10 +9,17 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct UserDecryptionResponseModel { #[serde( rename = "masterPasswordUnlock", diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs index 94ebe9445..08378ad7c 100644 --- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, num::NonZeroU32}; +use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -35,6 +36,9 @@ pub struct IdentityTokenSuccessResponse { #[serde(rename = "keyConnectorUrl", alias = "KeyConnectorUrl")] key_connector_url: Option, + #[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")] + pub user_decryption_options: Option, + /// Stores unknown api response fields extra: Option>, } @@ -61,6 +65,7 @@ mod test { force_password_reset: Default::default(), api_use_key_connector: Default::default(), key_connector_url: Default::default(), + user_decryption_options: Default::default(), extra: Default::default(), } } diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs new file mode 100644 index 000000000..19c570570 --- /dev/null +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -0,0 +1,380 @@ +//! Mobile specific master password operations +//! +//! This module contains the data structures and error handling for master password unlock +//! operations. + +use std::num::NonZeroU32; + +use bitwarden_api_api::models::{ + master_password_unlock_response_model::MasterPasswordUnlockResponseModel, KdfType, +}; +use bitwarden_crypto::{CryptoError, EncString, Kdf}; +use bitwarden_error::bitwarden_error; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{require, MissingFieldError}; + +/// Error for master password related operations. +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum MasterPasswordError { + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), +} + +/// Represents the data required to unlock with the master password. +#[allow(missing_docs)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct MasterPasswordUnlockData { + pub kdf: Kdf, + pub master_key_wrapped_user_key: EncString, + pub salt: String, +} + +impl TryFrom for MasterPasswordUnlockData { + type Error = MasterPasswordError; + + fn try_from(response: MasterPasswordUnlockResponseModel) -> Result { + let kdf = match response.kdf.kdf_type { + KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { + iterations: parse_nonzero_u32( + response.kdf.iterations, + stringify!(response.kdf.iterations), + )?, + }, + KdfType::Argon2id => Kdf::Argon2id { + iterations: parse_nonzero_u32( + response.kdf.iterations, + stringify!(response.kdf.iterations), + )?, + memory: parse_nonzero_u32( + require!(response.kdf.memory), + stringify!(response.kdf.memory), + )?, + parallelism: parse_nonzero_u32( + require!(response.kdf.parallelism), + stringify!(response.kdf.parallelism), + )?, + }, + }; + + Ok(MasterPasswordUnlockData { + kdf, + master_key_wrapped_user_key: response.master_key_encrypted_user_key.as_str().parse()?, + salt: response.salt, + }) + } +} + +fn parse_nonzero_u32( + value: impl TryInto, + field_name: &'static str, +) -> Result { + let num: u32 = value + .try_into() + .map_err(|_| MissingFieldError(field_name))?; + NonZeroU32::new(num).ok_or(MissingFieldError(field_name)) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockKdfResponseModel}; + + use super::*; + + const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI="; + const TEST_SALT: &str = "test@example.com"; + + #[test] + fn test_try_from_master_password_unlock_response_model_pbkdf2_success() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(result.is_ok()); + let data = result.unwrap(); + + match data.kdf { + Kdf::PBKDF2 { iterations } => { + assert_eq!(iterations.get(), 600_000); + } + _ => panic!("Expected PBKDF2 KDF"), + } + + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_success() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(result.is_ok()); + let data = result.unwrap(); + + match data.kdf { + Kdf::Argon2id { + iterations, + memory, + parallelism, + } => { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } + _ => panic!("Expected Argon2id KDF"), + } + + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_invalid_user_key_crypto_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: TEST_INVALID_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::Crypto(_)))); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_memory_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: None, + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.memory" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_memory_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(0), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.memory" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: None, + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.parallelism" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(0), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.parallelism" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_pbkdf2_iterations_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 0, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.iterations" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_argon2id_iterations_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 0, + memory: Some(64), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.iterations" + ))) + )); + } + + #[test] + fn test_serde_serialization_pbkdf2() { + let data = MasterPasswordUnlockData { + kdf: Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), + }, + master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), + salt: TEST_SALT.to_string(), + }; + + let serialized = serde_json::to_string(&data).unwrap(); + let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); + + match (data.kdf, deserialized.kdf) { + (Kdf::PBKDF2 { iterations: i1 }, Kdf::PBKDF2 { iterations: i2 }) => { + assert_eq!(i1, i2); + } + _ => panic!("KDF types don't match"), + } + + assert_eq!( + data.master_key_wrapped_user_key.to_string(), + deserialized.master_key_wrapped_user_key.to_string() + ); + assert_eq!(data.salt, deserialized.salt); + } + + #[test] + fn test_serde_serialization_argon2id() { + let data = MasterPasswordUnlockData { + kdf: Kdf::Argon2id { + iterations: 3.try_into().unwrap(), + memory: 64.try_into().unwrap(), + parallelism: 4.try_into().unwrap(), + }, + master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), + salt: TEST_SALT.to_string(), + }; + + let serialized = serde_json::to_string(&data).unwrap(); + let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); + + match (data.kdf, deserialized.kdf) { + ( + Kdf::Argon2id { + iterations: i1, + memory: m1, + parallelism: p1, + }, + Kdf::Argon2id { + iterations: i2, + memory: m2, + parallelism: p2, + }, + ) => { + assert_eq!(i1, i2); + assert_eq!(m1, m2); + assert_eq!(p1, p2); + } + _ => panic!("KDF types don't match"), + } + + assert_eq!( + data.master_key_wrapped_user_key.to_string(), + deserialized.master_key_wrapped_user_key.to_string() + ); + assert_eq!(data.salt, deserialized.salt); + } +} diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 10be377a8..3fc1bb5bf 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,8 +18,13 @@ mod crypto_client; #[cfg(feature = "internal")] pub use crypto_client::CryptoClient; +#[cfg(feature = "internal")] +pub mod master_password; #[cfg(feature = "internal")] mod security_state; +#[cfg(feature = "internal")] +pub mod user_decryption; + #[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs new file mode 100644 index 000000000..c752cf193 --- /dev/null +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -0,0 +1,149 @@ +//! Mobile specific user decryption operations +//! +//! This module contains the data structures and error handling for user decryption operations, + +use bitwarden_api_api::models::UserDecryptionResponseModel; +use bitwarden_error::bitwarden_error; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; + +/// Error for master user decryption related operations. +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum UserDecryptionError { + #[error(transparent)] + MasterPasswordError(#[from] MasterPasswordError), +} + +/// Represents data required to decrypt user's vault. +/// Currently, this is only used for master password unlock. +#[allow(missing_docs)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct UserDecryptionData { + pub master_password_unlock: Option, +} + +impl TryFrom for UserDecryptionData { + type Error = UserDecryptionError; + + fn try_from(response: UserDecryptionResponseModel) -> Result { + let master_password_unlock = response + .master_password_unlock + .map(|response| MasterPasswordUnlockData::try_from(*response)) + .transpose()?; + + Ok(UserDecryptionData { + master_password_unlock, + }) + } +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockResponseModel}; + use bitwarden_crypto::Kdf; + + use super::*; + + const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_SALT: &str = "test@example.com"; + + #[test] + fn test_try_from_user_decryption_response_model_success() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { + kdf: Box::new( + bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }, + ), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + })), + }; + + let result = UserDecryptionData::try_from(response); + assert!(result.is_ok()); + + let user_decryption_data = result.unwrap(); + + assert!(user_decryption_data.master_password_unlock.is_some()); + + let master_password_unlock = user_decryption_data.master_password_unlock.unwrap(); + + match master_password_unlock.kdf { + Kdf::Argon2id { + iterations, + memory, + parallelism, + } => { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } + _ => panic!("Expected Argon2id KDF"), + } + + assert_eq!(master_password_unlock.salt, TEST_SALT); + assert_eq!( + master_password_unlock + .master_key_wrapped_user_key + .to_string(), + TEST_USER_KEY + ); + } + + #[test] + fn test_try_from_user_decryption_response_model_master_password_unlock_none_success() { + let response = UserDecryptionResponseModel { + master_password_unlock: None, + }; + + let result = UserDecryptionData::try_from(response); + assert!(result.is_ok()); + + let user_decryption_data = result.unwrap(); + + assert!(user_decryption_data.master_password_unlock.is_none()); + } + + #[test] + fn test_try_from_user_decryption_response_model_missing_field_error() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { + kdf: Box::new( + bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: None, + parallelism: None, + }, + ), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + })), + }; + + let result = UserDecryptionData::try_from(response); + assert!(matches!( + result, + Err(UserDecryptionError::MasterPasswordError( + MasterPasswordError::MissingField(_) + )) + )); + } +} diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index a85384f6e..87da34516 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } base64 = ">=0.22.1, <0.23.0" +bitwarden-api-api = { workspace = true, features = ["wasm"] } bitwarden-auth = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } diff --git a/crates/bitwarden-wasm-internal/src/custom_types.rs b/crates/bitwarden-wasm-internal/src/custom_types.rs index f910d1173..e70056983 100644 --- a/crates/bitwarden-wasm-internal/src/custom_types.rs +++ b/crates/bitwarden-wasm-internal/src/custom_types.rs @@ -30,4 +30,9 @@ export type Utc = unknown; * An integer that is known not to equal zero. */ export type NonZeroU32 = number; + +/** + * An interger that is valid KdfType + */ +export type KdfType = number; "#; diff --git a/crates/bitwarden-wasm-internal/src/key_management/mod.rs b/crates/bitwarden-wasm-internal/src/key_management/mod.rs new file mode 100644 index 000000000..021fa70d6 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/key_management/mod.rs @@ -0,0 +1 @@ +mod user_decryption; diff --git a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs new file mode 100644 index 000000000..dd527d0c6 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs @@ -0,0 +1,24 @@ +use bitwarden_api_api::models::{MasterPasswordUnlockResponseModel, UserDecryptionResponseModel}; +use bitwarden_core::key_management::{ + master_password::{MasterPasswordError, MasterPasswordUnlockData}, + user_decryption::{UserDecryptionData, UserDecryptionError}, +}; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub struct UserDecryption {} + +#[wasm_bindgen] +impl UserDecryption { + pub fn get_user_decryption_data( + response: UserDecryptionResponseModel, + ) -> Result { + UserDecryptionData::try_from(response) + } + + pub fn get_master_password_unlock_data( + response: MasterPasswordUnlockResponseModel, + ) -> Result { + MasterPasswordUnlockData::try_from(response) + } +} diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index 253ec8ffd..0e9951b4f 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -3,6 +3,7 @@ mod client; mod custom_types; mod init; +mod key_management; mod platform; mod pure_crypto; mod ssh; diff --git a/support/openapi-template/Cargo.mustache b/support/openapi-template/Cargo.mustache index cdaf12fc9..4a71adb53 100644 --- a/support/openapi-template/Cargo.mustache +++ b/support/openapi-template/Cargo.mustache @@ -14,16 +14,24 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true +[features] +default = [] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support + [dependencies] -serde = { version = "^1.0", features = ["derive"] } +serde = { workspace = true, features = ["derive"] } {{#serdeWith}} -serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } +serde_with = { version = ">=3.8, <4", default-features = false, features = [ + "base64", + "std", + "macros" +] } {{/serdeWith}} -serde_json = "^1.0" -serde_repr = "^0.1" -url = "^2.5" +serde_json = { workspace = true } +serde_repr = { workspace = true } +url = ">=2.5, <3" {{#hasUUIDs}} -uuid = { version = "^1.8", features = ["serde", "v4"] } +uuid = { workspace = true, features = ["serde", "v4", "js"] } {{/hasUUIDs}} {{#hyper}} {{#hyper0x}} @@ -46,27 +54,39 @@ secrecy = "0.8.0" {{/withAWSV4Signature}} {{#reqwest}} {{^supportAsync}} -reqwest = { version = "^0.12", default-features = false, features = ["json", "blocking", "multipart", "http2"] } +reqwest = { workspace = true, features = [ + "json", + "blocking", + "multipart", + "http2", +], default-features = false } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "blocking", "multipart"] } {{/supportMiddleware}} {{/supportAsync}} {{#supportAsync}} -reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } +reqwest = { workspace = true, features = [ + "json", + "multipart", + "http2", +], default-features = false } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} -async-trait = "^0.1" +async-trait = { workspace = true } # TODO: propose to Yoshidan to externalize this as non google related crate, so that it can easily be extended for other cloud providers. google-cloud-token = "^0.1" {{/supportTokenSource}} {{/supportAsync}} {{/reqwest}} {{#reqwestTrait}} -async-trait = "^0.1" -reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } -{{#supportMiddleware}} +async-trait = { workspace = true } +reqwest = { workspace = true, features = [ + "json", + "multipart", + "http2", +], default-features = false }{{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} @@ -90,3 +110,5 @@ mockall = ["dep:mockall"] bon = ["dep:bon"] {{/useBonBuilder}} {{/reqwestTrait}} +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } +wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } \ No newline at end of file diff --git a/support/openapi-template/model.mustache b/support/openapi-template/model.mustache index 8652cfe1c..f10433332 100644 --- a/support/openapi-template/model.mustache +++ b/support/openapi-template/model.mustache @@ -3,6 +3,10 @@ use crate::models; use serde::{Deserialize, Serialize}; {{#models}} {{#model}} +{{#vendorExtensions.x-sdk-wasm}} +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; +{{/vendorExtensions.x-sdk-wasm}} {{^isEnum}}{{#vendorExtensions.x-rust-has-byte-array}} use serde_with::serde_as; {{/vendorExtensions.x-rust-has-byte-array}}{{/isEnum}} @@ -20,6 +24,13 @@ use serde_repr::{Serialize_repr,Deserialize_repr}; /// {{{description}}} #[repr(i64)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -45,6 +56,13 @@ impl std::fmt::Display for {{{classname}}} { {{^isInteger}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -78,6 +96,13 @@ impl Default for {{{classname}}} { {{#discriminator}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "{{{propertyBaseName}}}")] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{^oneOf}} {{#mappedModels}} @@ -123,6 +148,13 @@ impl Default for {{classname}} { {{^discriminator}} {{#vendorExtensions.x-rust-has-byte-array}}#[serde_as] {{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub struct {{{classname}}} { {{#vars}} {{#description}} @@ -180,6 +212,13 @@ impl {{{classname}}} { {{/description}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{classname}} { {{#composedSchemas.oneOf}} {{#description}} @@ -202,6 +241,13 @@ impl Default for {{classname}} { {{#isEnum}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{enumName}}} { {{#allowableValues}} {{#enumVars}}