From ac1e990b1867d76aac603178a75122f02ffbf88a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 14:46:18 +0100 Subject: [PATCH 01/29] Implement TDE v2 registration --- Cargo.lock | 6 + .../src/models/keys_request_model.rs | 7 + .../src/models/account_keys_request_model.rs | 57 +++++++ .../src/models/keys_request_model.rs | 7 + .../bitwarden-api-identity/src/models/mod.rs | 8 + ...c_key_encryption_key_pair_request_model.rs | 40 +++++ .../src/models/security_state_model.rs | 30 ++++ .../signature_key_pair_request_model.rs | 37 +++++ crates/bitwarden-auth/Cargo.toml | 9 +- crates/bitwarden-auth/src/registration.rs | 139 +++++++++++++++++- crates/bitwarden-core/src/error.rs | 7 +- .../src/key_management/crypto_client.rs | 95 +++++++++++- .../bitwarden-crypto/src/keys/device_key.rs | 9 +- 13 files changed, 441 insertions(+), 10 deletions(-) create mode 100644 crates/bitwarden-api-identity/src/models/account_keys_request_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/public_key_encryption_key_pair_request_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/security_state_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/signature_key_pair_request_model.rs diff --git a/Cargo.lock b/Cargo.lock index 375bf3687..70caa8e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,16 +506,22 @@ 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", + "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..7b59fb14c 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" @@ -24,13 +25,19 @@ 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-error = { workspace = true } +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 } +uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true } diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index b1b7e9e76..3d5cc1c44 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -5,7 +5,19 @@ //! 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 serde_bytes::ByteBuf; +use thiserror::Error; +use tracing::info; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -33,4 +45,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; + #[allow(unused_variables)] + 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(TdeRegistrationResult { + 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) +)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +pub struct TdeRegistrationResult { + /// 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: ByteBuf, +} + +/// 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-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..b21471e34 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 { 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, From 5d6067a9c019787dcd591ad5ce6c596bceac22fb Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 15:03:53 +0100 Subject: [PATCH 02/29] Add tests --- .../src/key_management/crypto_client.rs | 78 ++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index b21471e34..d52004776 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -289,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() { @@ -319,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" + ); + } } From 9cdb0b2b3a229c2c72725da43dcf226892de1ff9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 17:35:45 +0100 Subject: [PATCH 03/29] Cleanup cargo toml --- crates/bitwarden-auth/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 7b59fb14c..606bbe01a 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -20,7 +20,7 @@ wasm = [ "bitwarden-crypto/wasm", "dep:tsify", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures" + "dep:wasm-bindgen-futures", ] # WASM support # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. @@ -29,7 +29,7 @@ bitwarden-api-api = { workspace = true } bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } bitwarden-encoding = { workspace = true } -bitwarden-error.workspace = true +bitwarden-error = { workspace = true } chrono = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } From 9ff75beac0329b8918ee6ad27d58168fc9264b55 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 17:36:15 +0100 Subject: [PATCH 04/29] Cleanup unused --- crates/bitwarden-auth/src/registration.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 3d5cc1c44..a99e175ae 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -59,7 +59,6 @@ impl RegistrationClient { trust_device: bool, ) -> Result { let client = &self.client.internal; - #[allow(unused_variables)] let api_client = &client.get_api_configurations().await.api_client; let user_id = UserId::from_str(user_id.as_str()).map_err(|_| UserRegistrationError::Serialization)?; From 571c9cd433ae69bcc4c5a8598e8299b1c7b4397a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 18:00:36 +0100 Subject: [PATCH 05/29] Add uniffi --- Cargo.lock | 1 + crates/bitwarden-auth/Cargo.toml | 2 ++ crates/bitwarden-auth/src/lib.rs | 3 +++ crates/bitwarden-auth/src/registration.rs | 7 +++++-- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70caa8e56..5c70657f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -521,6 +521,7 @@ dependencies = [ "tokio", "tracing", "tsify", + "uniffi", "uuid", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 606bbe01a..673ea3ce5 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -15,6 +15,7 @@ license-file.workspace = true keywords.workspace = true [features] +uniffi = ["dep:uniffi"] # Uniffi bindings wasm = [ "bitwarden-core/wasm", "bitwarden-crypto/wasm", @@ -37,6 +38,7 @@ 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 a99e175ae..3445d58a6 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -15,7 +15,6 @@ use bitwarden_core::{ }; use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; -use serde_bytes::ByteBuf; use thiserror::Error; use tracing::info; #[cfg(feature = "wasm")] @@ -23,6 +22,7 @@ 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)] @@ -34,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. @@ -146,6 +148,7 @@ impl RegistrationClient { 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 TdeRegistrationResult { /// The account cryptographic state of the user @@ -153,7 +156,7 @@ pub struct TdeRegistrationResult { /// 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: ByteBuf, + pub user_key: B64, } /// Errors that can occur during user registration. From 2d21cfe6359244e4b5ecb14af113777547adb98c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 18:04:36 +0100 Subject: [PATCH 06/29] Rename result to response --- crates/bitwarden-auth/src/registration.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 3445d58a6..002338565 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -59,7 +59,7 @@ impl RegistrationClient { user_id: String, device_id: String, trust_device: bool, - ) -> Result { + ) -> Result { let client = &self.client.internal; let api_client = &client.get_api_configurations().await.api_client; let user_id = @@ -134,7 +134,7 @@ impl RegistrationClient { 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(TdeRegistrationResult { + 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(), @@ -150,7 +150,7 @@ impl RegistrationClient { )] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] -pub struct TdeRegistrationResult { +pub struct TdeRegistrationResponse { /// The account cryptographic state of the user pub account_cryptographic_state: WrappedAccountCryptographicState, /// The device key From a43b6241cd8f95772859be0bb265b54e97d9b3dc Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 19:05:30 +0100 Subject: [PATCH 07/29] Fix uniffi build --- crates/bitwarden-auth/src/send_access/access_token_response.rs | 3 +++ .../src/send_access/api/token_api_error_response.rs | 3 +++ 2 files changed, 6 insertions(+) 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; From df619e49b428db1813e59ba75c3b62c039732520 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 11 Dec 2025 19:12:12 +0100 Subject: [PATCH 08/29] Run cargo sort on workspace --- crates/bitwarden-auth/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 673ea3ce5..fdaaaec31 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -21,7 +21,7 @@ wasm = [ "bitwarden-crypto/wasm", "dep:tsify", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures", + "dep:wasm-bindgen-futures" ] # WASM support # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. From 09accdde902481fa62f18a2b8b85fdacb0af8504 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 10:15:12 +0100 Subject: [PATCH 09/29] Fix build and replace device key type --- crates/bitwarden-auth/Cargo.toml | 3 +-- crates/bitwarden-auth/src/registration.rs | 4 ++-- .../bitwarden-auth/src/send_access/access_token_response.rs | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 999f20da8..82493827f 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -15,13 +15,12 @@ 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" + "dep:wasm-bindgen-futures", ] # WASM support uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] # Uniffi bindings diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 002338565..db9ae0349 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -136,7 +136,7 @@ impl RegistrationClient { // 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(), + device_key: device_key_set.device_key, user_key: user_key.to_encoded().to_vec().into(), }) } @@ -154,7 +154,7 @@ pub struct TdeRegistrationResponse { /// The account cryptographic state of the user pub account_cryptographic_state: WrappedAccountCryptographicState, /// The device key - pub device_key: String, + 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, } 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 bf47d7f57..43dd56a8f 100644 --- a/crates/bitwarden-auth/src/send_access/access_token_response.rs +++ b/crates/bitwarden-auth/src/send_access/access_token_response.rs @@ -2,9 +2,6 @@ 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)] From 9ec7a32334b9ac41fd02dcbf4e3fd74d5c2fd074 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 10:34:59 +0100 Subject: [PATCH 10/29] Apply feedback --- crates/bitwarden-auth/src/registration.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index db9ae0349..167aef971 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -11,7 +11,8 @@ use bitwarden_api_api::models::{ DeviceKeysRequestModel, KeysRequestModel, OrganizationUserResetPasswordEnrollmentRequestModel, }; use bitwarden_core::{ - Client, UserId, key_management::account_cryptographic_state::WrappedAccountCryptographicState, + Client, OrganizationId, UserId, + key_management::account_cryptographic_state::WrappedAccountCryptographicState, }; use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; @@ -52,18 +53,16 @@ impl RegistrationClient { /// admin password reset and finally enrolls the user to TDE unlock. pub async fn post_keys_for_tde_registration( &self, - org_id: String, + org_id: OrganizationId, 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, + user_id: UserId, 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"); @@ -104,7 +103,7 @@ impl RegistrationClient { api_client .organization_users_api() .put_reset_password_enrollment( - uuid::Uuid::parse_str(&org_id).map_err(|_| UserRegistrationError::Serialization)?, + org_id.into(), user_id.into(), Some(OrganizationUserResetPasswordEnrollmentRequestModel { reset_password_key: Some(reset_password_key.to_string()), @@ -169,7 +168,4 @@ pub enum UserRegistrationError { /// Cryptography initialization failed. #[error("Cryptography initialization failed")] Crypto, - /// Serialization or deserialization error - #[error("Serialization error")] - Serialization, } From 25a017ddd209655c22a3287bfa2f524bac804409 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 12:41:00 +0100 Subject: [PATCH 11/29] Mock api and add tests --- crates/bitwarden-auth/Cargo.toml | 1 + crates/bitwarden-auth/src/registration.rs | 487 ++++++++++++++---- .../account_cryptographic_state.rs | 117 ++--- .../src/key_management/crypto_client.rs | 35 +- 4 files changed, 454 insertions(+), 186 deletions(-) diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 82493827f..acf4d9440 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -44,6 +44,7 @@ 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 167aef971..e3f895fd9 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -5,8 +5,6 @@ //! authentication method such as SSO or master password, and a decryption method such as //! key-connector, TDE, or master password. -use std::str::FromStr; - use bitwarden_api_api::models::{ DeviceKeysRequestModel, KeysRequestModel, OrganizationUserResetPasswordEnrollmentRequestModel, }; @@ -21,6 +19,28 @@ 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 ID for TDE enrollment + pub device_id: 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))] @@ -39,106 +59,97 @@ impl RegistrationClient { #[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) { - 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. - } - /// 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: OrganizationId, - org_public_key: B64, - // Note: Ideally these would be set for the register client, however no such functionality - // exists at the moment - user_id: UserId, - device_id: String, - trust_device: bool, + request: TdeRegistrationRequest, ) -> Result { let client = &self.client.internal; let api_client = &client.get_api_configurations().await.api_client; + internal_post_keys_for_tde_registration(&self, api_client, request).await + } +} - // 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)?; +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 ( + cryptography_state, + user_key, + account_cryptographic_state_request, + device_key_set, + reset_password_key, + ) = registration_client + .client + .crypto() + .make_user_tde_registration(request.user_id, request.org_public_key.clone()) + .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 keys_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(keys_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( + request.org_id.into(), + request.user_id.into(), + Some(OrganizationUserResetPasswordEnrollmentRequestModel { + reset_password_key: Some(reset_password_key.to_string()), + master_password_hash: None, + }), + ) + .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"); + if request.trust_device { + // Next, enroll the user for TDE unlock + info!("Enrolling into trusted device decryption"); api_client - .organization_users_api() - .put_reset_password_enrollment( - org_id.into(), - user_id.into(), - Some(OrganizationUserResetPasswordEnrollmentRequestModel { - reset_password_key: Some(reset_password_key.to_string()), - master_password_hash: None, - }), + .devices_api() + .put_keys( + request.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)?; - - 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, - user_key: user_key.to_encoded().to_vec().into(), - }) } + + 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, + user_key: user_key.to_encoded().to_vec().into(), + }) } /// Result of TDE registration process. @@ -169,3 +180,303 @@ pub enum UserRegistrationError { #[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, + }) + }); + 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_id: 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, + }) + }); + 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_id: 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::new( + std::io::ErrorKind::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_id: 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(), UserRegistrationError::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, + }) + }); + 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::new( + std::io::ErrorKind::Other, + "API error", + )), + )) + }); + // Device key enrollment should not be made if reset password enrollment fails + mock.devices_api.expect_put_keys().times(0).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_id: 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(), UserRegistrationError::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, + }) + }); + mock.organization_users_api + .expect_put_reset_password_enrollment() + .times(1) + .once() + .returning(move |_org_id, _user_id, _body| Ok(())); + mock.devices_api + .expect_put_keys() + .times(1) + .once() + .returning(move |_device_id, _body| { + Err(bitwarden_api_api::apis::Error::Serde( + serde_json::Error::io(std::io::Error::new( + std::io::ErrorKind::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_id: 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(), UserRegistrationError::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/key_management/account_cryptographic_state.rs b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs index fd423f9ec..0f6fe6818 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,40 +99,45 @@ 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()), + account_public_key: Some(B64::from(public_key.to_der()?).to_string()), + signature_key_pair: match signature_keypair.as_ref() { None => None, - }, - signature_key_pair: match self { - WrappedAccountCryptographicState::V1 { .. } => None, - WrappedAccountCryptographicState::V2 { signing_key, .. } => Some(Box::new( + Some((signing_key, verifying_key)) => Some(Box::new( bitwarden_api_api::models::SignatureKeyPairRequestModel { wrapped_signing_key: Some(signing_key.to_string()), verifying_key: Some( B64::from( - verifying_key - .as_ref() + Some(verifying_key) .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(), - } + signature_algorithm: Some(verifying_key).map(|vk| match vk.algorithm() { + SignatureAlgorithm::Ed25519 => "ed25519".to_string(), }), }, )), @@ -146,29 +150,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, })) @@ -281,46 +281,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, @@ -467,9 +427,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_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index d52004776..d04cb4b2b 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,11 +1,9 @@ -use std::sync::RwLock; - use bitwarden_api_api::models::AccountKeysRequestModel; #[cfg(feature = "wasm")] use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, CryptoError, Decryptable, DeviceKey, Kdf, KeyStore, - RotateableKeySet, SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, + AsymmetricPublicCryptoKey, CryptoError, Decryptable, DeviceKey, Kdf, RotateableKeySet, + SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; @@ -227,37 +225,32 @@ impl CryptoClient { MakeKeysError, > { let mut ctx = self.client.internal.get_key_store().context_mut(); - let (user_key, wrapped_state) = + let (user_key_id, 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)?; + #[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)?; - 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)?; + #[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(&store) + .to_request_model(&user_key_id, &mut ctx) .map_err(|_| MakeKeysError::RequestModelCreation)?; + #[expect(deprecated)] Ok(( wrapped_state, - user_key.to_owned(), + ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned(), cryptography_state_request_model, device_key, admin_reset, From 34f632edda49fcfc75c6815d54a305edb77ac6d4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 12:49:20 +0100 Subject: [PATCH 12/29] Add error tracing for api requests --- crates/bitwarden-auth/src/registration.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index e3f895fd9..408030983 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -108,7 +108,10 @@ async fn internal_post_keys_for_tde_registration( .accounts_api() .post_keys(Some(keys_request)) .await - .map_err(|_| UserRegistrationError::Api)?; + .map_err(|e| { + tracing::error!("Failed to post account keys: {e:?}"); + UserRegistrationError::Api + })?; // Next, enroll the user for reset password using the reset password key generated above. info!("Enrolling into admin account recovery"); @@ -123,7 +126,10 @@ async fn internal_post_keys_for_tde_registration( }), ) .await - .map_err(|_| UserRegistrationError::Api)?; + .map_err(|e| { + tracing::error!("Failed to enroll for reset password: {e:?}"); + UserRegistrationError::Api + })?; if request.trust_device { // Next, enroll the user for TDE unlock @@ -139,7 +145,10 @@ async fn internal_post_keys_for_tde_registration( )), ) .await - .map_err(|_| UserRegistrationError::Api)?; + .map_err(|e| { + tracing::error!("Failed to enroll device for TDE: {e:?}"); + UserRegistrationError::Api + })?; } info!("User initialized!"); From c14935bf061cee738578f2a6c37b9dc14def25d7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 12:57:11 +0100 Subject: [PATCH 13/29] Apply clippy --- crates/bitwarden-auth/src/registration.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 408030983..4032504d8 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -67,7 +67,7 @@ impl RegistrationClient { ) -> Result { let client = &self.client.internal; let api_client = &client.get_api_configurations().await.api_client; - internal_post_keys_for_tde_registration(&self, api_client, request).await + internal_post_keys_for_tde_registration(self, api_client, request).await } } @@ -339,8 +339,7 @@ mod tests { .once() .returning(move |_body| { Err(bitwarden_api_api::apis::Error::Serde( - serde_json::Error::io(std::io::Error::new( - std::io::ErrorKind::Other, + serde_json::Error::io(std::io::Error::other( "API error", )), )) @@ -397,8 +396,7 @@ mod tests { .once() .returning(move |_org_id, _user_id, _body| { Err(bitwarden_api_api::apis::Error::Serde( - serde_json::Error::io(std::io::Error::new( - std::io::ErrorKind::Other, + serde_json::Error::io(std::io::Error::other( "API error", )), )) @@ -458,8 +456,7 @@ mod tests { .once() .returning(move |_device_id, _body| { Err(bitwarden_api_api::apis::Error::Serde( - serde_json::Error::io(std::io::Error::new( - std::io::ErrorKind::Other, + serde_json::Error::io(std::io::Error::other( "API error", )), )) From abe43b5cef67883924cf9eb425676db697d1a0a6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 13:23:56 +0100 Subject: [PATCH 14/29] Clean up tests --- crates/bitwarden-auth/src/registration.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 4032504d8..9f881bbfe 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -339,9 +339,7 @@ mod tests { .once() .returning(move |_body| { Err(bitwarden_api_api::apis::Error::Serde( - serde_json::Error::io(std::io::Error::other( - "API error", - )), + serde_json::Error::io(std::io::Error::other("API error")), )) }); // Subsequent API calls should not be made if post_keys fails @@ -396,13 +394,11 @@ mod tests { .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", - )), + 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().times(0).never(); + mock.devices_api.expect_put_keys().never(); }); let request = TdeRegistrationRequest { @@ -447,18 +443,14 @@ mod tests { }); mock.organization_users_api .expect_put_reset_password_enrollment() - .times(1) .once() .returning(move |_org_id, _user_id, _body| Ok(())); mock.devices_api .expect_put_keys() - .times(1) .once() .returning(move |_device_id, _body| { Err(bitwarden_api_api::apis::Error::Serde( - serde_json::Error::io(std::io::Error::other( - "API error", - )), + serde_json::Error::io(std::io::Error::other("API error")), )) }); }); From f8eaa9a871f768c7d734622256c395e881179e85 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 23:33:52 +0100 Subject: [PATCH 15/29] Apply feedback for device identifier --- crates/bitwarden-auth/src/registration.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 9f881bbfe..854662e3e 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -36,7 +36,7 @@ pub struct TdeRegistrationRequest { /// User ID for the account being initialized pub user_id: UserId, /// Device ID for TDE enrollment - pub device_id: String, + pub device_identifier: String, /// Whether to trust this device for TDE pub trust_device: bool, } @@ -137,7 +137,7 @@ async fn internal_post_keys_for_tde_registration( api_client .devices_api() .put_keys( - request.device_id.as_str(), + request.device_identifier.as_str(), Some(DeviceKeysRequestModel::new( device_key_set.protected_user_key.to_string(), device_key_set.protected_device_private_key.to_string(), @@ -265,7 +265,7 @@ mod tests { org_id: TEST_ORG_ID.parse().unwrap(), org_public_key: TEST_ORG_PUBLIC_KEY.into(), user_id: TEST_USER_ID.parse().unwrap(), - device_id: TEST_DEVICE_ID.to_string(), + device_identifier: TEST_DEVICE_ID.to_string(), trust_device: true, }; @@ -311,7 +311,7 @@ mod tests { org_id: TEST_ORG_ID.parse().unwrap(), org_public_key: TEST_ORG_PUBLIC_KEY.into(), user_id: TEST_USER_ID.parse().unwrap(), - device_id: TEST_DEVICE_ID.to_string(), + device_identifier: TEST_DEVICE_ID.to_string(), trust_device: false, // trust_device is false }; @@ -353,7 +353,7 @@ mod tests { org_id: TEST_ORG_ID.parse().unwrap(), org_public_key: TEST_ORG_PUBLIC_KEY.into(), user_id: TEST_USER_ID.parse().unwrap(), - device_id: TEST_DEVICE_ID.to_string(), + device_identifier: TEST_DEVICE_ID.to_string(), trust_device: true, }; @@ -405,7 +405,7 @@ mod tests { org_id: TEST_ORG_ID.parse().unwrap(), org_public_key: TEST_ORG_PUBLIC_KEY.into(), user_id: TEST_USER_ID.parse().unwrap(), - device_id: TEST_DEVICE_ID.to_string(), + device_identifier: TEST_DEVICE_ID.to_string(), trust_device: true, }; @@ -459,7 +459,7 @@ mod tests { org_id: TEST_ORG_ID.parse().unwrap(), org_public_key: TEST_ORG_PUBLIC_KEY.into(), user_id: TEST_USER_ID.parse().unwrap(), - device_id: TEST_DEVICE_ID.to_string(), + device_identifier: TEST_DEVICE_ID.to_string(), trust_device: true, // trust_device is true, so device enrollment should be attempted }; From f80e516a91230242074146ba231988939a5f28b8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 15 Dec 2025 23:34:37 +0100 Subject: [PATCH 16/29] Fix comment --- crates/bitwarden-auth/src/registration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 854662e3e..991b12ac5 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -35,7 +35,7 @@ pub struct TdeRegistrationRequest { pub org_public_key: B64, /// User ID for the account being initialized pub user_id: UserId, - /// Device ID for TDE enrollment + /// Device identifier for TDE enrollment pub device_identifier: String, /// Whether to trust this device for TDE pub trust_device: bool, From 94202af16fe6edb626d47511d4bf437a2747b2ee Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 13:33:22 +0100 Subject: [PATCH 17/29] Remove unused wasm macro derive --- crates/bitwarden-core/src/error.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index 052151127..442d77c2b 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -53,11 +53,6 @@ pub struct NotAuthenticatedError; /// Client's user ID is already set. #[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; From 597e51d4f50d224715053205f9f868b7e25aa03d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 13:54:32 +0100 Subject: [PATCH 18/29] tmp --- crates/bitwarden-auth/src/registration.rs | 51 ++++-- .../src/key_management/crypto.rs | 155 +++++++++++++++++- .../src/key_management/crypto_client.rs | 144 +--------------- 3 files changed, 190 insertions(+), 160 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 991b12ac5..8ca195697 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -78,13 +78,7 @@ async fn internal_post_keys_for_tde_registration( ) -> Result { // 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, - ) = registration_client + let tde_registration_crypto_result = registration_client .client .crypto() .make_user_tde_registration(request.user_id, request.org_public_key.clone()) @@ -93,13 +87,17 @@ async fn internal_post_keys_for_tde_registration( // 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(account_cryptographic_state_request.clone())), + account_keys: Some(Box::new( + tde_registration_crypto_result.account_keys_request.clone(), + )), // Note: This property is deprecated and will be removed - public_key: account_cryptographic_state_request + public_key: tde_registration_crypto_result + .account_keys_request .account_public_key .ok_or(UserRegistrationError::Crypto)?, // Note: This property is deprecated and will be removed - encrypted_private_key: account_cryptographic_state_request + encrypted_private_key: tde_registration_crypto_result + .account_keys_request .user_key_encrypted_account_private_key .ok_or(UserRegistrationError::Crypto)?, }; @@ -121,7 +119,11 @@ async fn internal_post_keys_for_tde_registration( request.org_id.into(), request.user_id.into(), Some(OrganizationUserResetPasswordEnrollmentRequestModel { - reset_password_key: Some(reset_password_key.to_string()), + reset_password_key: Some( + tde_registration_crypto_result + .reset_password_key + .to_string(), + ), master_password_hash: None, }), ) @@ -139,9 +141,18 @@ async fn internal_post_keys_for_tde_registration( .put_keys( request.device_identifier.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(), + 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 @@ -155,9 +166,15 @@ async fn internal_post_keys_for_tde_registration( // 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, - user_key: user_key.to_encoded().to_vec().into(), + 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(), }) } 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 d04cb4b2b..9d3142d50 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -2,13 +2,11 @@ use bitwarden_api_api::models::AccountKeysRequestModel; #[cfg(feature = "wasm")] use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, CryptoError, Decryptable, DeviceKey, Kdf, RotateableKeySet, - SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, + CryptoError, Decryptable, Kdf, RotateableKeySet, 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::*; @@ -35,8 +33,9 @@ use crate::{ AccountCryptographyInitializationError, WrappedAccountCryptographicState, }, 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, }, }, @@ -207,70 +206,15 @@ impl CryptoClient { /// 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_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(( - wrapped_state, - ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned(), - cryptography_state_request_model, - device_key, - admin_reset, - )) + ) -> Result { + make_user_tde_registration(&self.client, user_id, org_public_key) } } -#[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 { /// Access to crypto functionality. pub fn crypto(&self) -> CryptoClient { @@ -282,18 +226,10 @@ impl Client { #[cfg(test)] mod tests { - use std::num::NonZeroU32; - - use bitwarden_crypto::{ - AsymmetricCryptoKey, BitwardenLegacyKeyBytes, PublicKeyEncryptionAlgorithm, - SymmetricCryptoKey, - }; + use bitwarden_crypto::{BitwardenLegacyKeyBytes, SymmetricCryptoKey}; use super::*; - use crate::{ - client::test_accounts::test_bitwarden_com_account, - key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, - }; + use crate::client::test_accounts::test_bitwarden_com_account; #[tokio::test] async fn test_enroll_pin_envelope() { @@ -323,68 +259,4 @@ mod tests { 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" - ); - } } From b8cf88c0851f7ea7b91842de057711e1d57d0717 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 13:58:28 +0100 Subject: [PATCH 19/29] Clippy fixes --- .../bitwarden-core/src/key_management/crypto_client.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 9d3142d50..fbb3cae89 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,8 +1,7 @@ -use bitwarden_api_api::models::AccountKeysRequestModel; #[cfg(feature = "wasm")] use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; use bitwarden_crypto::{ - CryptoError, Decryptable, Kdf, RotateableKeySet, SymmetricCryptoKey, TrustDeviceResponse, + CryptoError, Decryptable, Kdf, RotateableKeySet, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; @@ -28,17 +27,12 @@ use crate::{ Client, UserId, client::encryption_settings::EncryptionSettingsError, error::StatefulCryptoError, - key_management::{ - account_cryptographic_state::{ - AccountCryptographyInitializationError, WrappedAccountCryptographicState, - }, - crypto::{ + key_management::crypto::{ 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, }, - }, }; /// A client for the crypto operations. From 9d6f80d7c184743188e8286faa5a6fcb8a3fe909 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 13:59:28 +0100 Subject: [PATCH 20/29] Cargo fmt --- .../src/key_management/crypto_client.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index fbb3cae89..8a221fa55 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,8 +1,6 @@ #[cfg(feature = "wasm")] use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; -use bitwarden_crypto::{ - CryptoError, Decryptable, Kdf, RotateableKeySet, -}; +use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet}; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; use bitwarden_encoding::B64; @@ -28,11 +26,11 @@ use crate::{ client::encryption_settings::EncryptionSettingsError, error::StatefulCryptoError, key_management::crypto::{ - 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, - }, + 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, + }, }; /// A client for the crypto operations. From 0ab541652e1eb9735c30e1dba170e7d35b36396a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 14:03:33 +0100 Subject: [PATCH 21/29] Apply feedback --- .../src/key_management/account_cryptographic_state.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) 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 0f6fe6818..70e86b0a8 100644 --- a/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs +++ b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs @@ -128,15 +128,8 @@ impl WrappedAccountCryptographicState { Some((signing_key, verifying_key)) => Some(Box::new( bitwarden_api_api::models::SignatureKeyPairRequestModel { wrapped_signing_key: Some(signing_key.to_string()), - verifying_key: Some( - B64::from( - Some(verifying_key) - .map(|vk| vk.to_cose()) - .ok_or(AccountCryptographyInitializationError::CorruptData)?, - ) - .to_string(), - ), - signature_algorithm: Some(verifying_key).map(|vk| match vk.algorithm() { + verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()), + signature_algorithm: Some(match verifying_key.algorithm() { SignatureAlgorithm::Ed25519 => "ed25519".to_string(), }), }, From 60d4564816ac042e68e9a6867ea75e29f80fdc24 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 14:07:15 +0100 Subject: [PATCH 22/29] Cargo clippy --- .../src/key_management/account_cryptographic_state.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 70e86b0a8..718a3f00f 100644 --- a/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs +++ b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs @@ -123,9 +123,7 @@ impl WrappedAccountCryptographicState { 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: Some(B64::from(public_key.to_der()?).to_string()), - signature_key_pair: match signature_keypair.as_ref() { - None => None, - Some((signing_key, verifying_key)) => Some(Box::new( + 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.to_cose()).to_string()), @@ -134,7 +132,6 @@ impl WrappedAccountCryptographicState { }), }, )), - }, public_key_encryption_key_pair: Some(Box::new( bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel { wrapped_private_key: match self { From 3e9bb95368deba7e6dcefe02fe5ec33cdceb4544 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 14:08:46 +0100 Subject: [PATCH 23/29] Cargo fmt --- .../src/key_management/account_cryptographic_state.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 718a3f00f..75455cca7 100644 --- a/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs +++ b/crates/bitwarden-core/src/key_management/account_cryptographic_state.rs @@ -123,15 +123,17 @@ impl WrappedAccountCryptographicState { 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: 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 { + 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.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 { From c6968b9f0157119189e4bf67fa7ce1fb248cee31 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 16 Dec 2025 16:20:22 +0100 Subject: [PATCH 24/29] Rename to registration error --- crates/bitwarden-auth/src/registration.rs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 8ca195697..128bc19c8 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -64,7 +64,7 @@ impl RegistrationClient { pub async fn post_keys_for_tde_registration( &self, request: TdeRegistrationRequest, - ) -> Result { + ) -> Result { let client = &self.client.internal; let api_client = &client.get_api_configurations().await.api_client; internal_post_keys_for_tde_registration(self, api_client, request).await @@ -75,14 +75,14 @@ async fn internal_post_keys_for_tde_registration( registration_client: &RegistrationClient, api_client: &bitwarden_api_api::apis::ApiClient, request: TdeRegistrationRequest, -) -> Result { +) -> 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(|_| UserRegistrationError::Crypto)?; + .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. @@ -94,12 +94,12 @@ async fn internal_post_keys_for_tde_registration( public_key: tde_registration_crypto_result .account_keys_request .account_public_key - .ok_or(UserRegistrationError::Crypto)?, + .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(UserRegistrationError::Crypto)?, + .ok_or(RegistrationError::Crypto)?, }; info!("Posting user account cryptographic state to server"); api_client @@ -108,7 +108,7 @@ async fn internal_post_keys_for_tde_registration( .await .map_err(|e| { tracing::error!("Failed to post account keys: {e:?}"); - UserRegistrationError::Api + RegistrationError::Api })?; // Next, enroll the user for reset password using the reset password key generated above. @@ -130,7 +130,7 @@ async fn internal_post_keys_for_tde_registration( .await .map_err(|e| { tracing::error!("Failed to enroll for reset password: {e:?}"); - UserRegistrationError::Api + RegistrationError::Api })?; if request.trust_device { @@ -158,7 +158,7 @@ async fn internal_post_keys_for_tde_registration( .await .map_err(|e| { tracing::error!("Failed to enroll device for TDE: {e:?}"); - UserRegistrationError::Api + RegistrationError::Api })?; } @@ -198,7 +198,7 @@ pub struct TdeRegistrationResponse { /// Errors that can occur during user registration. #[derive(Debug, Error)] #[bitwarden_error(flat)] -pub enum UserRegistrationError { +pub enum RegistrationError { /// API call failed. #[error("Api call failed")] Api, @@ -379,7 +379,7 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), UserRegistrationError::Api)); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); // Assert that the mock expectations were met if let ApiClient::Mock(mut mock) = api_client { @@ -431,7 +431,7 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), UserRegistrationError::Api)); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); // Assert that the mock expectations were met if let ApiClient::Mock(mut mock) = api_client { @@ -485,7 +485,7 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), UserRegistrationError::Api)); + assert!(matches!(result.unwrap_err(), RegistrationError::Api)); // Assert that the mock expectations were met if let ApiClient::Mock(mut mock) = api_client { From 998a2169db74f837bc0fe7f69bc8379023c2daae Mon Sep 17 00:00:00 2001 From: Todd Martin Date: Tue, 16 Dec 2025 16:56:22 -0500 Subject: [PATCH 25/29] Updated binding docs. --- support/openapi-template/README.mustache | 29 ++++++++---------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/support/openapi-template/README.mustache b/support/openapi-template/README.mustache index 5d04c23e2..85f818ed8 100644 --- a/support/openapi-template/README.mustache +++ b/support/openapi-template/README.mustache @@ -1,14 +1,16 @@ # Rust API client for {{{packageName}}} -{{#appDescriptionWithNewLines}} -{{{.}}} -{{/appDescriptionWithNewLines}} +## Updating bindings after a server API change -{{#infoUrl}} -For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) -{{/infoUrl}} +When a server API change is made, new bindings will need to be generated to reflect this change for consumption in the SDK. -## Overview +This can be done the following ways: +1. Wait for an automatic binding update to run, which is scheduled every 2 weeks. +2. Manually run the `Update API Bindings` workflow in the `sdk-internal` repo. + +Both of these will generate a PR that will require approval from any teams whose owned code is affected by the binding updates. + +## Current binding details This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. @@ -21,15 +23,9 @@ This API client was generated by the [OpenAPI Generator](https://openapi-generat - Generator version: {{generatorVersion}} - Build package: `{{{generatorClass}}}` -## Installation -Put the package under your project folder in a directory named `{{packageName}}` and add the following to `Cargo.toml` under `[dependencies]`: -``` -{{{packageName}}} = { path = "./{{{packageName}}}" } -``` - -## Documentation for API Endpoints +## Documentation for API endpoints All URIs are relative to *{{{basePath}}}* @@ -48,8 +44,3 @@ To get access to the crate's generated documentation, use: ``` cargo doc --open ``` - -## Author - -{{#apiInfo}}{{#apis}}{{#-last}}{{{infoEmail}}} -{{/-last}}{{/apis}}{{/apiInfo}} From a6beee3661dc8fee079044cd3ba231a0e3f2b6bd Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:57:59 +0000 Subject: [PATCH 26/29] Update API bindings - 04efe402bebc5aef3b06cd40c37474be0d114634 --- .../.openapi-generator/FILES | 1 - crates/bitwarden-api-api/README.md | 33 ++-- crates/bitwarden-api-api/src/apis/mod.rs | 12 -- .../src/apis/phishing_domains_api.rs | 170 ------------------ .../src/models/keys_request_model.rs | 7 + .../src/models/keys_response_model.rs | 9 + .../.openapi-generator/FILES | 4 + crates/bitwarden-api-identity/README.md | 35 ++-- .../src/models/account_keys_request_model.rs | 57 ++++++ .../src/models/keys_request_model.rs | 7 + .../bitwarden-api-identity/src/models/mod.rs | 8 + ...c_key_encryption_key_pair_request_model.rs | 40 +++++ .../src/models/security_state_model.rs | 30 ++++ .../signature_key_pair_request_model.rs | 37 ++++ 14 files changed, 233 insertions(+), 217 deletions(-) delete mode 100644 crates/bitwarden-api-api/src/apis/phishing_domains_api.rs create mode 100644 crates/bitwarden-api-identity/src/models/account_keys_request_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/public_key_encryption_key_pair_request_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/security_state_model.rs create mode 100644 crates/bitwarden-api-identity/src/models/signature_key_pair_request_model.rs diff --git a/crates/bitwarden-api-api/.openapi-generator/FILES b/crates/bitwarden-api-api/.openapi-generator/FILES index 2c631ad8e..7f002359e 100644 --- a/crates/bitwarden-api-api/.openapi-generator/FILES +++ b/crates/bitwarden-api-api/.openapi-generator/FILES @@ -33,7 +33,6 @@ src/apis/organization_reports_api.rs src/apis/organization_sponsorships_api.rs src/apis/organization_users_api.rs src/apis/organizations_api.rs -src/apis/phishing_domains_api.rs src/apis/plans_api.rs src/apis/policies_api.rs src/apis/projects_api.rs diff --git a/crates/bitwarden-api-api/README.md b/crates/bitwarden-api-api/README.md index 3bbf7d92d..6acf795a8 100644 --- a/crates/bitwarden-api-api/README.md +++ b/crates/bitwarden-api-api/README.md @@ -1,9 +1,19 @@ # Rust API client for bitwarden-api-api -No description provided (generated by Openapi Generator -https://github.com/openapitools/openapi-generator) +## Updating bindings after a server API change -## Overview +When a server API change is made, new bindings will need to be generated to reflect this change for +consumption in the SDK. + +This can be done the following ways: + +1. Wait for an automatic binding update to run, which is scheduled every 2 weeks. +2. Manually run the `Update API Bindings` workflow in the `sdk-internal` repo. + +Both of these will generate a PR that will require approval from any teams whose owned code is +affected by the binding updates. + +## Current binding details This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API @@ -12,20 +22,11 @@ client. - API version: latest - Package version: 2.0.0 - Server Git commit: - [`196e555116aba4666ae8c9cc9080291c42c1fe46`](https://github.com/bitwarden/server/commit/196e555116aba4666ae8c9cc9080291c42c1fe46) + [`04efe402bebc5aef3b06cd40c37474be0d114634`](https://github.com/bitwarden/server/commit/04efe402bebc5aef3b06cd40c37474be0d114634) - Generator version: 7.15.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` -## Installation - -Put the package under your project folder in a directory named `bitwarden-api-api` and add the -following to `Cargo.toml` under `[dependencies]`: - -``` -bitwarden-api-api = { path = "./bitwarden-api-api" } -``` - -## Documentation for API Endpoints +## Documentation for API endpoints All URIs are relative to *https://api.bitwarden.com* @@ -342,8 +343,6 @@ All URIs are relative to *https://api.bitwarden.com* | _OrganizationsApi_ | [**put**](docs/OrganizationsApi.md#organizations_put) | **PUT** /organizations/{organizationId} | | _OrganizationsApi_ | [**put_collection_management**](docs/OrganizationsApi.md#organizations_put_collection_management) | **PUT** /organizations/{id}/collection-management | | _OrganizationsApi_ | [**rotate_api_key**](docs/OrganizationsApi.md#organizations_rotate_api_key) | **POST** /organizations/{id}/rotate-api-key | -| _PhishingDomainsApi_ | [**get_checksum**](docs/PhishingDomainsApi.md#phishing_domains_get_checksum) | **GET** /phishing-domains/checksum | -| _PhishingDomainsApi_ | [**get_phishing_domains**](docs/PhishingDomainsApi.md#phishing_domains_get_phishing_domains) | **GET** /phishing-domains | | _PlansApi_ | [**get**](docs/PlansApi.md#plans_get) | **GET** /plans | | _PlansApi_ | [**get_premium_plan**](docs/PlansApi.md#plans_get_premium_plan) | **GET** /plans/premium | | _PoliciesApi_ | [**get**](docs/PoliciesApi.md#policies_get) | **GET** /organizations/{orgId}/policies/{type} | @@ -992,5 +991,3 @@ To get access to the crate's generated documentation, use: ``` cargo doc --open ``` - -## Author diff --git a/crates/bitwarden-api-api/src/apis/mod.rs b/crates/bitwarden-api-api/src/apis/mod.rs index 0d302a7b4..3548fad35 100644 --- a/crates/bitwarden-api-api/src/apis/mod.rs +++ b/crates/bitwarden-api-api/src/apis/mod.rs @@ -144,7 +144,6 @@ pub mod organization_reports_api; pub mod organization_sponsorships_api; pub mod organization_users_api; pub mod organizations_api; -pub mod phishing_domains_api; pub mod plans_api; pub mod policies_api; pub mod projects_api; @@ -226,7 +225,6 @@ struct ApiClientReal { organization_sponsorships_api: organization_sponsorships_api::OrganizationSponsorshipsApiClient, organization_users_api: organization_users_api::OrganizationUsersApiClient, organizations_api: organizations_api::OrganizationsApiClient, - phishing_domains_api: phishing_domains_api::PhishingDomainsApiClient, plans_api: plans_api::PlansApiClient, policies_api: policies_api::PoliciesApiClient, projects_api: projects_api::ProjectsApiClient, @@ -304,7 +302,6 @@ pub struct ApiClientMock { organization_sponsorships_api::MockOrganizationSponsorshipsApi, pub organization_users_api: organization_users_api::MockOrganizationUsersApi, pub organizations_api: organizations_api::MockOrganizationsApi, - pub phishing_domains_api: phishing_domains_api::MockPhishingDomainsApi, pub plans_api: plans_api::MockPlansApi, pub policies_api: policies_api::MockPoliciesApi, pub projects_api: projects_api::MockProjectsApi, @@ -379,7 +376,6 @@ impl ApiClient { organization_sponsorships_api: organization_sponsorships_api::OrganizationSponsorshipsApiClient::new(configuration.clone()), organization_users_api: organization_users_api::OrganizationUsersApiClient::new(configuration.clone()), organizations_api: organizations_api::OrganizationsApiClient::new(configuration.clone()), - phishing_domains_api: phishing_domains_api::PhishingDomainsApiClient::new(configuration.clone()), plans_api: plans_api::PlansApiClient::new(configuration.clone()), policies_api: policies_api::PoliciesApiClient::new(configuration.clone()), projects_api: projects_api::ProjectsApiClient::new(configuration.clone()), @@ -451,7 +447,6 @@ impl ApiClient { organization_sponsorships_api: organization_sponsorships_api::MockOrganizationSponsorshipsApi::new(), organization_users_api: organization_users_api::MockOrganizationUsersApi::new(), organizations_api: organizations_api::MockOrganizationsApi::new(), - phishing_domains_api: phishing_domains_api::MockPhishingDomainsApi::new(), plans_api: plans_api::MockPlansApi::new(), policies_api: policies_api::MockPoliciesApi::new(), projects_api: projects_api::MockProjectsApi::new(), @@ -737,13 +732,6 @@ impl ApiClient { ApiClient::Mock(mock) => &mock.organizations_api, } } - pub fn phishing_domains_api(&self) -> &dyn phishing_domains_api::PhishingDomainsApi { - match self { - ApiClient::Real(real) => &real.phishing_domains_api, - #[cfg(feature = "mockall")] - ApiClient::Mock(mock) => &mock.phishing_domains_api, - } - } pub fn plans_api(&self) -> &dyn plans_api::PlansApi { match self { ApiClient::Real(real) => &real.plans_api, diff --git a/crates/bitwarden-api-api/src/apis/phishing_domains_api.rs b/crates/bitwarden-api-api/src/apis/phishing_domains_api.rs deleted file mode 100644 index ca14870d3..000000000 --- a/crates/bitwarden-api-api/src/apis/phishing_domains_api.rs +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Bitwarden Internal API - * - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: latest - * - * Generated by: https://openapi-generator.tech - */ - -use std::sync::Arc; - -use async_trait::async_trait; -#[cfg(feature = "mockall")] -use mockall::automock; -use reqwest; -use serde::{Deserialize, Serialize, de::Error as _}; - -use super::{Error, configuration}; -use crate::{ - apis::{ContentType, ResponseContent}, - models, -}; - -#[cfg_attr(feature = "mockall", automock)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait PhishingDomainsApi: Send + Sync { - /// GET /phishing-domains/checksum - async fn get_checksum(&self) -> Result>; - - /// GET /phishing-domains - async fn get_phishing_domains(&self) -> Result, Error>; -} - -pub struct PhishingDomainsApiClient { - configuration: Arc, -} - -impl PhishingDomainsApiClient { - pub fn new(configuration: Arc) -> Self { - Self { configuration } - } -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl PhishingDomainsApi for PhishingDomainsApiClient { - async fn get_checksum(&self) -> Result> { - let local_var_configuration = &self.configuration; - - let local_var_client = &local_var_configuration.client; - - let local_var_uri_str = format!( - "{}/phishing-domains/checksum", - local_var_configuration.base_path - ); - let mut local_var_req_builder = - local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); - - if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { - local_var_req_builder = local_var_req_builder - .header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); - } - if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { - local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); - }; - - let local_var_req = local_var_req_builder.build()?; - let local_var_resp = local_var_client.execute(local_var_req).await?; - - let local_var_status = local_var_resp.status(); - let local_var_content_type = local_var_resp - .headers() - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream"); - let local_var_content_type = super::ContentType::from(local_var_content_type); - let local_var_content = local_var_resp.text().await?; - - if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - match local_var_content_type { - ContentType::Json => serde_json::from_str(&local_var_content).map_err(Error::from), - ContentType::Text => return Ok(local_var_content), - ContentType::Unsupported(local_var_unknown_type) => { - return Err(Error::from(serde_json::Error::custom(format!( - "Received `{local_var_unknown_type}` content type response that cannot be converted to `String`" - )))); - } - } - } else { - let local_var_entity: Option = - serde_json::from_str(&local_var_content).ok(); - let local_var_error = ResponseContent { - status: local_var_status, - content: local_var_content, - entity: local_var_entity, - }; - Err(Error::ResponseError(local_var_error)) - } - } - - async fn get_phishing_domains(&self) -> Result, Error> { - let local_var_configuration = &self.configuration; - - let local_var_client = &local_var_configuration.client; - - let local_var_uri_str = format!("{}/phishing-domains", local_var_configuration.base_path); - let mut local_var_req_builder = - local_var_client.request(reqwest::Method::GET, local_var_uri_str.as_str()); - - if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { - local_var_req_builder = local_var_req_builder - .header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); - } - if let Some(ref local_var_token) = local_var_configuration.oauth_access_token { - local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.to_owned()); - }; - - let local_var_req = local_var_req_builder.build()?; - let local_var_resp = local_var_client.execute(local_var_req).await?; - - let local_var_status = local_var_resp.status(); - let local_var_content_type = local_var_resp - .headers() - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream"); - let local_var_content_type = super::ContentType::from(local_var_content_type); - let local_var_content = local_var_resp.text().await?; - - if !local_var_status.is_client_error() && !local_var_status.is_server_error() { - match local_var_content_type { - ContentType::Json => serde_json::from_str(&local_var_content).map_err(Error::from), - ContentType::Text => { - return Err(Error::from(serde_json::Error::custom( - "Received `text/plain` content type response that cannot be converted to `Vec<String>`", - ))); - } - ContentType::Unsupported(local_var_unknown_type) => { - return Err(Error::from(serde_json::Error::custom(format!( - "Received `{local_var_unknown_type}` content type response that cannot be converted to `Vec<String>`" - )))); - } - } - } else { - let local_var_entity: Option = - serde_json::from_str(&local_var_content).ok(); - let local_var_error = ResponseContent { - status: local_var_status, - content: local_var_content, - entity: local_var_entity, - }; - Err(Error::ResponseError(local_var_error)) - } - } -} - -/// struct for typed errors of method [`PhishingDomainsApi::get_checksum`] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum GetChecksumError { - UnknownValue(serde_json::Value), -} -/// struct for typed errors of method [`PhishingDomainsApi::get_phishing_domains`] -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum GetPhishingDomainsError { - UnknownValue(serde_json::Value), -} 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-api/src/models/keys_response_model.rs b/crates/bitwarden-api-api/src/models/keys_response_model.rs index 14dfba5d6..740649e67 100644 --- a/crates/bitwarden-api-api/src/models/keys_response_model.rs +++ b/crates/bitwarden-api-api/src/models/keys_response_model.rs @@ -20,6 +20,8 @@ pub struct KeysResponseModel { skip_serializing_if = "Option::is_none" )] pub object: Option, + /// The master key wrapped user key. The master key can either be a master-password master key + /// or a key-connector master key. #[serde(rename = "key", alias = "Key", skip_serializing_if = "Option::is_none")] pub key: Option, #[serde( @@ -34,6 +36,12 @@ pub struct KeysResponseModel { skip_serializing_if = "Option::is_none" )] pub private_key: Option, + #[serde( + rename = "accountKeys", + alias = "AccountKeys", + skip_serializing_if = "Option::is_none" + )] + pub account_keys: Option>, } impl KeysResponseModel { @@ -43,6 +51,7 @@ impl KeysResponseModel { key: None, public_key: None, private_key: None, + account_keys: None, } } } diff --git a/crates/bitwarden-api-identity/.openapi-generator/FILES b/crates/bitwarden-api-identity/.openapi-generator/FILES index 6ac78cfb0..810b885e5 100644 --- a/crates/bitwarden-api-identity/.openapi-generator/FILES +++ b/crates/bitwarden-api-identity/.openapi-generator/FILES @@ -5,6 +5,7 @@ src/apis/info_api.rs src/apis/mod.rs src/apis/sso_api.rs src/lib.rs +src/models/account_keys_request_model.rs src/models/assertion_options.rs src/models/authentication_extensions_client_inputs.rs src/models/authenticator_transport.rs @@ -18,10 +19,13 @@ src/models/product_tier_type.rs src/models/product_type.rs src/models/public_key_credential_descriptor.rs src/models/public_key_credential_type.rs +src/models/public_key_encryption_key_pair_request_model.rs src/models/register_finish_request_model.rs src/models/register_finish_response_model.rs src/models/register_send_verification_email_request_model.rs src/models/register_verification_email_clicked_request_model.rs +src/models/security_state_model.rs +src/models/signature_key_pair_request_model.rs src/models/trial_send_verification_email_request_model.rs src/models/user_verification_requirement.rs src/models/web_authn_login_assertion_options_response_model.rs diff --git a/crates/bitwarden-api-identity/README.md b/crates/bitwarden-api-identity/README.md index f238f249a..c0bdc5735 100644 --- a/crates/bitwarden-api-identity/README.md +++ b/crates/bitwarden-api-identity/README.md @@ -1,9 +1,19 @@ # Rust API client for bitwarden-api-identity -No description provided (generated by Openapi Generator -https://github.com/openapitools/openapi-generator) +## Updating bindings after a server API change -## Overview +When a server API change is made, new bindings will need to be generated to reflect this change for +consumption in the SDK. + +This can be done the following ways: + +1. Wait for an automatic binding update to run, which is scheduled every 2 weeks. +2. Manually run the `Update API Bindings` workflow in the `sdk-internal` repo. + +Both of these will generate a PR that will require approval from any teams whose owned code is +affected by the binding updates. + +## Current binding details This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API @@ -12,20 +22,11 @@ client. - API version: v1 - Package version: 2.0.0 - Server Git commit: - [`196e555116aba4666ae8c9cc9080291c42c1fe46`](https://github.com/bitwarden/server/commit/196e555116aba4666ae8c9cc9080291c42c1fe46) + [`04efe402bebc5aef3b06cd40c37474be0d114634`](https://github.com/bitwarden/server/commit/04efe402bebc5aef3b06cd40c37474be0d114634) - Generator version: 7.15.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` -## Installation - -Put the package under your project folder in a directory named `bitwarden-api-identity` and add the -following to `Cargo.toml` under `[dependencies]`: - -``` -bitwarden-api-identity = { path = "./bitwarden-api-identity" } -``` - -## Documentation for API Endpoints +## Documentation for API endpoints All URIs are relative to *https://identity.bitwarden.com* @@ -46,6 +47,7 @@ All URIs are relative to *https://identity.bitwarden.com* ## Documentation For Models +- [AccountKeysRequestModel](docs/AccountKeysRequestModel.md) - [AssertionOptions](docs/AssertionOptions.md) - [AuthenticationExtensionsClientInputs](docs/AuthenticationExtensionsClientInputs.md) - [AuthenticatorTransport](docs/AuthenticatorTransport.md) @@ -58,10 +60,13 @@ All URIs are relative to *https://identity.bitwarden.com* - [ProductType](docs/ProductType.md) - [PublicKeyCredentialDescriptor](docs/PublicKeyCredentialDescriptor.md) - [PublicKeyCredentialType](docs/PublicKeyCredentialType.md) +- [PublicKeyEncryptionKeyPairRequestModel](docs/PublicKeyEncryptionKeyPairRequestModel.md) - [RegisterFinishRequestModel](docs/RegisterFinishRequestModel.md) - [RegisterFinishResponseModel](docs/RegisterFinishResponseModel.md) - [RegisterSendVerificationEmailRequestModel](docs/RegisterSendVerificationEmailRequestModel.md) - [RegisterVerificationEmailClickedRequestModel](docs/RegisterVerificationEmailClickedRequestModel.md) +- [SecurityStateModel](docs/SecurityStateModel.md) +- [SignatureKeyPairRequestModel](docs/SignatureKeyPairRequestModel.md) - [TrialSendVerificationEmailRequestModel](docs/TrialSendVerificationEmailRequestModel.md) - [UserVerificationRequirement](docs/UserVerificationRequirement.md) - [WebAuthnLoginAssertionOptionsResponseModel](docs/WebAuthnLoginAssertionOptionsResponseModel.md) @@ -71,5 +76,3 @@ To get access to the crate's generated documentation, use: ``` cargo doc --open ``` - -## Author 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..12e49ab54 100644 --- a/crates/bitwarden-api-identity/src/models/mod.rs +++ b/crates/bitwarden-api-identity/src/models/mod.rs @@ -1,3 +1,5 @@ +pub mod account_keys_request_model; +pub use self::account_keys_request_model::AccountKeysRequestModel; pub mod assertion_options; pub use self::assertion_options::AssertionOptions; pub mod authentication_extensions_client_inputs; @@ -22,6 +24,8 @@ pub mod public_key_credential_descriptor; pub use self::public_key_credential_descriptor::PublicKeyCredentialDescriptor; pub mod public_key_credential_type; pub use self::public_key_credential_type::PublicKeyCredentialType; +pub mod public_key_encryption_key_pair_request_model; +pub use self::public_key_encryption_key_pair_request_model::PublicKeyEncryptionKeyPairRequestModel; pub mod register_finish_request_model; pub use self::register_finish_request_model::RegisterFinishRequestModel; pub mod register_finish_response_model; @@ -30,6 +34,10 @@ pub mod register_send_verification_email_request_model; pub use self::register_send_verification_email_request_model::RegisterSendVerificationEmailRequestModel; pub mod register_verification_email_clicked_request_model; pub use self::register_verification_email_clicked_request_model::RegisterVerificationEmailClickedRequestModel; +pub mod security_state_model; +pub use self::security_state_model::SecurityStateModel; +pub mod signature_key_pair_request_model; +pub use self::signature_key_pair_request_model::SignatureKeyPairRequestModel; pub mod trial_send_verification_email_request_model; pub use self::trial_send_verification_email_request_model::TrialSendVerificationEmailRequestModel; pub mod user_verification_requirement; 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, + } + } +} From 1fcbfaff0c60c9c416d3cd67f5eef98b6aa0d210 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:03:50 +0000 Subject: [PATCH 27/29] Update API bindings - d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3 --- crates/bitwarden-api-api/README.md | 2 +- crates/bitwarden-api-identity/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-api-api/README.md b/crates/bitwarden-api-api/README.md index 6acf795a8..03c2e3fce 100644 --- a/crates/bitwarden-api-api/README.md +++ b/crates/bitwarden-api-api/README.md @@ -22,7 +22,7 @@ client. - API version: latest - Package version: 2.0.0 - Server Git commit: - [`04efe402bebc5aef3b06cd40c37474be0d114634`](https://github.com/bitwarden/server/commit/04efe402bebc5aef3b06cd40c37474be0d114634) + [`d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3`](https://github.com/bitwarden/server/commit/d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3) - Generator version: 7.15.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` diff --git a/crates/bitwarden-api-identity/README.md b/crates/bitwarden-api-identity/README.md index c0bdc5735..41bdcf272 100644 --- a/crates/bitwarden-api-identity/README.md +++ b/crates/bitwarden-api-identity/README.md @@ -22,7 +22,7 @@ client. - API version: v1 - Package version: 2.0.0 - Server Git commit: - [`04efe402bebc5aef3b06cd40c37474be0d114634`](https://github.com/bitwarden/server/commit/04efe402bebc5aef3b06cd40c37474be0d114634) + [`d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3`](https://github.com/bitwarden/server/commit/d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3) - Generator version: 7.15.0 - Build package: `org.openapitools.codegen.languages.RustClientCodegen` From 5af24600b0c30be637406666250ad3763973e23a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 18 Dec 2025 13:21:17 +0100 Subject: [PATCH 28/29] Fix build after merge --- Cargo.lock | 1 + crates/bitwarden-api-identity/src/models/mod.rs | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25ae0d31a..e67b077b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -542,6 +542,7 @@ dependencies = [ "tracing", "tsify", "uniffi", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "wiremock", diff --git a/crates/bitwarden-api-identity/src/models/mod.rs b/crates/bitwarden-api-identity/src/models/mod.rs index 90425d59f..12e49ab54 100644 --- a/crates/bitwarden-api-identity/src/models/mod.rs +++ b/crates/bitwarden-api-identity/src/models/mod.rs @@ -12,14 +12,6 @@ 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; From aff137cece4d764c69f535eef53137e188566c33 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 18 Dec 2025 13:38:51 +0100 Subject: [PATCH 29/29] Fix tests --- crates/bitwarden-auth/src/registration.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bitwarden-auth/src/registration.rs b/crates/bitwarden-auth/src/registration.rs index 128bc19c8..71d7250fc 100644 --- a/crates/bitwarden-auth/src/registration.rs +++ b/crates/bitwarden-auth/src/registration.rs @@ -254,6 +254,7 @@ mod tests { key: None, public_key: None, private_key: None, + account_keys: None, }) }); mock.organization_users_api @@ -314,6 +315,7 @@ mod tests { key: None, public_key: None, private_key: None, + account_keys: None, }) }); mock.organization_users_api @@ -404,6 +406,7 @@ mod tests { key: None, public_key: None, private_key: None, + account_keys: None, }) }); mock.organization_users_api @@ -456,6 +459,7 @@ mod tests { key: None, public_key: None, private_key: None, + account_keys: None, }) }); mock.organization_users_api