diff --git a/Cargo.lock b/Cargo.lock index 375bf3687..5c70657f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,16 +506,23 @@ dependencies = [ name = "bitwarden-auth" version = "2.0.0" dependencies = [ + "bitwarden-api-api", "bitwarden-core", + "bitwarden-crypto", + "bitwarden-encoding", "bitwarden-error", "bitwarden-test", "chrono", "reqwest", "serde", + "serde_bytes", "serde_json", "thiserror 1.0.69", "tokio", + "tracing", "tsify", + "uniffi", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "wiremock", diff --git a/crates/bitwarden-api-api/src/models/keys_request_model.rs b/crates/bitwarden-api-api/src/models/keys_request_model.rs index 316d0f860..955b0ed79 100644 --- a/crates/bitwarden-api-api/src/models/keys_request_model.rs +++ b/crates/bitwarden-api-api/src/models/keys_request_model.rs @@ -18,6 +18,12 @@ pub struct KeysRequestModel { pub public_key: String, #[serde(rename = "encryptedPrivateKey", alias = "EncryptedPrivateKey")] pub encrypted_private_key: String, + #[serde( + rename = "accountKeys", + alias = "AccountKeys", + skip_serializing_if = "Option::is_none" + )] + pub account_keys: Option>, } impl KeysRequestModel { @@ -25,6 +31,7 @@ impl KeysRequestModel { KeysRequestModel { public_key, encrypted_private_key, + account_keys: None, } } } diff --git a/crates/bitwarden-api-identity/src/models/account_keys_request_model.rs b/crates/bitwarden-api-identity/src/models/account_keys_request_model.rs new file mode 100644 index 000000000..98f14010b --- /dev/null +++ b/crates/bitwarden-api-identity/src/models/account_keys_request_model.rs @@ -0,0 +1,57 @@ +/* + * Bitwarden Identity + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: v1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountKeysRequestModel { + #[serde( + rename = "userKeyEncryptedAccountPrivateKey", + alias = "UserKeyEncryptedAccountPrivateKey" + )] + pub user_key_encrypted_account_private_key: Option, + #[serde(rename = "accountPublicKey", alias = "AccountPublicKey")] + pub account_public_key: Option, + #[serde( + rename = "publicKeyEncryptionKeyPair", + alias = "PublicKeyEncryptionKeyPair", + skip_serializing_if = "Option::is_none" + )] + pub public_key_encryption_key_pair: Option>, + #[serde( + rename = "signatureKeyPair", + alias = "SignatureKeyPair", + skip_serializing_if = "Option::is_none" + )] + pub signature_key_pair: Option>, + #[serde( + rename = "securityState", + alias = "SecurityState", + skip_serializing_if = "Option::is_none" + )] + pub security_state: Option>, +} + +impl AccountKeysRequestModel { + pub fn new( + user_key_encrypted_account_private_key: Option, + account_public_key: Option, + ) -> AccountKeysRequestModel { + AccountKeysRequestModel { + user_key_encrypted_account_private_key, + account_public_key, + public_key_encryption_key_pair: None, + signature_key_pair: None, + security_state: None, + } + } +} diff --git a/crates/bitwarden-api-identity/src/models/keys_request_model.rs b/crates/bitwarden-api-identity/src/models/keys_request_model.rs index 5362cbdc1..a6e9cd3df 100644 --- a/crates/bitwarden-api-identity/src/models/keys_request_model.rs +++ b/crates/bitwarden-api-identity/src/models/keys_request_model.rs @@ -18,6 +18,12 @@ pub struct KeysRequestModel { pub public_key: String, #[serde(rename = "encryptedPrivateKey", alias = "EncryptedPrivateKey")] pub encrypted_private_key: String, + #[serde( + rename = "accountKeys", + alias = "AccountKeys", + skip_serializing_if = "Option::is_none" + )] + pub account_keys: Option>, } impl KeysRequestModel { @@ -25,6 +31,7 @@ impl KeysRequestModel { KeysRequestModel { public_key, encrypted_private_key, + account_keys: None, } } } diff --git a/crates/bitwarden-api-identity/src/models/mod.rs b/crates/bitwarden-api-identity/src/models/mod.rs index 500492f30..9174f6f6b 100644 --- a/crates/bitwarden-api-identity/src/models/mod.rs +++ b/crates/bitwarden-api-identity/src/models/mod.rs @@ -10,6 +10,14 @@ pub mod kdf_type; pub use self::kdf_type::KdfType; pub mod keys_request_model; pub use self::keys_request_model::KeysRequestModel; +pub mod account_keys_request_model; +pub use self::account_keys_request_model::AccountKeysRequestModel; +pub mod public_key_encryption_key_pair_request_model; +pub use self::public_key_encryption_key_pair_request_model::PublicKeyEncryptionKeyPairRequestModel; +pub mod signature_key_pair_request_model; +pub use self::signature_key_pair_request_model::SignatureKeyPairRequestModel; +pub mod security_state_model; +pub use self::security_state_model::SecurityStateModel; pub mod password_prelogin_request_model; pub use self::password_prelogin_request_model::PasswordPreloginRequestModel; pub mod password_prelogin_response_model; diff --git a/crates/bitwarden-api-identity/src/models/public_key_encryption_key_pair_request_model.rs b/crates/bitwarden-api-identity/src/models/public_key_encryption_key_pair_request_model.rs new file mode 100644 index 000000000..07d6afa77 --- /dev/null +++ b/crates/bitwarden-api-identity/src/models/public_key_encryption_key_pair_request_model.rs @@ -0,0 +1,40 @@ +/* + * Bitwarden Identity + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: v1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct PublicKeyEncryptionKeyPairRequestModel { + #[serde(rename = "wrappedPrivateKey", alias = "WrappedPrivateKey")] + pub wrapped_private_key: Option, + #[serde(rename = "publicKey", alias = "PublicKey")] + pub public_key: Option, + #[serde( + rename = "signedPublicKey", + alias = "SignedPublicKey", + skip_serializing_if = "Option::is_none" + )] + pub signed_public_key: Option, +} + +impl PublicKeyEncryptionKeyPairRequestModel { + pub fn new( + wrapped_private_key: Option, + public_key: Option, + ) -> PublicKeyEncryptionKeyPairRequestModel { + PublicKeyEncryptionKeyPairRequestModel { + wrapped_private_key, + public_key, + signed_public_key: None, + } + } +} diff --git a/crates/bitwarden-api-identity/src/models/security_state_model.rs b/crates/bitwarden-api-identity/src/models/security_state_model.rs new file mode 100644 index 000000000..74e558e0a --- /dev/null +++ b/crates/bitwarden-api-identity/src/models/security_state_model.rs @@ -0,0 +1,30 @@ +/* + * Bitwarden Identity + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: v1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SecurityStateModel { + #[serde(rename = "securityState", alias = "SecurityState")] + pub security_state: Option, + #[serde(rename = "securityVersion", alias = "SecurityVersion")] + pub security_version: i32, +} + +impl SecurityStateModel { + pub fn new(security_state: Option, security_version: i32) -> SecurityStateModel { + SecurityStateModel { + security_state, + security_version, + } + } +} diff --git a/crates/bitwarden-api-identity/src/models/signature_key_pair_request_model.rs b/crates/bitwarden-api-identity/src/models/signature_key_pair_request_model.rs new file mode 100644 index 000000000..4509af850 --- /dev/null +++ b/crates/bitwarden-api-identity/src/models/signature_key_pair_request_model.rs @@ -0,0 +1,37 @@ +/* + * Bitwarden Identity + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: v1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::{Deserialize, Serialize}; + +use crate::models; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SignatureKeyPairRequestModel { + #[serde(rename = "signatureAlgorithm", alias = "SignatureAlgorithm")] + pub signature_algorithm: Option, + #[serde(rename = "wrappedSigningKey", alias = "WrappedSigningKey")] + pub wrapped_signing_key: Option, + #[serde(rename = "verifyingKey", alias = "VerifyingKey")] + pub verifying_key: Option, +} + +impl SignatureKeyPairRequestModel { + pub fn new( + signature_algorithm: Option, + wrapped_signing_key: Option, + verifying_key: Option, + ) -> SignatureKeyPairRequestModel { + SignatureKeyPairRequestModel { + signature_algorithm, + wrapped_signing_key, + verifying_key, + } + } +} diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 416b18449..fdaaaec31 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -15,8 +15,10 @@ license-file.workspace = true keywords.workspace = true [features] +uniffi = ["dep:uniffi"] # Uniffi bindings wasm = [ "bitwarden-core/wasm", + "bitwarden-crypto/wasm", "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures" @@ -24,13 +26,20 @@ wasm = [ # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. [dependencies] +bitwarden-api-api = { workspace = true } bitwarden-core = { workspace = true, features = ["internal"] } +bitwarden-crypto = { workspace = true } +bitwarden-encoding = { workspace = true } bitwarden-error = { workspace = true } chrono = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } +serde_bytes = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } tsify = { workspace = true, optional = true } +uniffi = { workspace = true, optional = true } +uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true } diff --git a/crates/bitwarden-auth/src/lib.rs b/crates/bitwarden-auth/src/lib.rs index db5dc561f..87c20820e 100644 --- a/crates/bitwarden-auth/src/lib.rs +++ b/crates/bitwarden-auth/src/lib.rs @@ -1,5 +1,8 @@ #![doc = include_str!("../README.md")] +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); + mod auth_client; pub mod identity; diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index b1b7e9e76..002338565 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -5,12 +5,24 @@ //! authentication method such as SSO or master password, and a decryption method such as //! key-connector, TDE, or master password. -use bitwarden_core::Client; +use std::str::FromStr; + +use bitwarden_api_api::models::{ + DeviceKeysRequestModel, KeysRequestModel, OrganizationUserResetPasswordEnrollmentRequestModel, +}; +use bitwarden_core::{ + Client, UserId, key_management::account_cryptographic_state::WrappedAccountCryptographicState, +}; +use bitwarden_encoding::B64; +use bitwarden_error::bitwarden_error; +use thiserror::Error; +use tracing::info; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; /// Client for initializing a user account. #[derive(Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Object))] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct RegistrationClient { #[allow(dead_code)] @@ -22,6 +34,8 @@ impl RegistrationClient { Self { client } } } + +#[cfg_attr(feature = "uniffi", uniffi::export)] #[cfg_attr(feature = "wasm", wasm_bindgen)] impl RegistrationClient { /// Example method to demonstrate usage of the client. @@ -33,4 +47,129 @@ impl RegistrationClient { let api_client = &client.get_api_configurations().await.api_client; // Do API request here. It will be authenticated using the client's tokens. } + + /// Initializes a new cryptographic state for a user and posts it to the server; enrolls in + /// admin password reset and finally enrolls the user to TDE unlock. + pub async fn post_keys_for_tde_registration( + &self, + org_id: String, + org_public_key: B64, + // Note: Ideally these would be set for the register client, however no such functionality + // exists at the moment + user_id: String, + device_id: String, + trust_device: bool, + ) -> Result { + let client = &self.client.internal; + let api_client = &client.get_api_configurations().await.api_client; + let user_id = + UserId::from_str(user_id.as_str()).map_err(|_| UserRegistrationError::Serialization)?; + + // First call crypto API to get all keys + info!("Initializing account cryptography"); + let ( + cryptography_state, + user_key, + account_cryptographic_state_request, + device_key_set, + reset_password_key, + ) = self + .client + .crypto() + .make_user_tde_registration(user_id, org_public_key) + .map_err(|_| UserRegistrationError::Crypto)?; + + // Post the generated keys to the API here. The user now has keys and is "registered", but + // has no unlock method. + let request = KeysRequestModel { + account_keys: Some(Box::new(account_cryptographic_state_request.clone())), + // Note: This property is deprecated and will be removed + public_key: account_cryptographic_state_request + .account_public_key + .ok_or(UserRegistrationError::Crypto)?, + // Note: This property is deprecated and will be removed + encrypted_private_key: account_cryptographic_state_request + .user_key_encrypted_account_private_key + .ok_or(UserRegistrationError::Crypto)?, + }; + info!("Posting user account cryptographic state to server"); + api_client + .accounts_api() + .post_keys(Some(request)) + .await + .map_err(|_| UserRegistrationError::Api)?; + + // Next, enroll the user for reset password using the reset password key generated above. + info!("Enrolling into admin account recovery"); + api_client + .organization_users_api() + .put_reset_password_enrollment( + uuid::Uuid::parse_str(&org_id).map_err(|_| UserRegistrationError::Serialization)?, + user_id.into(), + Some(OrganizationUserResetPasswordEnrollmentRequestModel { + reset_password_key: Some(reset_password_key.to_string()), + master_password_hash: None, + }), + ) + .await + .map_err(|_| UserRegistrationError::Api)?; + + if trust_device { + // Next, enroll the user for TDE unlock + info!("Enrolling into trusted device decryption"); + api_client + .devices_api() + .put_keys( + device_id.as_str(), + Some(DeviceKeysRequestModel::new( + device_key_set.protected_user_key.to_string(), + device_key_set.protected_device_private_key.to_string(), + device_key_set.protected_device_public_key.to_string(), + )), + ) + .await + .map_err(|_| UserRegistrationError::Api)?; + } + + info!("User initialized!"); + // Note: This passing out of state and keys is temporary. Once SDK state management is more + // mature, the account cryptographic state and keys should be set directly here. + Ok(TdeRegistrationResponse { + account_cryptographic_state: cryptography_state, + device_key: device_key_set.device_key.to_string(), + user_key: user_key.to_encoded().to_vec().into(), + }) + } +} + +/// Result of TDE registration process. +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct TdeRegistrationResponse { + /// The account cryptographic state of the user + pub account_cryptographic_state: WrappedAccountCryptographicState, + /// The device key + pub device_key: String, + /// The decrypted user key. This can be used to get the consuming client to an unlocked state. + pub user_key: B64, +} + +/// Errors that can occur during user registration. +#[derive(Debug, Error)] +#[bitwarden_error(flat)] +pub enum UserRegistrationError { + /// API call failed. + #[error("Api call failed")] + Api, + /// Cryptography initialization failed. + #[error("Cryptography initialization failed")] + Crypto, + /// Serialization or deserialization error + #[error("Serialization error")] + Serialization, } diff --git a/crates/bitwarden-auth/src/send_access/access_token_response.rs b/crates/bitwarden-auth/src/send_access/access_token_response.rs index 29e7cdbc8..b21138c44 100644 --- a/crates/bitwarden-auth/src/send_access/access_token_response.rs +++ b/crates/bitwarden-auth/src/send_access/access_token_response.rs @@ -2,6 +2,9 @@ use std::fmt::Debug; use crate::send_access::api::{SendAccessTokenApiErrorResponse, SendAccessTokenApiSuccessResponse}; +#[cfg(feature = "uniffi")] +uniffi::custom_newtype!(UnexpectedIdentityError, String); + /// A send access token which can be used to access a send. #[derive(serde::Serialize, serde::Deserialize, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/bitwarden-auth/src/send_access/api/token_api_error_response.rs b/crates/bitwarden-auth/src/send_access/api/token_api_error_response.rs index 1c17cca0c..2a69dd606 100644 --- a/crates/bitwarden-auth/src/send_access/api/token_api_error_response.rs +++ b/crates/bitwarden-auth/src/send_access/api/token_api_error_response.rs @@ -4,6 +4,7 @@ use tsify::Tsify; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] /// Invalid request errors - typically due to missing parameters. pub enum SendAccessTokenInvalidRequestError { @@ -26,6 +27,7 @@ pub enum SendAccessTokenInvalidRequestError { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] /// Invalid grant errors - typically due to invalid credentials. pub enum SendAccessTokenInvalidGrantError { @@ -51,6 +53,7 @@ pub enum SendAccessTokenInvalidGrantError { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] #[serde(tag = "error")] // ^ "error" becomes the variant discriminator which matches against the rename annotations; diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index 9c6c9ba6f..052151127 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -52,7 +52,12 @@ impl_bitwarden_error!(IdentityError, ApiError); pub struct NotAuthenticatedError; /// Client's user ID is already set. -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize, serde::Deserialize, Clone)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] #[error("The client user ID is already set")] pub struct UserIdAlreadySetError; diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index a2afc8e17..d52004776 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,9 +1,16 @@ +use std::sync::RwLock; + +use bitwarden_api_api::models::AccountKeysRequestModel; #[cfg(feature = "wasm")] use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; -use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet}; +use bitwarden_crypto::{ + AsymmetricPublicCryptoKey, CryptoError, Decryptable, DeviceKey, Kdf, KeyStore, + RotateableKeySet, SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, +}; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; use bitwarden_encoding::B64; +use bitwarden_error::bitwarden_error; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -22,13 +29,18 @@ use crate::key_management::{ }, }; use crate::{ - Client, + Client, UserId, client::encryption_settings::EncryptionSettingsError, error::StatefulCryptoError, - key_management::crypto::{ - CryptoClientError, EnrollPinResponse, UpdateKdfResponse, UserCryptoV2KeysResponse, - enroll_pin, get_v2_rotated_account_keys, make_update_kdf, make_update_password, - make_v2_keys_for_v1_user, + key_management::{ + account_cryptographic_state::{ + AccountCryptographyInitializationError, WrappedAccountCryptographicState, + }, + crypto::{ + CryptoClientError, EnrollPinResponse, UpdateKdfResponse, UserCryptoV2KeysResponse, + enroll_pin, get_v2_rotated_account_keys, make_update_kdf, make_update_password, + make_v2_keys_for_v1_user, + }, }, }; @@ -193,6 +205,77 @@ impl CryptoClient { ) -> Result { derive_key_connector(request) } + + /// Creates a new V2 account cryptographic state for TDE registration. + /// This generates fresh cryptographic keys (private key, signing key, signed public key, + /// and security state) wrapped with a new user key. + /// + /// Returns the wrapped account cryptographic state that can be used for registration. + /// The user key is not returned but is set in the client's key store. + pub fn make_user_tde_registration( + &self, + user_id: UserId, + org_public_key: B64, + ) -> Result< + ( + WrappedAccountCryptographicState, + SymmetricCryptoKey, + AccountKeysRequestModel, + TrustDeviceResponse, + UnsignedSharedKey, + ), + MakeKeysError, + > { + let mut ctx = self.client.internal.get_key_store().context_mut(); + let (user_key, wrapped_state) = + WrappedAccountCryptographicState::make(&mut ctx, user_id) + .map_err(MakeKeysError::AccountCryptographyInitialization)?; + #[expect(deprecated)] + let user_key = ctx.dangerous_get_symmetric_key(user_key)?; + + // TDE unlock method + let device_key = DeviceKey::trust_device(user_key)?; + + // Account recovery enrollment + let public_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(&org_public_key)) + .map_err(MakeKeysError::Crypto)?; + let admin_reset = UnsignedSharedKey::encapsulate_key_unsigned(user_key, &public_key) + .map_err(MakeKeysError::Crypto)?; + + let store = KeyStore::default(); + let mut ctx = store.context_mut(); + let user_key_id = ctx.add_local_symmetric_key(user_key.to_owned()); + let security_state = RwLock::new(None); + wrapped_state + .set_to_context(&security_state, user_key_id, &store, ctx) + .map_err(MakeKeysError::AccountCryptographyInitialization)?; + + let cryptography_state_request_model = wrapped_state + .to_request_model(&store) + .map_err(|_| MakeKeysError::RequestModelCreation)?; + + Ok(( + wrapped_state, + user_key.to_owned(), + cryptography_state_request_model, + device_key, + admin_reset, + )) + } +} + +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum MakeKeysError { + /// Failed to initialize account cryptography + #[error("Failed to initialize account cryptography")] + AccountCryptographyInitialization(AccountCryptographyInitializationError), + #[error("Failed to make a request model")] + RequestModelCreation, + /// Generic crypto error + #[error("Cryptography error: {0}")] + Crypto(#[from] CryptoError), } impl Client { @@ -206,10 +289,18 @@ impl Client { #[cfg(test)] mod tests { - use bitwarden_crypto::{BitwardenLegacyKeyBytes, SymmetricCryptoKey}; + use std::num::NonZeroU32; + + use bitwarden_crypto::{ + AsymmetricCryptoKey, BitwardenLegacyKeyBytes, PublicKeyEncryptionAlgorithm, + SymmetricCryptoKey, + }; use super::*; - use crate::client::test_accounts::test_bitwarden_com_account; + use crate::{ + client::test_accounts::test_bitwarden_com_account, + key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + }; #[tokio::test] async fn test_enroll_pin_envelope() { @@ -236,7 +327,71 @@ mod tests { ) .unwrap(), ); - let user_key_final = SymmetricCryptoKey::try_from(&secret).unwrap(); + let user_key_final = SymmetricCryptoKey::try_from(&secret).expect("valid user key"); assert_eq!(user_key_initial, user_key_final); } + + #[tokio::test] + async fn test_make_user_tde_registration() { + let user_id = UserId::new_v4(); + let email = "test@bitwarden.com"; + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).expect("valid iteration count"), + }; + + // Generate a mock organization public key for TDE enrollment + let org_key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + let org_public_key_der = org_key + .to_public_key() + .to_der() + .expect("valid public key DER"); + let org_public_key = B64::from(org_public_key_der.as_ref().to_vec()); + + // Create a client and generate TDE registration keys + let registration_client = Client::new(None); + let (wrapped_state, _user_key, _account_keys_request, device_key, admin_reset) = + registration_client + .crypto() + .make_user_tde_registration(user_id, org_public_key) + .expect("TDE registration should succeed"); + + // Initialize a new client using the TDE device key + let unlock_client = Client::new(None); + unlock_client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + user_id: Some(user_id), + kdf_params: kdf, + email: email.to_owned(), + account_cryptographic_state: wrapped_state, + method: InitUserCryptoMethod::DeviceKey { + device_key: device_key.device_key.to_string(), + protected_device_private_key: device_key.protected_device_private_key, + device_protected_user_key: device_key.protected_user_key, + }, + }) + .await + .expect("initializing user crypto with TDE device key should succeed"); + + // Verify we can retrieve the user encryption key + let retrieved_key = unlock_client + .crypto() + .get_user_encryption_key() + .await + .expect("should be able to get user encryption key"); + + // The retrieved key should be a valid symmetric key + let retrieved_symmetric_key = SymmetricCryptoKey::try_from(retrieved_key) + .expect("retrieved key should be valid symmetric key"); + + // Verify that the org key can decrypt the admin_reset UnsignedSharedKey + // and that the decrypted key matches the user's encryption key + let decrypted_user_key = admin_reset + .decapsulate_key_unsigned(&org_key) + .expect("org key should be able to decrypt admin reset key"); + assert_eq!( + retrieved_symmetric_key, decrypted_user_key, + "decrypted admin reset key should match the user's encryption key" + ); + } } diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs index a9b8be12d..3e82f6958 100644 --- a/crates/bitwarden-crypto/src/keys/device_key.rs +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -1,4 +1,5 @@ use bitwarden_encoding::B64; +use serde::{Deserialize, Serialize}; use super::{AsymmetricCryptoKey, PublicKeyEncryptionAlgorithm}; use crate::{ @@ -14,8 +15,14 @@ use crate::{ pub struct DeviceKey(SymmetricCryptoKey); #[allow(missing_docs)] -#[derive(Debug)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +#[derive(Serialize, Deserialize)] pub struct TrustDeviceResponse { /// Base64 encoded device key pub device_key: B64,