diff --git a/Cargo.lock b/Cargo.lock index 24f977de9..e67b077b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,17 +526,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 2.0.12", "tokio", + "tracing", "tsify", "uniffi", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "wiremock", diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index d0b94c621..acf4d9440 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -17,6 +17,7 @@ keywords.workspace = true [features] wasm = [ "bitwarden-core/wasm", + "bitwarden-crypto/wasm", "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", @@ -25,18 +26,25 @@ uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] # Uniffi bindings # 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 } [dev-dependencies] +bitwarden-api-api = { workspace = true, features = ["mockall"] } bitwarden-test = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["rt"] } diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index b1b7e9e76..71d7250fc 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -5,12 +5,45 @@ //! 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 bitwarden_api_api::models::{ + DeviceKeysRequestModel, KeysRequestModel, OrganizationUserResetPasswordEnrollmentRequestModel, +}; +use bitwarden_core::{ + Client, OrganizationId, 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::*; +/// Request parameters for TDE (Trusted Device Encryption) registration. +#[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 TdeRegistrationRequest { + /// Organization ID to enroll in + pub org_id: OrganizationId, + /// Organization's public key for encrypting the reset password key. This should be verified by + /// the client and not verifying may compromise the security of the user's account. + pub org_public_key: B64, + /// User ID for the account being initialized + pub user_id: UserId, + /// Device identifier for TDE enrollment + pub device_identifier: String, + /// Whether to trust this device for TDE + pub trust_device: bool, +} + /// 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,15 +55,447 @@ 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. - /// Note: This will be removed once real methods are implemented. - #[allow(unused)] - async fn example(&self) { + /// 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, + request: TdeRegistrationRequest, + ) -> Result { let client = &self.client.internal; - #[allow(unused_variables)] let api_client = &client.get_api_configurations().await.api_client; - // Do API request here. It will be authenticated using the client's tokens. + internal_post_keys_for_tde_registration(self, api_client, request).await + } +} + +async fn internal_post_keys_for_tde_registration( + registration_client: &RegistrationClient, + api_client: &bitwarden_api_api::apis::ApiClient, + request: TdeRegistrationRequest, +) -> Result { + // First call crypto API to get all keys + info!("Initializing account cryptography"); + let tde_registration_crypto_result = registration_client + .client + .crypto() + .make_user_tde_registration(request.user_id, request.org_public_key.clone()) + .map_err(|_| RegistrationError::Crypto)?; + + // Post the generated keys to the API here. The user now has keys and is "registered", but + // has no unlock method. + let keys_request = KeysRequestModel { + account_keys: Some(Box::new( + tde_registration_crypto_result.account_keys_request.clone(), + )), + // Note: This property is deprecated and will be removed + public_key: tde_registration_crypto_result + .account_keys_request + .account_public_key + .ok_or(RegistrationError::Crypto)?, + // Note: This property is deprecated and will be removed + encrypted_private_key: tde_registration_crypto_result + .account_keys_request + .user_key_encrypted_account_private_key + .ok_or(RegistrationError::Crypto)?, + }; + info!("Posting user account cryptographic state to server"); + api_client + .accounts_api() + .post_keys(Some(keys_request)) + .await + .map_err(|e| { + tracing::error!("Failed to post account keys: {e:?}"); + RegistrationError::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( + request.org_id.into(), + request.user_id.into(), + Some(OrganizationUserResetPasswordEnrollmentRequestModel { + reset_password_key: Some( + tde_registration_crypto_result + .reset_password_key + .to_string(), + ), + master_password_hash: None, + }), + ) + .await + .map_err(|e| { + tracing::error!("Failed to enroll for reset password: {e:?}"); + RegistrationError::Api + })?; + + if request.trust_device { + // Next, enroll the user for TDE unlock + info!("Enrolling into trusted device decryption"); + api_client + .devices_api() + .put_keys( + request.device_identifier.as_str(), + Some(DeviceKeysRequestModel::new( + tde_registration_crypto_result + .trusted_device_keys + .protected_user_key + .to_string(), + tde_registration_crypto_result + .trusted_device_keys + .protected_device_private_key + .to_string(), + tde_registration_crypto_result + .trusted_device_keys + .protected_device_public_key + .to_string(), + )), + ) + .await + .map_err(|e| { + tracing::error!("Failed to enroll device for TDE: {e:?}"); + RegistrationError::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: tde_registration_crypto_result.account_cryptographic_state, + device_key: tde_registration_crypto_result + .trusted_device_keys + .device_key, + user_key: tde_registration_crypto_result + .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: B64, + /// 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 RegistrationError { + /// API call failed. + #[error("Api call failed")] + Api, + /// Cryptography initialization failed. + #[error("Cryptography initialization failed")] + Crypto, +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::{ + apis::ApiClient, + models::{DeviceResponseModel, KeysResponseModel}, + }; + use bitwarden_core::Client; + + use super::*; + + const TEST_USER_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8"; + const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8"; + const TEST_DEVICE_ID: &str = "test-device-id"; + + const TEST_ORG_PUBLIC_KEY: &[u8] = &[ + 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, + 48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148, + 115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250, + 229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134, + 243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92, + 88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100, + 76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88, + 195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130, + 14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39, + 80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213, + 211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16, + 12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144, + 255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190, + 248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108, + 201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1, + ]; + + #[tokio::test] + async fn test_post_keys_for_tde_registration_success() { + let client = Client::new(None); + let registration_client = RegistrationClient::new(client); + + let api_client = ApiClient::new_mocked(|mock| { + mock.accounts_api + .expect_post_keys() + .once() + .returning(move |_body| { + Ok(KeysResponseModel { + object: None, + key: None, + public_key: None, + private_key: None, + account_keys: None, + }) + }); + mock.organization_users_api + .expect_put_reset_password_enrollment() + .once() + .returning(move |_org_id, _user_id, _body| Ok(())); + mock.devices_api + .expect_put_keys() + .once() + .returning(move |_device_id, _body| { + Ok(DeviceResponseModel { + object: None, + id: None, + name: None, + r#type: None, + identifier: None, + creation_date: None, + is_trusted: None, + encrypted_user_key: None, + encrypted_public_key: None, + }) + }); + }); + + let request = TdeRegistrationRequest { + org_id: TEST_ORG_ID.parse().unwrap(), + org_public_key: TEST_ORG_PUBLIC_KEY.into(), + user_id: TEST_USER_ID.parse().unwrap(), + device_identifier: TEST_DEVICE_ID.to_string(), + trust_device: true, + }; + + let result = + internal_post_keys_for_tde_registration(®istration_client, &api_client, request) + .await; + + assert!(result.is_ok()); + // Assert that the mock expectations were met + if let ApiClient::Mock(mut mock) = api_client { + mock.accounts_api.checkpoint(); + mock.organization_users_api.checkpoint(); + mock.devices_api.checkpoint(); + } + } + + #[tokio::test] + async fn test_post_keys_for_tde_registration_trust_device_false() { + let client = Client::new(None); + let registration_client = RegistrationClient::new(client); + + let api_client = ApiClient::new_mocked(|mock| { + mock.accounts_api + .expect_post_keys() + .once() + .returning(move |_body| { + Ok(KeysResponseModel { + object: None, + key: None, + public_key: None, + private_key: None, + account_keys: None, + }) + }); + mock.organization_users_api + .expect_put_reset_password_enrollment() + .once() + .returning(move |_org_id, _user_id, _body| Ok(())); + // Explicitly expect that put_keys is never called when trust_device is false + mock.devices_api.expect_put_keys().never(); + }); + + let request = TdeRegistrationRequest { + org_id: TEST_ORG_ID.parse().unwrap(), + org_public_key: TEST_ORG_PUBLIC_KEY.into(), + user_id: TEST_USER_ID.parse().unwrap(), + device_identifier: TEST_DEVICE_ID.to_string(), + trust_device: false, // trust_device is false + }; + + let result = + internal_post_keys_for_tde_registration(®istration_client, &api_client, request) + .await; + + assert!(result.is_ok()); + // Assert that the mock expectations were met (put_keys should not have been called) + if let ApiClient::Mock(mut mock) = api_client { + mock.accounts_api.checkpoint(); + mock.organization_users_api.checkpoint(); + mock.devices_api.checkpoint(); + } + } + + #[tokio::test] + async fn test_post_keys_for_tde_registration_post_keys_failure() { + let client = Client::new(None); + let registration_client = RegistrationClient::new(client); + + let api_client = ApiClient::new_mocked(|mock| { + mock.accounts_api + .expect_post_keys() + .once() + .returning(move |_body| { + Err(bitwarden_api_api::apis::Error::Serde( + serde_json::Error::io(std::io::Error::other("API error")), + )) + }); + // Subsequent API calls should not be made if post_keys fails + mock.organization_users_api + .expect_put_reset_password_enrollment() + .never(); + mock.devices_api.expect_put_keys().never(); + }); + + let request = TdeRegistrationRequest { + org_id: TEST_ORG_ID.parse().unwrap(), + org_public_key: TEST_ORG_PUBLIC_KEY.into(), + user_id: TEST_USER_ID.parse().unwrap(), + device_identifier: TEST_DEVICE_ID.to_string(), + trust_device: true, + }; + + let result = + internal_post_keys_for_tde_registration(®istration_client, &api_client, request) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); + + // Assert that the mock expectations were met + if let ApiClient::Mock(mut mock) = api_client { + mock.accounts_api.checkpoint(); + mock.organization_users_api.checkpoint(); + mock.devices_api.checkpoint(); + } + } + + #[tokio::test] + async fn test_post_keys_for_tde_registration_reset_password_enrollment_failure() { + let client = Client::new(None); + let registration_client = RegistrationClient::new(client); + + let api_client = ApiClient::new_mocked(|mock| { + mock.accounts_api + .expect_post_keys() + .once() + .returning(move |_body| { + Ok(KeysResponseModel { + object: None, + key: None, + public_key: None, + private_key: None, + account_keys: None, + }) + }); + mock.organization_users_api + .expect_put_reset_password_enrollment() + .once() + .returning(move |_org_id, _user_id, _body| { + Err(bitwarden_api_api::apis::Error::Serde( + serde_json::Error::io(std::io::Error::other("API error")), + )) + }); + // Device key enrollment should not be made if reset password enrollment fails + mock.devices_api.expect_put_keys().never(); + }); + + let request = TdeRegistrationRequest { + org_id: TEST_ORG_ID.parse().unwrap(), + org_public_key: TEST_ORG_PUBLIC_KEY.into(), + user_id: TEST_USER_ID.parse().unwrap(), + device_identifier: TEST_DEVICE_ID.to_string(), + trust_device: true, + }; + + let result = + internal_post_keys_for_tde_registration(®istration_client, &api_client, request) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); + + // Assert that the mock expectations were met + if let ApiClient::Mock(mut mock) = api_client { + mock.accounts_api.checkpoint(); + mock.organization_users_api.checkpoint(); + mock.devices_api.checkpoint(); + } + } + + #[tokio::test] + async fn test_post_keys_for_tde_registration_device_keys_failure() { + let client = Client::new(None); + let registration_client = RegistrationClient::new(client); + + let api_client = ApiClient::new_mocked(|mock| { + mock.accounts_api + .expect_post_keys() + .once() + .returning(move |_body| { + Ok(KeysResponseModel { + object: None, + key: None, + public_key: None, + private_key: None, + account_keys: None, + }) + }); + mock.organization_users_api + .expect_put_reset_password_enrollment() + .once() + .returning(move |_org_id, _user_id, _body| Ok(())); + mock.devices_api + .expect_put_keys() + .once() + .returning(move |_device_id, _body| { + Err(bitwarden_api_api::apis::Error::Serde( + serde_json::Error::io(std::io::Error::other("API error")), + )) + }); + }); + + let request = TdeRegistrationRequest { + org_id: TEST_ORG_ID.parse().unwrap(), + org_public_key: TEST_ORG_PUBLIC_KEY.into(), + user_id: TEST_USER_ID.parse().unwrap(), + device_identifier: TEST_DEVICE_ID.to_string(), + trust_device: true, // trust_device is true, so device enrollment should be attempted + }; + + let result = + internal_post_keys_for_tde_registration(®istration_client, &api_client, request) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); + + // Assert that the mock expectations were met + if let ApiClient::Mock(mut mock) = api_client { + mock.accounts_api.checkpoint(); + mock.organization_users_api.checkpoint(); + mock.devices_api.checkpoint(); + } } } diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index 9c6c9ba6f..442d77c2b 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -52,7 +52,7 @@ 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)] #[error("The client user ID is already set")] pub struct UserIdAlreadySetError; diff --git a/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs index fd058d5aa..892f1e2c6 100644 --- a/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs +++ b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs @@ -12,9 +12,8 @@ use std::sync::RwLock; use bitwarden_api_api::models::{AccountKeysRequestModel, SecurityStateModel}; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext, + CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext, PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm, - VerifyingKey, }; use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; @@ -100,44 +99,41 @@ impl WrappedAccountCryptographicState { /// user key required to unlock this state. pub fn to_request_model( &self, - store: &KeyStore, + user_key: &SymmetricKeyId, + ctx: &mut KeyStoreContext, ) -> Result { - let verifying_key = self.verifying_key(store)?; + let private_key = match self { + WrappedAccountCryptographicState::V1 { private_key } + | WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(), + }; + let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?; + let public_key = ctx.get_public_key(private_key_tmp_id)?; + + let signature_keypair = match self { + WrappedAccountCryptographicState::V1 { .. } => None, + WrappedAccountCryptographicState::V2 { signing_key, .. } => { + let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?; + let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?; + Some((signing_key.clone(), verifying_key)) + } + }; + Ok(AccountKeysRequestModel { // Note: This property is deprecated and should be removed after a transition period. - user_key_encrypted_account_private_key: match self { - WrappedAccountCryptographicState::V1 { private_key } - | WrappedAccountCryptographicState::V2 { private_key, .. } => { - Some(private_key.to_string()) - } - }, + user_key_encrypted_account_private_key: Some(private_key.to_string()), // Note: This property is deprecated and should be removed after a transition period. - account_public_key: match self.public_key(store)? { - Some(pk) => Some(B64::from(pk.to_der()?).to_string()), - None => None, - }, - signature_key_pair: match self { - WrappedAccountCryptographicState::V1 { .. } => None, - WrappedAccountCryptographicState::V2 { signing_key, .. } => Some(Box::new( - bitwarden_api_api::models::SignatureKeyPairRequestModel { + account_public_key: Some(B64::from(public_key.to_der()?).to_string()), + signature_key_pair: signature_keypair + .as_ref() + .map(|(signing_key, verifying_key)| { + Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel { wrapped_signing_key: Some(signing_key.to_string()), - verifying_key: Some( - B64::from( - verifying_key - .as_ref() - .map(|vk| vk.to_cose()) - .ok_or(AccountCryptographyInitializationError::CorruptData)?, - ) - .to_string(), - ), - signature_algorithm: verifying_key.as_ref().map(|vk| { - match vk.algorithm() { - SignatureAlgorithm::Ed25519 => "ed25519".to_string(), - } + verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()), + signature_algorithm: Some(match verifying_key.algorithm() { + SignatureAlgorithm::Ed25519 => "ed25519".to_string(), }), - }, - )), - }, + }) + }), public_key_encryption_key_pair: Some(Box::new( bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel { wrapped_private_key: match self { @@ -146,29 +142,25 @@ impl WrappedAccountCryptographicState { Some(private_key.to_string()) } }, - public_key: match self.public_key(store) { - Ok(Some(pk)) => Some(B64::from(pk.to_der()?).to_string()), - _ => None, - }, + public_key: Some(B64::from(public_key.to_der()?).to_string()), signed_public_key: match self.signed_public_key() { Ok(Some(spk)) => Some(spk.clone().into()), _ => None, }, }, )), - security_state: match self { - WrappedAccountCryptographicState::V1 { .. } => None, - WrappedAccountCryptographicState::V2 { security_state, .. } => { - // ensure we have a verifying key reference and convert the verified state's - // version to i32 for the API model - let vk_ref = verifying_key - .as_ref() - .ok_or(AccountCryptographyInitializationError::CorruptData)?; + security_state: match (self, signature_keypair.as_ref()) { + (_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None, + ( + WrappedAccountCryptographicState::V2 { security_state, .. }, + Some((_, verifying_key)), + ) => { + // Convert the verified state's version to i32 for the API model Some(Box::new(SecurityStateModel { security_state: Some(security_state.into()), security_version: security_state - .clone() - .verify_and_unwrap(vk_ref) + .to_owned() + .verify_and_unwrap(verifying_key) .map_err(|_| AccountCryptographyInitializationError::TamperedData)? .version() as i32, })) @@ -286,46 +278,6 @@ impl WrappedAccountCryptographicState { Ok(()) } - /// Retrieve the verifying key from the wrapped state, if present. This requires the user key to - /// be present in the store. - fn verifying_key( - &self, - store: &KeyStore, - ) -> Result, AccountCryptographyInitializationError> { - match self { - WrappedAccountCryptographicState::V1 { .. } => Ok(None), - WrappedAccountCryptographicState::V2 { signing_key, .. } => { - let mut ctx = store.context_mut(); - let signing_key = ctx - .unwrap_signing_key(SymmetricKeyId::User, signing_key) - .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?; - ctx.get_verifying_key(signing_key) - .map(Some) - .map_err(|e| e.into()) - } - } - } - - /// Retrieve the public key from the wrapped state, if present. This requires the user key to - /// be present in the store. - fn public_key( - &self, - store: &KeyStore, - ) -> Result, AccountCryptographyInitializationError> { - match self { - WrappedAccountCryptographicState::V1 { private_key } - | WrappedAccountCryptographicState::V2 { private_key, .. } => { - let mut ctx = store.context_mut(); - let private_key = ctx - .unwrap_private_key(SymmetricKeyId::User, private_key) - .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?; - ctx.get_public_key(private_key) - .map(Some) - .map_err(|e| e.into()) - } - } - } - /// Retrieve the signed public key from the wrapped state, if present. fn signed_public_key( &self, @@ -472,9 +424,12 @@ mod tests { wrapped_account_cryptography_state .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx) .unwrap(); + + let mut ctx = temp_store.context_mut(); let model = wrapped_account_cryptography_state - .to_request_model(&temp_store) + .to_request_model(&SymmetricKeyId::User, &mut ctx) .expect("to_private_keys_request_model should succeed"); + drop(ctx); let ctx = temp_store.context(); diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 040668b2f..8be76d1d1 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -6,12 +6,13 @@ use std::collections::HashMap; +use bitwarden_api_api::models::AccountKeysRequestModel; use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, RotateableKeySet, - SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, - UnsignedSharedKey, UserKey, dangerous_get_v2_rotated_account_keys, - derive_symmetric_key_from_prf, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseSerializable, CryptoError, DeviceKey, + EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, + PrimitiveEncryptable, RotateableKeySet, SignatureAlgorithm, SignedPublicKey, SigningKey, + SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, + dangerous_get_v2_rotated_account_keys, derive_symmetric_key_from_prf, safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeError}, }; use bitwarden_encoding::B64; @@ -28,7 +29,9 @@ use crate::{ error::StatefulCryptoError, key_management::{ AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, - account_cryptographic_state::WrappedAccountCryptographicState, + account_cryptographic_state::{ + AccountCryptographyInitializationError, WrappedAccountCryptographicState, + }, master_password::{MasterPasswordAuthenticationData, MasterPasswordUnlockData}, }, }; @@ -848,11 +851,78 @@ pub(crate) fn get_v2_rotated_account_keys( }) } +/// The response from `make_user_tde_registration`. +pub struct MakeTdeRegistrationResponse { + /// The account cryptographic state + pub account_cryptographic_state: WrappedAccountCryptographicState, + /// The user's user key + pub user_key: SymmetricCryptoKey, + /// The request model for the account cryptographic state (also called Account Keys) + pub account_keys_request: AccountKeysRequestModel, + /// The keys needed to set up TDE decryption + pub trusted_device_keys: TrustDeviceResponse, + /// The key needed for admin password reset + pub reset_password_key: UnsignedSharedKey, +} + +/// Errors that can occur when making keys for TDE registration. +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum MakeKeysError { + /// Failed to initialize account cryptography + #[error("Failed to initialize account cryptography")] + AccountCryptographyInitialization(AccountCryptographyInitializationError), + /// Failed to create request model + #[error("Failed to make a request model")] + RequestModelCreation, + /// Generic crypto error + #[error("Cryptography error: {0}")] + Crypto(#[from] CryptoError), +} + +/// Create the data needed to register for TDE (Trusted Device Enrollment) +pub(crate) fn make_user_tde_registration( + client: &Client, + user_id: UserId, + org_public_key: B64, +) -> Result { + let mut ctx = client.internal.get_key_store().context_mut(); + let (user_key_id, wrapped_state) = WrappedAccountCryptographicState::make(&mut ctx, user_id) + .map_err(MakeKeysError::AccountCryptographyInitialization)?; + // TDE unlock method + #[expect(deprecated)] + let device_key = DeviceKey::trust_device(ctx.dangerous_get_symmetric_key(user_key_id)?)?; + + // Account recovery enrollment + let public_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(&org_public_key)) + .map_err(MakeKeysError::Crypto)?; + #[expect(deprecated)] + let admin_reset = UnsignedSharedKey::encapsulate_key_unsigned( + ctx.dangerous_get_symmetric_key(user_key_id)?, + &public_key, + ) + .map_err(MakeKeysError::Crypto)?; + + let cryptography_state_request_model = wrapped_state + .to_request_model(&user_key_id, &mut ctx) + .map_err(|_| MakeKeysError::RequestModelCreation)?; + + #[expect(deprecated)] + Ok(MakeTdeRegistrationResponse { + account_cryptographic_state: wrapped_state, + user_key: ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned(), + account_keys_request: cryptography_state_request_model, + trusted_device_keys: device_key, + reset_password_key: admin_reset, + }) +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; - use bitwarden_crypto::{RsaKeyPair, SymmetricKeyAlgorithm}; + use bitwarden_crypto::{PublicKeyEncryptionAlgorithm, RsaKeyPair, SymmetricKeyAlgorithm}; use super::*; use crate::Client; @@ -1575,4 +1645,75 @@ mod tests { panic!("Expected username login method"); } } + + #[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 make_keys_response = 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: make_keys_response.account_cryptographic_state, + method: InitUserCryptoMethod::DeviceKey { + device_key: make_keys_response + .trusted_device_keys + .device_key + .to_string(), + protected_device_private_key: make_keys_response + .trusted_device_keys + .protected_device_private_key, + device_protected_user_key: make_keys_response + .trusted_device_keys + .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_key UnsignedSharedKey + // and that the decrypted key matches the user's encryption key + let decrypted_user_key = make_keys_response + .reset_password_key + .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-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index a2afc8e17..8a221fa55 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -22,12 +22,13 @@ 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, + CryptoClientError, EnrollPinResponse, MakeKeysError, MakeTdeRegistrationResponse, + UpdateKdfResponse, UserCryptoV2KeysResponse, enroll_pin, get_v2_rotated_account_keys, + make_update_kdf, make_update_password, make_user_tde_registration, make_v2_keys_for_v1_user, }, }; @@ -193,6 +194,17 @@ 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. + pub fn make_user_tde_registration( + &self, + user_id: UserId, + org_public_key: B64, + ) -> Result { + make_user_tde_registration(&self.client, user_id, org_public_key) + } } impl Client { @@ -236,7 +248,7 @@ 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); } } 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,