#[cfg(any(feature = "wasm", test))]
use bitwarden_crypto::safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeNamespace};
use bitwarden_crypto::{
BitwardenLegacyKeyBytes, CryptoError, Decryptable, Kdf, PrimitiveEncryptable, RotateableKeySet,
SymmetricCryptoKey, SymmetricKeyAlgorithm,
};
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, UnsignedSharedKey};
use bitwarden_encoding::B64;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
use super::crypto::{
DeriveKeyConnectorError, DeriveKeyConnectorRequest, EnrollAdminPasswordResetError,
MakeJitMasterPasswordRegistrationResponse, MakeKeyConnectorRegistrationResponse,
MakeKeyPairResponse, MakeUserMasterPasswordRegistrationResponse, VerifyAsymmetricKeysRequest,
VerifyAsymmetricKeysResponse, derive_key_connector, make_key_pair,
make_user_jit_master_password_registration, make_user_key_connector_registration,
make_user_password_registration, verify_asymmetric_keys,
};
use crate::key_management::V2UpgradeToken;
#[cfg(feature = "internal")]
use crate::key_management::{
SymmetricKeySlotId,
crypto::{
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set,
},
};
#[expect(deprecated)]
use crate::{
Client,
client::encryption_settings::EncryptionSettingsError,
error::{NotAuthenticatedError, 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,
},
};
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct CryptoClient {
pub(crate) client: crate::Client,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl CryptoClient {
pub async fn initialize_user_crypto(
&self,
req: InitUserCryptoRequest,
) -> Result<(), EncryptionSettingsError> {
initialize_user_crypto(&self.client, req).await
}
pub async fn initialize_org_crypto(
&self,
req: InitOrgCryptoRequest,
) -> Result<(), EncryptionSettingsError> {
initialize_org_crypto(&self.client, req).await
}
pub fn make_key_pair(&self, user_key: B64) -> Result<MakeKeyPairResponse, CryptoError> {
make_key_pair(user_key)
}
pub fn verify_asymmetric_keys(
&self,
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse, CryptoError> {
verify_asymmetric_keys(request)
}
pub fn make_keys_for_user_crypto_v2(
&self,
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
#[expect(deprecated)]
make_v2_keys_for_v1_user(&self.client)
}
pub fn get_v2_rotated_account_keys(
&self,
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
#[expect(deprecated)]
get_v2_rotated_account_keys(&self.client)
}
pub async fn make_update_kdf(
&self,
password: String,
kdf: Kdf,
) -> Result<UpdateKdfResponse, CryptoClientError> {
make_update_kdf(&self.client, &password, &kdf).await
}
pub fn enroll_pin(&self, pin: String) -> Result<EnrollPinResponse, CryptoClientError> {
enroll_pin(&self.client, pin)
}
pub fn enroll_pin_with_encrypted_pin(
&self,
encrypted_pin: String,
) -> Result<EnrollPinResponse, CryptoClientError> {
let encrypted_pin: EncString = encrypted_pin.parse()?;
let pin = encrypted_pin.decrypt(
&mut self.client.internal.get_key_store().context_mut(),
SymmetricKeySlotId::User,
)?;
enroll_pin(&self.client, pin)
}
#[cfg(any(feature = "wasm", test))]
pub fn unseal_password_protected_key_envelope(
&self,
pin: String,
envelope: PasswordProtectedKeyEnvelope,
) -> Result<Vec<u8>, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
let key_slot = envelope.unseal(
pin.as_str(),
PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
&mut ctx,
)?;
#[allow(deprecated)]
let key = ctx.dangerous_get_symmetric_key(key_slot)?;
Ok(key.to_encoded().to_vec())
}
pub fn encrypt_with_local_user_data_key(
&self,
plaintext: String,
) -> Result<String, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
plaintext
.encrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
.map_err(CryptoClientError::Crypto)
.map(|enc| enc.to_string())
}
pub fn decrypt_with_local_user_data_key(
&self,
encrypted_plaintext: String,
) -> Result<String, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
let encrypted: EncString = encrypted_plaintext
.parse()
.map_err(CryptoClientError::Crypto)?;
encrypted
.decrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
.map_err(CryptoClientError::Crypto)
}
pub async fn get_user_encryption_key(&self) -> Result<B64, CryptoClientError> {
get_user_encryption_key(&self.client).await
}
pub fn get_key_id_for_symmetric_key(
key: Vec<u8>,
) -> Result<Option<Vec<u8>>, CryptoClientError> {
let symmetric_key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key))?;
Ok(symmetric_key.key_id().map(|id| id.as_slice().to_vec()))
}
}
impl CryptoClient {
pub async fn make_update_password(
&self,
new_password: String,
) -> Result<UpdatePasswordResponse, CryptoClientError> {
make_update_password(&self.client, new_password).await
}
pub async fn derive_pin_key(
&self,
pin: String,
) -> Result<DerivePinKeyResponse, CryptoClientError> {
derive_pin_key(&self.client, pin).await
}
pub async fn derive_pin_user_key(
&self,
encrypted_pin: EncString,
) -> Result<EncString, CryptoClientError> {
derive_pin_user_key(&self.client, encrypted_pin).await
}
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet, CryptoClientError> {
make_prf_user_key_set(&self.client, prf)
}
pub fn enroll_admin_password_reset(
&self,
public_key: B64,
) -> Result<UnsignedSharedKey, EnrollAdminPasswordResetError> {
enroll_admin_password_reset(&self.client, public_key)
}
pub fn derive_key_connector(
&self,
request: DeriveKeyConnectorRequest,
) -> Result<B64, DeriveKeyConnectorError> {
derive_key_connector(request)
}
pub fn make_user_tde_registration(
&self,
org_public_key: B64,
) -> Result<MakeTdeRegistrationResponse, MakeKeysError> {
make_user_tde_registration(&self.client, org_public_key)
}
pub fn make_user_key_connector_registration(
&self,
) -> Result<MakeKeyConnectorRegistrationResponse, MakeKeysError> {
make_user_key_connector_registration(&self.client)
}
pub fn make_user_jit_master_password_registration(
&self,
master_password: String,
salt: String,
org_public_key: B64,
) -> Result<MakeJitMasterPasswordRegistrationResponse, MakeKeysError> {
make_user_jit_master_password_registration(
&self.client,
master_password,
salt,
org_public_key,
)
}
pub fn make_user_password_registration(
&self,
master_password: String,
salt: String,
) -> Result<MakeUserMasterPasswordRegistrationResponse, MakeKeysError> {
make_user_password_registration(&self.client, master_password, salt)
}
pub fn get_upgraded_user_key(
&self,
upgrade_token: Option<V2UpgradeToken>,
) -> Result<B64, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
let algorithm = ctx
.get_symmetric_key_algorithm(SymmetricKeySlotId::User)
.map_err(|_| CryptoClientError::NotAuthenticated(NotAuthenticatedError))?;
match (algorithm, upgrade_token) {
(SymmetricKeyAlgorithm::XChaCha20Poly1305, _) => {
#[allow(deprecated)]
let current_key = ctx
.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.map_err(|_| CryptoClientError::NotAuthenticated(NotAuthenticatedError))?;
Ok(current_key.clone().to_base64())
}
(SymmetricKeyAlgorithm::Aes256CbcHmac, Some(token)) => {
let v2_key_id = token
.unwrap_v2(SymmetricKeySlotId::User, &mut ctx)
.map_err(|_| CryptoClientError::InvalidUpgradeToken)?;
#[allow(deprecated)]
let v2_key = ctx
.dangerous_get_symmetric_key(v2_key_id)
.map_err(|_| CryptoClientError::InvalidUpgradeToken)?;
Ok(v2_key.clone().to_base64())
}
(SymmetricKeyAlgorithm::Aes256CbcHmac, None) => {
Err(CryptoClientError::UpgradeTokenRequired)
}
}
}
}
impl Client {
pub fn crypto(&self) -> CryptoClient {
CryptoClient {
client: self.clone(),
}
}
}
#[cfg(test)]
mod tests {
use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey};
use super::*;
use crate::{
client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
key_management::{KeySlotIds, V2UpgradeToken},
};
#[tokio::test]
async fn test_enroll_pin_envelope() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let user_key_initial =
SymmetricCryptoKey::try_from(client.crypto().get_user_encryption_key().await.unwrap())
.unwrap();
let pin = "1234";
let enroll_response = client.crypto().enroll_pin(pin.to_string()).unwrap();
let re_enroll_response = client
.crypto()
.enroll_pin_with_encrypted_pin(enroll_response.user_key_encrypted_pin.to_string())
.unwrap();
let secret = BitwardenLegacyKeyBytes::from(
client
.crypto()
.unseal_password_protected_key_envelope(
pin.to_string(),
re_enroll_response.pin_protected_user_key_envelope,
)
.unwrap(),
);
let user_key_final = SymmetricCryptoKey::try_from(&secret).expect("valid user key");
assert_eq!(user_key_initial, user_key_final);
}
#[test]
fn test_get_upgraded_user_key_not_authenticated() {
let client = Client::new(None);
let result = client.crypto().get_upgraded_user_key(None);
assert!(matches!(
result,
Err(CryptoClientError::NotAuthenticated(_))
));
}
#[tokio::test]
async fn test_get_upgraded_user_key_v1_no_token_returns_error() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let result = client.crypto().get_upgraded_user_key(None);
assert!(matches!(
result,
Err(CryptoClientError::UpgradeTokenRequired)
));
}
#[tokio::test]
async fn test_get_upgraded_user_key_v1_with_token_returns_v2_key() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let (token, expected_v2_b64) = {
let mut ctx = client.internal.get_key_store().context_mut();
let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
#[allow(deprecated)]
let v2_key = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap().clone();
let token = V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap();
(token, v2_key.to_base64())
};
let result = client.crypto().get_upgraded_user_key(Some(token)).unwrap();
assert_eq!(result, expected_v2_b64);
}
#[tokio::test]
async fn test_get_upgraded_user_key_v1_invalid_token_returns_error() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let mismatched_token = {
let key_store = KeyStore::<KeySlotIds>::default();
let mut ctx = key_store.context_mut();
let wrong_v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
V2UpgradeToken::create(wrong_v1_id, v2_id, &ctx).unwrap()
};
let result = client
.crypto()
.get_upgraded_user_key(Some(mismatched_token));
assert!(matches!(
result,
Err(CryptoClientError::InvalidUpgradeToken)
));
}
#[tokio::test]
async fn test_get_upgraded_user_key_already_v2_no_token_returns_v2_key() {
let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
let result = client.crypto().get_upgraded_user_key(None).unwrap();
let result_key = SymmetricCryptoKey::try_from(result).unwrap();
assert!(
matches!(result_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)),
"V2 user should receive a V2 key"
);
}
#[tokio::test]
async fn test_get_upgraded_user_key_already_v2_with_token_ignored() {
let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
let dummy_token = {
let key_store = KeyStore::<KeySlotIds>::default();
let mut ctx = key_store.context_mut();
let v1_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
V2UpgradeToken::create(v1_id, v2_id, &ctx).unwrap()
};
let result_with_token = client
.crypto()
.get_upgraded_user_key(Some(dummy_token))
.unwrap();
let result_no_token = client.crypto().get_upgraded_user_key(None).unwrap();
assert_eq!(
result_with_token, result_no_token,
"Token must be ignored for a V2 user"
);
}
}