#[cfg(feature = "internal")]
use std::sync::RwLock;
use std::sync::{Arc, OnceLock};
use bitwarden_crypto::KeyStore;
#[cfg(any(feature = "internal", feature = "secrets"))]
use bitwarden_crypto::SymmetricCryptoKey;
#[cfg(feature = "internal")]
use bitwarden_crypto::{
EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey, safe::PasswordProtectedKeyEnvelope,
};
use bitwarden_state::registry::StateRegistry;
#[cfg(feature = "internal")]
use tracing::{debug, info, instrument};
use crate::{
DeviceType, UserId, auth::auth_tokens::TokenHandler, error::UserIdAlreadySetError,
key_management::KeySlotIds,
};
#[cfg(any(feature = "internal", feature = "secrets"))]
use crate::{
OrganizationId, client::encryption_settings::EncryptionSettings,
client::login_method::LoginMethod,
};
#[cfg(feature = "internal")]
use crate::{
client::{
encryption_settings::EncryptionSettingsError,
flags::Flags,
login_method::UserLoginMethod,
persisted_state::{FLAGS, USER_ID, USER_LOGIN_METHOD},
},
error::NotAuthenticatedError,
key_management::{
MasterPasswordUnlockData, SecurityState, V2UpgradeToken,
account_cryptographic_state::WrappedAccountCryptographicState, state_bridge::StateBridge,
},
};
#[allow(missing_docs)]
pub struct ApiConfigurations {
pub identity_client: bitwarden_api_identity::apis::ApiClient,
pub api_client: bitwarden_api_api::apis::ApiClient,
pub identity_config: bitwarden_api_identity::Configuration,
pub api_config: bitwarden_api_api::Configuration,
pub device_type: DeviceType,
}
impl std::fmt::Debug for ApiConfigurations {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiConfigurations")
.field("device_type", &self.device_type)
.finish_non_exhaustive()
}
}
impl ApiConfigurations {
pub(crate) fn new(
identity_config: bitwarden_api_identity::Configuration,
api_config: bitwarden_api_api::Configuration,
device_type: DeviceType,
) -> Arc<Self> {
let identity = Arc::new(identity_config.clone());
let api = Arc::new(api_config.clone());
let identity_client = bitwarden_api_identity::apis::ApiClient::new(&identity);
let api_client = bitwarden_api_api::apis::ApiClient::new(&api);
Arc::new(Self {
identity_client,
api_client,
identity_config,
api_config,
device_type,
})
}
#[cfg(feature = "test-fixtures")]
pub fn from_api_client(api_client: bitwarden_api_api::apis::ApiClient) -> Self {
let dummy_config = bitwarden_api_base::Configuration {
base_path: String::new(),
client: reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build(),
};
Self {
api_client,
identity_client: bitwarden_api_identity::apis::ApiClient::new(&std::sync::Arc::new(
dummy_config.clone(),
)),
api_config: dummy_config.clone(),
identity_config: dummy_config,
device_type: DeviceType::SDK,
}
}
pub(crate) fn get_key_connector_client(
self: &Arc<Self>,
key_connector_url: String,
) -> bitwarden_api_key_connector::apis::ApiClient {
let api = self.api_config.clone();
let key_connector = bitwarden_api_base::Configuration {
base_path: key_connector_url,
client: api.client,
};
bitwarden_api_key_connector::apis::ApiClient::new(&Arc::new(key_connector))
}
}
#[allow(missing_docs)]
pub struct InternalClient {
pub(crate) user_id: OnceLock<UserId>,
#[cfg_attr(not(any(feature = "internal", feature = "secrets")), allow(dead_code))]
pub(crate) token_handler: Arc<dyn TokenHandler>,
pub(super) api_configurations: Arc<ApiConfigurations>,
#[allow(unused)]
pub(crate) external_http_client: reqwest::Client,
pub(super) key_store: KeyStore<KeySlotIds>,
#[cfg(feature = "internal")]
pub(crate) security_state: RwLock<Option<SecurityState>>,
#[cfg_attr(not(feature = "internal"), allow(dead_code))]
pub(crate) state_registry: StateRegistry,
#[cfg(feature = "internal")]
pub(crate) state_bridge: StateBridge,
}
impl InternalClient {
#[cfg(feature = "internal")]
pub async fn load_flags(&self, flags: std::collections::HashMap<String, bool>) {
let flags = Flags::load_from_map(flags);
match self.state_registry.setting(FLAGS) {
Ok(setting) => {
if let Err(e) = setting.update(flags).await {
tracing::warn!("Failed to persist flags: {e}");
}
}
Err(e) => tracing::warn!("Flags setting unavailable: {e}"),
}
}
#[cfg(feature = "internal")]
pub async fn get_flags(&self) -> Flags {
let setting = match self.state_registry.setting(FLAGS) {
Ok(setting) => setting,
Err(e) => {
tracing::warn!("Flags setting unavailable, using defaults: {e}");
return Flags::default();
}
};
match setting.get().await {
Ok(Some(flags)) => flags,
Ok(None) => Flags::default(),
Err(e) => {
tracing::warn!("Failed to read flags, using defaults: {e}");
Flags::default()
}
}
}
#[cfg(feature = "internal")]
pub(crate) async fn get_login_method(&self) -> Option<UserLoginMethod> {
self.state_registry
.setting(USER_LOGIN_METHOD)
.ok()?
.get()
.await
.ok()
.flatten()
}
#[cfg(any(feature = "internal", feature = "secrets"))]
pub(crate) async fn set_login_method(&self, login_method: LoginMethod) {
match login_method {
#[cfg(feature = "internal")]
LoginMethod::User(lm) => {
if let Ok(setting) = self.state_registry.setting(USER_LOGIN_METHOD) {
setting.update(lm).await.ok();
}
}
#[cfg(feature = "secrets")]
LoginMethod::ServiceAccount(lm) => {
self.token_handler.set_sm_login_method(lm).await;
}
}
}
#[cfg(any(feature = "internal", feature = "secrets"))]
pub(crate) async fn set_tokens(
&self,
token: String,
refresh_token: Option<String>,
expires_in: u64,
) {
self.token_handler
.set_tokens(token, refresh_token, expires_in)
.await;
}
#[allow(missing_docs)]
#[cfg(feature = "internal")]
pub async fn get_kdf(&self) -> Result<Kdf, NotAuthenticatedError> {
match self.get_login_method().await {
Some(UserLoginMethod::Username { kdf, .. } | UserLoginMethod::ApiKey { kdf, .. }) => {
Ok(kdf)
}
None => Err(NotAuthenticatedError),
}
}
pub fn get_key_connector_client(
&self,
key_connector_url: String,
) -> bitwarden_api_key_connector::apis::ApiClient {
self.api_configurations
.get_key_connector_client(key_connector_url)
}
pub fn get_api_configurations(&self) -> Arc<ApiConfigurations> {
self.api_configurations.clone()
}
#[allow(missing_docs)]
#[cfg(feature = "internal")]
pub fn get_http_client(&self) -> &reqwest::Client {
&self.external_http_client
}
#[allow(missing_docs)]
pub fn get_key_store(&self) -> &KeyStore<KeySlotIds> {
&self.key_store
}
#[cfg(feature = "internal")]
pub fn get_security_version(&self) -> u64 {
self.security_state
.read()
.expect("RwLock is not poisoned")
.as_ref()
.map_or(1, |state| state.version())
}
#[allow(missing_docs)]
pub async fn init_user_id(&self, user_id: UserId) -> Result<(), UserIdAlreadySetError> {
let set_uuid = self.user_id.get_or_init(|| user_id);
if *set_uuid != user_id {
return Err(UserIdAlreadySetError);
}
#[cfg(feature = "internal")]
if let Ok(setting) = self.state_registry.setting(USER_ID)
&& let Err(e) = setting.update(user_id).await
{
tracing::warn!("Failed to persist user_id: {e}");
}
Ok(())
}
#[allow(missing_docs)]
pub fn get_user_id(&self) -> Option<UserId> {
self.user_id.get().copied()
}
#[cfg(feature = "internal")]
#[instrument(err, skip_all)]
pub(crate) fn initialize_user_crypto_key_connector_key(
&self,
master_key: MasterKey,
user_key: EncString,
account_crypto_state: WrappedAccountCryptographicState,
upgrade_token: &Option<V2UpgradeToken>,
) -> Result<(), EncryptionSettingsError> {
let user_key = master_key.decrypt_user_key(user_key)?;
self.initialize_user_crypto_decrypted_key(user_key, account_crypto_state, upgrade_token)
}
#[cfg(feature = "internal")]
#[instrument(err, skip_all, fields(user_id = ?self.get_user_id()))]
pub(crate) fn initialize_user_crypto_decrypted_key(
&self,
user_key: SymmetricCryptoKey,
account_crypto_state: WrappedAccountCryptographicState,
upgrade_token: &Option<V2UpgradeToken>,
) -> Result<(), EncryptionSettingsError> {
let mut ctx = self.key_store.context_mut();
let user_key_id = ctx.add_local_symmetric_key(user_key.clone());
let user_key_id = match (&user_key, upgrade_token) {
(SymmetricCryptoKey::Aes256CbcHmacKey(_), Some(token)) => {
info!("V1 user key detected with upgrade token, extracting V2 key");
token
.unwrap_v2(user_key_id, &mut ctx)
.map_err(|_| EncryptionSettingsError::InvalidUpgradeToken)?
}
(SymmetricCryptoKey::XChaCha20Poly1305Key(_), Some(_)) => {
debug!("V2 user key already present, ignoring upgrade token");
user_key_id
}
_ => user_key_id,
};
info!("Setting user key with ID {:?}", user_key_id);
account_crypto_state
.set_to_context(&self.security_state, user_key_id, &self.key_store, ctx)
.map_err(|_| EncryptionSettingsError::CryptoInitialization)
}
#[cfg(feature = "internal")]
#[instrument(err, skip_all)]
pub(crate) fn initialize_user_crypto_pin(
&self,
pin_key: PinKey,
pin_protected_user_key: EncString,
account_crypto_state: WrappedAccountCryptographicState,
upgrade_token: &Option<V2UpgradeToken>,
) -> Result<(), EncryptionSettingsError> {
let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?;
self.initialize_user_crypto_decrypted_key(
decrypted_user_key,
account_crypto_state,
upgrade_token,
)
}
#[cfg(feature = "internal")]
#[instrument(err, skip_all)]
pub(crate) fn initialize_user_crypto_pin_envelope(
&self,
pin: String,
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
account_crypto_state: WrappedAccountCryptographicState,
upgrade_token: &Option<V2UpgradeToken>,
) -> Result<(), EncryptionSettingsError> {
let decrypted_user_key = {
use bitwarden_crypto::safe::PasswordProtectedKeyEnvelopeNamespace;
let ctx = &mut self.key_store.context_mut();
let decrypted_user_key_id = pin_protected_user_key_envelope
.unseal(&pin, PasswordProtectedKeyEnvelopeNamespace::PinUnlock, ctx)
.map_err(|_| EncryptionSettingsError::WrongPin)?;
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(decrypted_user_key_id)?
.clone()
};
self.initialize_user_crypto_decrypted_key(
decrypted_user_key,
account_crypto_state,
upgrade_token,
)
}
#[cfg(feature = "secrets")]
pub(crate) fn initialize_crypto_single_org_key(
&self,
organization_id: OrganizationId,
key: SymmetricCryptoKey,
) {
EncryptionSettings::new_single_org_key(organization_id, key, &self.key_store);
}
#[allow(missing_docs)]
#[cfg(feature = "internal")]
pub fn initialize_org_crypto(
&self,
org_keys: Vec<(OrganizationId, UnsignedSharedKey)>,
) -> Result<(), EncryptionSettingsError> {
EncryptionSettings::set_org_keys(org_keys, &self.key_store)
}
#[cfg(feature = "internal")]
#[instrument(err, skip_all)]
pub(crate) fn initialize_user_crypto_master_password_unlock(
&self,
password: String,
master_password_unlock: MasterPasswordUnlockData,
account_crypto_state: WrappedAccountCryptographicState,
upgrade_token: &Option<V2UpgradeToken>,
) -> Result<(), EncryptionSettingsError> {
let master_key = MasterKey::derive(
&password,
&master_password_unlock.salt,
&master_password_unlock.kdf,
)?;
let user_key =
master_key.decrypt_user_key(master_password_unlock.master_key_wrapped_user_key)?;
self.initialize_user_crypto_decrypted_key(user_key, account_crypto_state, upgrade_token)
}
#[cfg(feature = "internal")]
pub async fn set_user_master_password_unlock(
&self,
master_password_unlock: MasterPasswordUnlockData,
) -> Result<(), NotAuthenticatedError> {
let new_kdf = master_password_unlock.kdf;
let login_method = self.get_login_method().await.ok_or(NotAuthenticatedError)?;
let kdf = self.get_kdf().await?;
if kdf != new_kdf {
match login_method {
UserLoginMethod::Username {
client_id, email, ..
} => {
self.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id,
email,
kdf: new_kdf,
}))
.await
}
UserLoginMethod::ApiKey {
client_id,
client_secret,
email,
..
} => {
self.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey {
client_id,
client_secret,
email,
kdf: new_kdf,
}))
.await
}
};
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use bitwarden_crypto::{EncString, Kdf, MasterKey};
use crate::{
Client,
client::{UserLoginMethod, test_accounts::test_bitwarden_com_account},
key_management::MasterPasswordUnlockData,
};
const TEST_ACCOUNT_EMAIL: &str = "test@bitwarden.com";
const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
#[tokio::test]
async fn initializing_user_multiple_times() {
use super::*;
use crate::client::persisted_state::USER_ID;
let client = Client::new(None);
let user_id = UserId::new_v4();
assert!(client.internal.init_user_id(user_id).await.is_ok());
assert_eq!(client.internal.get_user_id(), Some(user_id));
let persisted = client
.internal
.state_registry
.setting(USER_ID)
.unwrap()
.get()
.await
.unwrap();
assert_eq!(persisted, Some(user_id));
assert!(client.internal.init_user_id(user_id).await.is_ok());
let different_user_id = UserId::new_v4();
assert!(
client
.internal
.init_user_id(different_user_id)
.await
.is_err()
);
}
#[tokio::test]
async fn load_flags_round_trips_through_setting() {
use std::collections::HashMap;
use super::*;
let client = Client::new(None);
let initial = client.internal.get_flags().await;
assert!(!initial.enable_cipher_key_encryption);
assert!(!initial.strict_cipher_decryption);
let mut map = HashMap::new();
map.insert("enableCipherKeyEncryption".to_string(), true);
map.insert("pm-34500-strict-cipher-decryption".to_string(), true);
client.internal.load_flags(map).await;
let loaded = client.internal.get_flags().await;
assert!(loaded.enable_cipher_key_encryption);
assert!(loaded.strict_cipher_decryption);
let persisted = client
.internal
.state_registry
.setting(FLAGS)
.unwrap()
.get()
.await
.unwrap()
.expect("flags should be persisted after load_flags");
assert!(persisted.enable_cipher_key_encryption);
assert!(persisted.strict_cipher_decryption);
}
#[tokio::test]
async fn test_set_user_master_password_unlock_kdf_updated() {
let new_kdf = Kdf::Argon2id {
iterations: NonZeroU32::new(4).unwrap(),
memory: NonZeroU32::new(65).unwrap(),
parallelism: NonZeroU32::new(5).unwrap(),
};
let user_key: EncString = TEST_ACCOUNT_USER_KEY.parse().expect("Invalid user key");
let email = TEST_ACCOUNT_EMAIL.to_owned();
let client = Client::init_test_account(test_bitwarden_com_account()).await;
client
.internal
.set_user_master_password_unlock(MasterPasswordUnlockData {
kdf: new_kdf.clone(),
master_key_wrapped_user_key: user_key,
salt: email,
})
.await
.unwrap();
let kdf = client.internal.get_kdf().await.unwrap();
assert_eq!(kdf, new_kdf);
}
#[tokio::test]
async fn test_set_user_master_password_unlock_email_and_keys_not_updated() {
let password = "asdfasdfasdf".to_string();
let new_email = format!("{}@example.com", uuid::Uuid::new_v4());
let kdf = Kdf::default_pbkdf2();
let expected_email = TEST_ACCOUNT_EMAIL.to_owned();
let (new_user_key, new_encrypted_user_key) = {
let master_key = MasterKey::derive(&password, &new_email, &kdf).unwrap();
master_key.make_user_key().unwrap()
};
let client = Client::init_test_account(test_bitwarden_com_account()).await;
client
.internal
.set_user_master_password_unlock(MasterPasswordUnlockData {
kdf,
master_key_wrapped_user_key: new_encrypted_user_key,
salt: new_email,
})
.await
.unwrap();
let login_method = client.internal.get_login_method().await.unwrap();
match login_method {
UserLoginMethod::Username { email, .. } => {
assert_eq!(*email, expected_email);
}
_ => panic!("Expected username login method"),
}
let user_key = client.crypto().get_user_encryption_key().await.unwrap();
assert_ne!(user_key, new_user_key.0.to_base64());
}
}