//! Mobile specific crypto operations
//!
//! This module contains temporary code for handling mobile specific cryptographic operations until
//! the SDK is fully implemented. When porting functionality from `client` the mobile clients should
//! be updated to consume the regular code paths and in this module should eventually disappear.
use std::collections::HashMap;
use bitwarden_api_api::models::AccountKeysRequestModel;
use bitwarden_crypto::safe::PasswordProtectedKeyEnvelopeNamespace;
#[expect(deprecated)]
use bitwarden_crypto::{
CoseSerializable, CryptoError, DeviceKey, EncString, Kdf, KeyConnectorKey, KeyDecryptable,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, PrivateKey, PublicKey,
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;
use bitwarden_error::bitwarden_error;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::info;
#[cfg(feature = "wasm")]
use {tsify::Tsify, wasm_bindgen::prelude::*};
#[cfg(feature = "wasm")]
use crate::key_management::wasm_unlock_state::{
copy_user_key_to_client_managed_state, get_user_key_from_client_managed_state,
};
use crate::{
Client, NotAuthenticatedError, OrganizationId, UserId, WrongPasswordError,
client::{
LoginMethod, UserLoginMethod,
encryption_settings::EncryptionSettingsError,
persisted_state::{ACCOUNT_CRYPTO_STATE, OrganizationSharedKey},
},
error::StatefulCryptoError,
key_management::{
MasterPasswordError, PrivateKeySlotId, SecurityState, SignedSecurityState,
SigningKeySlotId, SymmetricKeySlotId, V2UpgradeToken,
account_cryptographic_state::{
AccountCryptographyInitializationError, WrappedAccountCryptographicState,
},
local_user_data_key_state::{
get_local_user_data_key_from_state, initialize_local_user_data_key_into_state,
},
master_password::{MasterPasswordAuthenticationData, MasterPasswordUnlockData},
},
};
/// Catch all error for mobile crypto operations.
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum CryptoClientError {
#[error(transparent)]
NotAuthenticated(#[from] NotAuthenticatedError),
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
#[error("Invalid KDF settings")]
InvalidKdfSettings,
#[error(transparent)]
PasswordProtectedKeyEnvelope(#[from] PasswordProtectedKeyEnvelopeError),
#[error("Invalid PRF input")]
InvalidPrfInput,
#[error("Invalid upgrade token")]
InvalidUpgradeToken,
#[error("Upgrade token is required for V1 keys")]
UpgradeTokenRequired,
#[error("Invalid key type")]
InvalidKeyType,
}
/// State used for initializing the user cryptographic state.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct InitUserCryptoRequest {
/// The user's ID.
pub user_id: Option<UserId>,
/// The user's KDF parameters, as received from the prelogin request
pub kdf_params: Kdf,
/// The user's email address
pub email: String,
/// The user's account cryptographic state, containing their signature and
/// public-key-encryption keys, along with the signed security state, protected by the user key
pub account_cryptographic_state: WrappedAccountCryptographicState,
/// The method to decrypt the user's account symmetric key (user key)
pub method: InitUserCryptoMethod,
/// Optional V2 upgrade token for automatic key rotation from V1 to V2
#[serde(skip_serializing_if = "Option::is_none")]
pub upgrade_token: Option<V2UpgradeToken>,
}
/// The crypto method used to initialize the user cryptographic state.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[allow(clippy::large_enum_variant)]
pub enum InitUserCryptoMethod {
/// Master Password Unlock
MasterPasswordUnlock {
/// The user's master password
password: String,
/// Contains the data needed to unlock with the master password
master_password_unlock: MasterPasswordUnlockData,
},
/// Read the user-key directly from client-managed state
/// Note: In contrast to [`InitUserCryptoMethod::DecryptedKey`], this does not update the state
/// after initalizing
#[cfg(feature = "wasm")]
ClientManagedState {},
/// Never lock and/or biometric unlock
DecryptedKey {
/// The user's decrypted encryption key, obtained using `get_user_encryption_key`
decrypted_user_key: String,
},
/// PIN
Pin {
/// The user's PIN
pin: String,
/// The user's symmetric crypto key, encrypted with the PIN. Use `derive_pin_key` to obtain
/// this.
pin_protected_user_key: EncString,
},
/// PIN Envelope
PinEnvelope {
/// The user's PIN
pin: String,
/// The user's symmetric crypto key, encrypted with the PIN-protected key envelope.
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
},
/// Auth request
AuthRequest {
/// Private Key generated by the `crate::auth::new_auth_request`.
request_private_key: B64,
/// The type of auth request
method: AuthRequestMethod,
},
/// Device Key
DeviceKey {
/// The device's DeviceKey
device_key: String,
/// The Device Private Key
protected_device_private_key: EncString,
/// The user's symmetric crypto key, encrypted with the Device Key.
device_protected_user_key: UnsignedSharedKey,
},
/// Key connector
KeyConnector {
/// Base64 encoded master key, retrieved from the key connector.
master_key: B64,
/// The user's encrypted symmetric crypto key
user_key: EncString,
},
/// In contrast to key-connector, this does all of the connection with key-connector in the sdk
KeyConnectorUrl {
/// The url to retrieve the key-connector-key from
url: String,
/// The encrypted user key, encrypted with the key connector key retrieved from the url
key_connector_key_wrapped_user_key: EncString,
},
}
/// Auth requests supports multiple initialization methods.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub enum AuthRequestMethod {
/// User Key
UserKey {
/// User Key protected by the private key provided in `AuthRequestResponse`.
protected_user_key: UnsignedSharedKey,
},
/// Master Key
MasterKey {
/// Master Key protected by the private key provided in `AuthRequestResponse`.
protected_master_key: UnsignedSharedKey,
/// User Key protected by the MasterKey, provided by the auth response.
auth_request_key: EncString,
},
}
/// Initialize the user's cryptographic state.
#[tracing::instrument(skip_all, err)]
pub(super) async fn initialize_user_crypto(
client: &Client,
req: InitUserCryptoRequest,
) -> Result<(), EncryptionSettingsError> {
use bitwarden_crypto::{DeviceKey, PinKey};
use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key};
if let Some(user_id) = req.user_id {
client.internal.init_user_id(user_id).await?;
}
tracing::Span::current().record(
"user_id",
client.internal.get_user_id().map(|id| id.to_string()),
);
let account_crypto_state = req.account_cryptographic_state.to_owned();
#[cfg(feature = "wasm")]
let should_copy_user_key = matches!(
req.method,
InitUserCryptoMethod::MasterPasswordUnlock { .. }
| InitUserCryptoMethod::DecryptedKey { .. }
| InitUserCryptoMethod::PinEnvelope { .. }
| InitUserCryptoMethod::KeyConnectorUrl { .. }
| InitUserCryptoMethod::AuthRequest { .. }
);
match req.method {
InitUserCryptoMethod::MasterPasswordUnlock {
password,
master_password_unlock,
} => {
client
.internal
.initialize_user_crypto_master_password_unlock(
password,
master_password_unlock,
account_crypto_state,
&req.upgrade_token,
)?;
}
#[cfg(feature = "wasm")]
InitUserCryptoMethod::ClientManagedState {} => {
let user_key = get_user_key_from_client_managed_state(client)
.await
.map_err(|_| EncryptionSettingsError::UserKeyStateRetrievalFailed)?;
client.internal.initialize_user_crypto_decrypted_key(
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => {
let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?;
client.internal.initialize_user_crypto_decrypted_key(
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::Pin {
pin,
pin_protected_user_key,
} => {
let pin_key = PinKey::derive(pin.as_bytes(), req.email.as_bytes(), &req.kdf_params)?;
client.internal.initialize_user_crypto_pin(
pin_key,
pin_protected_user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::PinEnvelope {
pin,
pin_protected_user_key_envelope,
} => {
client.internal.initialize_user_crypto_pin_envelope(
pin,
pin_protected_user_key_envelope,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::AuthRequest {
request_private_key,
method,
} => {
let user_key = match method {
AuthRequestMethod::UserKey { protected_user_key } => {
auth_request_decrypt_user_key(request_private_key, protected_user_key)?
}
AuthRequestMethod::MasterKey {
protected_master_key,
auth_request_key,
} => auth_request_decrypt_master_key(
request_private_key,
protected_master_key,
auth_request_key,
)?,
};
client.internal.initialize_user_crypto_decrypted_key(
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::DeviceKey {
device_key,
protected_device_private_key,
device_protected_user_key,
} => {
let device_key = DeviceKey::try_from(device_key)?;
let user_key = device_key
.decrypt_user_key(protected_device_private_key, device_protected_user_key)?;
client.internal.initialize_user_crypto_decrypted_key(
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::KeyConnector {
master_key,
user_key,
} => {
let bytes = master_key.into_bytes();
let master_key = MasterKey::try_from(bytes)?;
client.internal.initialize_user_crypto_key_connector_key(
master_key,
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
InitUserCryptoMethod::KeyConnectorUrl {
url,
key_connector_key_wrapped_user_key,
} => {
let api_client = client.internal.get_key_connector_client(url);
let key_connector_key_response = api_client
.user_keys_api()
.get_user_key()
.await
.map_err(|_| EncryptionSettingsError::KeyConnectorRetrievalFailed)?;
let key_connector_key = KeyConnectorKey::try_from(key_connector_key_response)?;
let user_key =
key_connector_key.decrypt_user_key(key_connector_key_wrapped_user_key)?;
client.internal.initialize_user_crypto_decrypted_key(
user_key,
account_crypto_state,
&req.upgrade_token,
)?;
}
}
#[cfg(feature = "wasm")]
if should_copy_user_key {
copy_user_key_to_client_managed_state(client)
.await
.map_err(|_| EncryptionSettingsError::UserKeyStateUpdateFailed)?;
}
initialize_user_local_data_key(client).await?;
client
.internal
.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "".to_string(),
email: req.email,
kdf: req.kdf_params,
}))
.await;
if let Ok(setting) = client.internal.state_registry.setting(ACCOUNT_CRYPTO_STATE)
&& let Err(e) = setting.update(req.account_cryptographic_state).await
{
tracing::warn!("Failed to persist account crypto state: {e}");
}
info!("User crypto initialized successfully");
Ok(())
}
/// Represents the request to initialize the user's organizational cryptographic state.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct InitOrgCryptoRequest {
/// The encryption keys for all the organizations the user is a part of
pub organization_keys: HashMap<OrganizationId, UnsignedSharedKey>,
}
/// Initialize the user's organizational cryptographic state.
pub(super) async fn initialize_org_crypto(
client: &Client,
req: InitOrgCryptoRequest,
) -> Result<(), EncryptionSettingsError> {
let organization_keys: Vec<_> = req.organization_keys.into_iter().collect();
client
.internal
.initialize_org_crypto(organization_keys.clone())?;
// Persist org keys for rehydration
if let Ok(repo) = client
.internal
.state_registry
.get::<OrganizationSharedKey>()
{
for (org_id, key) in organization_keys {
if let Err(e) = repo
.set(org_id, OrganizationSharedKey { org_id, key })
.await
{
tracing::warn!("Failed to persist org key for {org_id}: {e}");
}
}
}
Ok(())
}
pub(super) async fn get_user_encryption_key(client: &Client) -> Result<B64, CryptoClientError> {
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
// This is needed because the clients need access to the user encryption key
// in order to set side-effects such as biometrics, and never-lock
#[allow(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)?;
Ok(user_key.to_base64())
}
/// Response from the `update_kdf` function
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct UpdateKdfResponse {
/// The authentication data for the new KDF setting
master_password_authentication_data: MasterPasswordAuthenticationData,
/// The unlock data for the new KDF setting
master_password_unlock_data: MasterPasswordUnlockData,
/// The authentication data for the KDF setting prior to the change
old_master_password_authentication_data: MasterPasswordAuthenticationData,
}
pub(super) async fn make_update_kdf(
client: &Client,
password: &str,
new_kdf: &Kdf,
) -> Result<UpdateKdfResponse, CryptoClientError> {
let login_method = client
.internal
.get_login_method()
.await
.ok_or(NotAuthenticatedError)?;
let email = match login_method {
UserLoginMethod::Username { email, .. } | UserLoginMethod::ApiKey { email, .. } => email,
};
let old_authentication_data = MasterPasswordAuthenticationData::derive(
password,
&client
.internal
.get_kdf()
.await
.map_err(|_| NotAuthenticatedError)?,
&email,
)
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
let authentication_data = MasterPasswordAuthenticationData::derive(password, new_kdf, &email)
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
let unlock_data =
MasterPasswordUnlockData::derive(password, new_kdf, &email, SymmetricKeySlotId::User, &ctx)
.map_err(|_| CryptoClientError::InvalidKdfSettings)?;
Ok(UpdateKdfResponse {
master_password_authentication_data: authentication_data,
master_password_unlock_data: unlock_data,
old_master_password_authentication_data: old_authentication_data,
})
}
/// Response from the `make_update_password` function
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct UpdatePasswordResponse {
/// Hash of the new password
password_hash: B64,
/// User key, encrypted with the new password
new_key: EncString,
}
pub(super) async fn make_update_password(
client: &Client,
new_password: String,
) -> Result<UpdatePasswordResponse, CryptoClientError> {
let login_method = client
.internal
.get_login_method()
.await
.ok_or(NotAuthenticatedError)?;
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
// FIXME: [PM-18099] Once MasterKey deals with KeySlotIds, this should be updated
#[allow(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)?;
// Derive a new master key from password
let new_master_key = match login_method {
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. } => {
MasterKey::derive(&new_password, &email, &kdf)?
}
};
let new_key = new_master_key.encrypt_user_key(user_key)?;
let password_hash = new_master_key.derive_master_key_hash(
new_password.as_bytes(),
bitwarden_crypto::HashPurpose::ServerAuthorization,
);
Ok(UpdatePasswordResponse {
password_hash,
new_key,
})
}
/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct EnrollPinResponse {
/// [UserKey] protected by PIN
pub pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
/// PIN protected by [UserKey]
pub user_key_encrypted_pin: EncString,
}
pub(super) fn enroll_pin(
client: &Client,
pin: String,
) -> Result<EnrollPinResponse, CryptoClientError> {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();
let key_envelope = PasswordProtectedKeyEnvelope::seal(
SymmetricKeySlotId::User,
&pin,
PasswordProtectedKeyEnvelopeNamespace::PinUnlock,
&ctx,
)?;
let encrypted_pin = pin.encrypt(&mut ctx, SymmetricKeySlotId::User)?;
Ok(EnrollPinResponse {
pin_protected_user_key_envelope: key_envelope,
user_key_encrypted_pin: encrypted_pin,
})
}
/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct DerivePinKeyResponse {
/// [UserKey] protected by PIN
pin_protected_user_key: EncString,
/// PIN protected by [UserKey]
encrypted_pin: EncString,
}
pub(super) async fn derive_pin_key(
client: &Client,
pin: String,
) -> Result<DerivePinKeyResponse, CryptoClientError> {
let login_method = client
.internal
.get_login_method()
.await
.ok_or(NotAuthenticatedError)?;
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
// FIXME: [PM-18099] Once PinKey deals with KeySlotIds, this should be updated
#[allow(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)?;
let pin_protected_user_key = derive_pin_protected_user_key(&pin, &login_method, user_key)?;
Ok(DerivePinKeyResponse {
pin_protected_user_key,
encrypted_pin: pin.encrypt_with_key(user_key)?,
})
}
pub(super) async fn derive_pin_user_key(
client: &Client,
encrypted_pin: EncString,
) -> Result<EncString, CryptoClientError> {
let login_method = client
.internal
.get_login_method()
.await
.ok_or(NotAuthenticatedError)?;
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
// FIXME: [PM-18099] Once PinKey deals with KeySlotIds, this should be updated
#[allow(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)?;
let pin: String = encrypted_pin.decrypt_with_key(user_key)?;
derive_pin_protected_user_key(&pin, &login_method, user_key)
}
fn derive_pin_protected_user_key(
pin: &str,
login_method: &UserLoginMethod,
user_key: &SymmetricCryptoKey,
) -> Result<EncString, CryptoClientError> {
use bitwarden_crypto::PinKey;
let derived_key = match login_method {
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. } => {
PinKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?
}
};
Ok(derived_key.encrypt_user_key(user_key)?)
}
pub(super) fn make_prf_user_key_set(
client: &Client,
prf: B64,
) -> Result<RotateableKeySet, CryptoClientError> {
let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())
.map_err(|_| CryptoClientError::InvalidPrfInput)?;
let ctx = client.internal.get_key_store().context();
let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeySlotId::User)?;
Ok(key_set)
}
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum EnrollAdminPasswordResetError {
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
}
pub(super) fn enroll_admin_password_reset(
client: &Client,
public_key: B64,
) -> Result<UnsignedSharedKey, EnrollAdminPasswordResetError> {
use bitwarden_crypto::PublicKey;
let public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(&public_key))?;
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
// FIXME: [PM-18110] This should be removed once the key store can handle public key encryption
#[allow(deprecated)]
let key = ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)?;
#[expect(deprecated)]
Ok(UnsignedSharedKey::encapsulate_key_unsigned(
key,
&public_key,
)?)
}
/// Request for migrating an account from password to key connector.
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct DeriveKeyConnectorRequest {
/// Encrypted user key, used to validate the master key
pub user_key_encrypted: EncString,
/// The user's master password
pub password: String,
/// The KDF parameters used to derive the master key
pub kdf: Kdf,
/// The user's email address
pub email: String,
}
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum DeriveKeyConnectorError {
#[error(transparent)]
WrongPassword(#[from] WrongPasswordError),
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
}
/// Derive the master key for migrating to the key connector
pub(super) fn derive_key_connector(
request: DeriveKeyConnectorRequest,
) -> Result<B64, DeriveKeyConnectorError> {
let master_key = MasterKey::derive(&request.password, &request.email, &request.kdf)?;
master_key
.decrypt_user_key(request.user_key_encrypted)
.map_err(|_| WrongPasswordError)?;
Ok(master_key.to_base64())
}
/// Response from the `make_key_pair` function
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct MakeKeyPairResponse {
/// The user's public key
user_public_key: B64,
/// User's private key, encrypted with the user key
user_key_encrypted_private_key: EncString,
}
pub(super) fn make_key_pair(user_key: B64) -> Result<MakeKeyPairResponse, CryptoError> {
let user_key = UserKey::new(SymmetricCryptoKey::try_from(user_key)?);
let key_pair = user_key.make_key_pair()?;
Ok(MakeKeyPairResponse {
user_public_key: key_pair.public,
user_key_encrypted_private_key: key_pair.private,
})
}
/// Request for `verify_asymmetric_keys`.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct VerifyAsymmetricKeysRequest {
/// The user's user key
user_key: B64,
/// The user's public key
user_public_key: B64,
/// User's private key, encrypted with the user key
user_key_encrypted_private_key: EncString,
}
/// Response for `verify_asymmetric_keys`.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct VerifyAsymmetricKeysResponse {
/// Whether the user's private key was decryptable by the user key.
private_key_decryptable: bool,
/// Whether the user's private key was a valid RSA key and matched the public key provided.
valid_private_key: bool,
}
pub(super) fn verify_asymmetric_keys(
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse, CryptoError> {
#[derive(Debug, thiserror::Error)]
enum VerifyError {
#[error("Failed to decrypt private key: {0:?}")]
DecryptFailed(bitwarden_crypto::CryptoError),
#[error("Failed to parse decrypted private key: {0:?}")]
ParseFailed(bitwarden_crypto::CryptoError),
#[error("Failed to derive a public key: {0:?}")]
PublicFailed(bitwarden_crypto::CryptoError),
#[error("Derived public key doesn't match")]
KeyMismatch,
}
fn verify_inner(
user_key: &SymmetricCryptoKey,
request: &VerifyAsymmetricKeysRequest,
) -> Result<(), VerifyError> {
let decrypted_private_key: Vec<u8> = request
.user_key_encrypted_private_key
.decrypt_with_key(user_key)
.map_err(VerifyError::DecryptFailed)?;
let decrypted_private_key = Pkcs8PrivateKeyBytes::from(decrypted_private_key);
let private_key =
PrivateKey::from_der(&decrypted_private_key).map_err(VerifyError::ParseFailed)?;
let derived_public_key_vec = private_key
.to_public_key()
.to_der()
.map_err(VerifyError::PublicFailed)?;
let derived_public_key = B64::from(derived_public_key_vec);
if derived_public_key != request.user_public_key {
return Err(VerifyError::KeyMismatch);
}
Ok(())
}
let user_key = SymmetricCryptoKey::try_from(request.user_key.clone())?;
Ok(match verify_inner(&user_key, &request) {
Ok(_) => VerifyAsymmetricKeysResponse {
private_key_decryptable: true,
valid_private_key: true,
},
Err(error) => {
tracing::debug!(%error, "User asymmetric keys verification");
VerifyAsymmetricKeysResponse {
private_key_decryptable: !matches!(error, VerifyError::DecryptFailed(_)),
valid_private_key: false,
}
}
})
}
/// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct UserCryptoV2KeysResponse {
/// User key
user_key: B64,
/// Wrapped private key
private_key: EncString,
/// Public key
public_key: B64,
/// The user's public key, signed by the signing key
signed_public_key: SignedPublicKey,
/// Signing key, encrypted with the user's symmetric key
signing_key: EncString,
/// Base64 encoded verifying key
verifying_key: B64,
/// The user's signed security state
security_state: SignedSecurityState,
/// The security state's version
security_version: u64,
}
/// Creates the user's cryptographic state for v2 users. This includes ensuring signature key pair
/// is present, a signed public key is present, a security state is present and signed, and the user
/// key is a Cose key.
#[deprecated(note = "Use AccountCryptographicState::rotate instead")]
pub(crate) fn make_v2_keys_for_v1_user(
client: &Client,
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context();
// Re-use existing private key
let private_key_id = PrivateKeySlotId::UserPrivateKey;
// Ensure that the function is only called for a V1 user.
if client.internal.get_security_version() != 1 {
return Err(StatefulCryptoError::WrongAccountCryptoVersion {
expected: "1".to_string(),
got: 2,
});
}
// Ensure the user has a private key.
// V1 user must have a private key to upgrade. This should be ensured by the client before
// calling the upgrade function.
if !ctx.has_private_key(PrivateKeySlotId::UserPrivateKey) {
return Err(StatefulCryptoError::Crypto(CryptoError::MissingKeyId(
"UserPrivateKey".to_string(),
)));
}
#[allow(deprecated)]
let private_key = ctx.dangerous_get_private_key(private_key_id)?.clone();
// New user key
let user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
// New signing key
let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
let temporary_signing_key_id = ctx.add_local_signing_key(signing_key.clone());
// Sign existing public key
let signed_public_key = ctx.make_signed_public_key(private_key_id, temporary_signing_key_id)?;
let public_key = private_key.to_public_key();
// Initialize security state for the user
let security_state = SecurityState::new();
let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?;
Ok(UserCryptoV2KeysResponse {
user_key: user_key.to_base64(),
private_key: private_key.to_der()?.encrypt_with_key(&user_key)?,
public_key: public_key.to_der()?.into(),
signed_public_key,
signing_key: signing_key.to_cose().encrypt_with_key(&user_key)?,
verifying_key: signing_key.to_verifying_key().to_cose().into(),
security_state: signed_security_state,
security_version: security_state.version(),
})
}
/// Gets a set of new wrapped account keys for a user, given a new user key.
///
/// In the current implementation, it just re-encrypts any existing keys. This function expects a
/// user to be a v2 user; that is, they have a signing key, a cose user-key, and a private key
#[deprecated(note = "Use AccountCryptographicState::rotate instead")]
pub(crate) fn get_v2_rotated_account_keys(
client: &Client,
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context();
// Ensure that the function is only called for a V2 user.
// V2 users have a security version 2 or higher.
if client.internal.get_security_version() == 1 {
return Err(StatefulCryptoError::WrongAccountCryptoVersion {
expected: "2+".to_string(),
got: 1,
});
}
let security_state = client
.internal
.security_state
.read()
.expect("RwLock is not poisoned")
.to_owned()
// This cannot occur since the security version check above already ensures that the
// security state is present.
.ok_or(StatefulCryptoError::MissingSecurityState)?;
#[expect(deprecated)]
let rotated_keys = dangerous_get_v2_rotated_account_keys(
PrivateKeySlotId::UserPrivateKey,
SigningKeySlotId::UserSigningKey,
&ctx,
)?;
Ok(UserCryptoV2KeysResponse {
user_key: rotated_keys.user_key.to_base64(),
private_key: rotated_keys.private_key,
public_key: rotated_keys.public_key.into(),
signed_public_key: rotated_keys.signed_public_key,
signing_key: rotated_keys.signing_key,
verifying_key: rotated_keys.verifying_key.into(),
security_state: security_state.sign(SigningKeySlotId::UserSigningKey, &mut ctx)?,
security_version: security_state.version(),
})
}
/// 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,
}
/// The response from `make_user_jit_master_password_registration`.
pub struct MakeJitMasterPasswordRegistrationResponse {
/// The account cryptographic state
pub account_cryptographic_state: WrappedAccountCryptographicState,
/// The user's user key
pub user_key: SymmetricCryptoKey,
/// The master password unlock data
pub master_password_authentication_data: MasterPasswordAuthenticationData,
/// The master password unlock data
pub master_password_unlock_data: MasterPasswordUnlockData,
/// The request model for the account cryptographic state (also called Account Keys)
pub account_keys_request: AccountKeysRequestModel,
/// The key needed for admin password reset
pub reset_password_key: UnsignedSharedKey,
}
/// Errors that can occur when making keys for account cryptography 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 derive master password
#[error("Failed to derive master password")]
MasterPasswordDerivation(MasterPasswordError),
/// 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,
org_public_key: B64,
) -> Result<MakeTdeRegistrationResponse, MakeKeysError> {
let mut ctx = client.internal.get_key_store().context_mut();
let (user_key_id, wrapped_state) = WrappedAccountCryptographicState::make(&mut ctx)
.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 = PublicKey::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,
})
}
/// The response from `make_user_key_connector_registration`.
pub struct MakeKeyConnectorRegistrationResponse {
/// The account cryptographic state
pub account_cryptographic_state: WrappedAccountCryptographicState,
/// Encrypted user's user key, wrapped with the key connector key
pub key_connector_key_wrapped_user_key: EncString,
/// 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 key connector key used for unlocking
pub key_connector_key: KeyConnectorKey,
}
/// Create the data needed to register for Key Connector
pub(crate) fn make_user_key_connector_registration(
client: &Client,
) -> Result<MakeKeyConnectorRegistrationResponse, MakeKeysError> {
let mut ctx = client.internal.get_key_store().context_mut();
let (user_key_id, wrapped_state) = WrappedAccountCryptographicState::make(&mut ctx)
.map_err(MakeKeysError::AccountCryptographyInitialization)?;
#[expect(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned();
// Key Connector unlock method
let key_connector_key = KeyConnectorKey::make();
let wrapped_user_key = key_connector_key
.encrypt_user_key(&user_key)
.map_err(MakeKeysError::Crypto)?;
let cryptography_state_request_model =
wrapped_state
.to_request_model(&user_key_id, &mut ctx)
.map_err(MakeKeysError::AccountCryptographyInitialization)?;
Ok(MakeKeyConnectorRegistrationResponse {
account_cryptographic_state: wrapped_state,
key_connector_key_wrapped_user_key: wrapped_user_key,
user_key,
account_keys_request: cryptography_state_request_model,
key_connector_key,
})
}
/// Ensures the [`SymmetricKeySlotId::LocalUserData`] key is loaded into the key store context.
///
/// On first call the key is generated (wrapping the user key with itself) and persisted to state.
/// Subsequent calls are idempotent: if the key already exists in state it is loaded as-is,
/// preserving any data that was previously encrypted with it (e.g. after a key rotation).
async fn initialize_user_local_data_key(client: &Client) -> Result<(), EncryptionSettingsError> {
let user_id = client
.internal
.get_user_id()
.ok_or(EncryptionSettingsError::LocalUserDataKeyInitFailed)?;
initialize_local_user_data_key_into_state(client, user_id)
.await
.map_err(|_| EncryptionSettingsError::LocalUserDataKeyInitFailed)?;
let wrapped_key = get_local_user_data_key_from_state(client, user_id)
.await
.map_err(|_| EncryptionSettingsError::LocalUserDataKeyLoadFailed)?;
let mut ctx = client.internal.get_key_store().context_mut();
wrapped_key
.unwrap_to_context(&mut ctx)
.map_err(|_| EncryptionSettingsError::LocalUserDataKeyLoadFailed)
}
/// Create the data needed to register for JIT master password
pub(crate) fn make_user_jit_master_password_registration(
client: &Client,
master_password: String,
salt: String,
org_public_key: B64,
) -> Result<MakeJitMasterPasswordRegistrationResponse, MakeKeysError> {
let mut ctx = client.internal.get_key_store().context_mut();
let (user_key_id, wrapped_state) = WrappedAccountCryptographicState::make(&mut ctx)
.map_err(MakeKeysError::AccountCryptographyInitialization)?;
let kdf = Kdf::default_argon2();
#[expect(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned();
let master_password_unlock_data =
MasterPasswordUnlockData::derive(&master_password, &kdf, &salt, user_key_id, &ctx)
.map_err(MakeKeysError::MasterPasswordDerivation)?;
let master_password_authentication_data =
MasterPasswordAuthenticationData::derive(&master_password, &kdf, &salt)
.map_err(MakeKeysError::MasterPasswordDerivation)?;
let cryptography_state_request_model = wrapped_state
.to_request_model(&user_key_id, &mut ctx)
.map_err(|_| MakeKeysError::RequestModelCreation)?;
// Account recovery enrollment
let public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(&org_public_key))
.map_err(MakeKeysError::Crypto)?;
let admin_reset_key = UnsignedSharedKey::encapsulate(user_key_id, &public_key, &ctx)
.map_err(MakeKeysError::Crypto)?;
Ok(MakeJitMasterPasswordRegistrationResponse {
account_cryptographic_state: wrapped_state,
user_key,
master_password_unlock_data,
master_password_authentication_data,
account_keys_request: cryptography_state_request_model,
reset_password_key: admin_reset_key,
})
}
/// Response from `make_user_password_registration`
pub struct MakeUserMasterPasswordRegistrationResponse {
/// The wrapped account cryptographic state
pub account_cryptographic_state: WrappedAccountCryptographicState,
/// The master password unlock data
pub master_password_unlock_data: MasterPasswordUnlockData,
/// The master password authentication data
pub master_password_authentication_data: MasterPasswordAuthenticationData,
/// The request model for account cryptographic key state
pub account_keys_request: AccountKeysRequestModel,
/// The user's user key
pub user_key: SymmetricCryptoKey,
}
/// Creates cryptographic data needed for user master password registration
pub(crate) fn make_user_password_registration(
client: &Client,
master_password: String,
salt: String,
) -> Result<MakeUserMasterPasswordRegistrationResponse, MakeKeysError> {
// make_user_v2_crypto_state() - Creates user key (xchacha20-poly1305), RSA keypair, ed25519
// signature keypair, and signed security state
let mut ctx = client.internal.get_key_store().context_mut();
let (user_key_id, wrapped_state) = WrappedAccountCryptographicState::make(&mut ctx)
.map_err(MakeKeysError::AccountCryptographyInitialization)?;
let kdf = Kdf::default_argon2();
#[expect(deprecated)]
let user_key = ctx.dangerous_get_symmetric_key(user_key_id)?.to_owned();
let master_password_unlock_data =
MasterPasswordUnlockData::derive(&master_password, &kdf, &salt, user_key_id, &ctx)
.map_err(MakeKeysError::MasterPasswordDerivation)?;
let master_password_authentication_data =
MasterPasswordAuthenticationData::derive(&master_password, &kdf, &salt)
.map_err(MakeKeysError::MasterPasswordDerivation)?;
let account_keys_request = wrapped_state
.to_request_model(&user_key_id, &mut ctx)
.map_err(|_| MakeKeysError::RequestModelCreation)?;
Ok(MakeUserMasterPasswordRegistrationResponse {
account_cryptographic_state: wrapped_state,
master_password_unlock_data,
master_password_authentication_data,
account_keys_request,
user_key,
})
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use bitwarden_crypto::{
Decryptable, KeyStore, PrivateKey, PublicKeyEncryptionAlgorithm, RsaKeyPair,
SymmetricKeyAlgorithm,
};
use super::*;
use crate::{
Client,
client::test_accounts::{test_bitwarden_com_account, test_bitwarden_com_account_v2},
key_management::{KeySlotIds, V2UpgradeToken},
};
const TEST_VECTOR_USER_KEY_V2_B64: &str = "pQEEAlACHUUoybNAuJoZzqNMxz2bAzoAARFvBIQDBAUGIFggAvGl4ifaUAomQdCdUPpXLHtypiQxHjZwRHeI83caZM4B";
const TEST_VECTOR_PRIVATE_KEY_V2: &str = "7.g1gdowE6AAERbwMZARwEUAIdRSjJs0C4mhnOo0zHPZuhBVgYthGLGqVLPeidY8mNMxpLJn3fyeSxyaWsWQTR6pxmRV2DyGZXly/0l9KK+Rsfetl9wvYIz0O4/RW3R6wf7eGxo5XmicV3WnFsoAmIQObxkKWShxFyjzg+ocKItQDzG7Gp6+MW4biTrAlfK51ML/ZS+PCjLmgI1QQr4eMHjiwA2TBKtKkxfjoTJkMXECpRVLEXOo8/mbIGYkuabbSA7oU+TJ0yXlfKDtD25gnyO7tjW/0JMFUaoEKRJOuKoXTN4n/ks4Hbxk0X5/DzfG05rxWad2UNBjNg7ehW99WrQ+33ckdQFKMQOri/rt8JzzrF1k11/jMJ+Y2TADKNHr91NalnUX+yqZAAe3sRt5Pv5ZhLIwRMKQi/1NrLcsQPRuUnogVSPOoMnE/eD6F70iU60Z6pvm1iBw2IvELZcrs/oxpO2SeCue08fIZW/jNZokbLnm90tQ7QeZTUpiPALhUgfGOa3J9VOJ7jQGCqDjd9CzV2DCVfhKCapeTbldm+RwEWBz5VvorH5vMx1AzbPRJxdIQuxcg3NqRrXrYC7fyZljWaPB9qP1tztiPtd1PpGEgxLByIfR6fqyZMCvOBsWbd0H6NhF8mNVdDw60+skFRdbRBTSCjCtKZeLVuVFb8ioH45PR5oXjtx4atIDzu6DKm6TTMCbR6DjZuZZ8GbwHxuUD2mDD3pAFhaof9kR3lQdjy7Zb4EzUUYskQxzcLPcqzp9ZgB3Rg91SStBCCMhdQ6AnhTy+VTGt/mY5AbBXNRSL6fI0r+P9K8CcEI4bNZCDkwwQr5v4O4ykSUzIvmVU0zKzDngy9bteIZuhkvGUoZlQ9UATNGPhoLfqq2eSvqEXkCbxTVZ5D+Ww9pHmWeVcvoBhcl5MvicfeQt++dY3tPjIfZq87nlugG4HiNbcv9nbVpgwe3v8cFetWXQgnO4uhx8JHSwGoSuxHFZtl2sdahjTHavRHnYjSABEFrViUKgb12UDD5ow1GAL62wVdSJKRf9HlLbJhN3PBxuh5L/E0wy1wGA9ecXtw/R1ktvXZ7RklGAt1TmNzZv6vI2J/CMXvndOX9rEpjKMbwbIDAjQ9PxiWdcnmc5SowT9f6yfIjbjXnRMWWidPAua7sgrtej4HP4Qjz1fpgLMLCRyF97tbMTmsAI5Cuj98Buh9PwcdyXj5SbVuHdJS1ehv9b5SWPsD4pwOm3+otVNK6FTazhoUl47AZoAoQzXfsXxrzqYzvF0yJkCnk9S1dcij1L569gQ43CJO6o6jIZFJvA4EmZDl95ELu+BC+x37Ip8dq4JLPsANDVSqvXO9tfDUIXEx25AaOYhW2KAUoDve/fbsU8d0UZR1o/w+ZrOQwawCIPeVPtbh7KFRVQi/rPI+Abl6XR6qMJbKPegliYGUuGF2oEMEc6QLTsMRCEPuw0S3kxbNfVPqml8nGhB2r8zUHBY1diJEmipVghnwH74gIKnyJ2C9nKjV8noUfKzqyV8vxUX2G5yXgodx8Jn0cWs3XhWuApFla9z4R28W/4jA1jK2WQMlx+b6xKUWgRk8+fYsc0HSt2fDrQ9pLpnjb8ME59RCxSPV++PThpnR2JtastZBZur2hBIJsGILCAmufUU4VC4gBKPhNfu/OK4Ktgz+uQlUa9fEC/FnkpTRQPxHuQjSQSNrIIyW1bIRBtnwjvvvNoui9FZJ";
#[allow(unused)]
const TEST_VECTOR_PUBLIC_KEY_V2: &str = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/+1jPJ1HqcaCdKrTPms8XJcvnmd9alI42U2XF/4GMNTM5KF1gI6snhR/23ZLatZRFMHoK8ZCMSpGNkjLadArz52ldceTvBOhQUiWylkZQ4NfNa3xIYJubXOmkeDyfNuyLxVZvcZOko9PdT+Qx2QxDrFi2XNo2I7aVFd19/COIEkex4mJ0eA3MHFpKCdxYbcTAsGID8+kVR9L84S1JptZoG8x+iB/D3/Q4y02UsQYpFTu0vbPY84YmW03ngJdxWzS8X4/UJI/jaEn5rO4xlU5QcL0l4IybP5LRpE9XEeUHATKVOG7eNfpe9zDfKV2qQoofQMH9VvkWO4psaWDjBSdwIDAQAB";
#[allow(unused)]
const TEST_VECTOR_SIGNED_PUBLIC_KEY_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8BoFkBTqNpYWxnb3JpdGhtAG1jb250ZW50Rm9ybWF0AGlwdWJsaWNLZXlZASYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDP/7WM8nUepxoJ0qtM+azxcly+eZ31qUjjZTZcX/gYw1MzkoXWAjqyeFH/bdktq1lEUwegrxkIxKkY2SMtp0CvPnaV1x5O8E6FBSJbKWRlDg181rfEhgm5tc6aR4PJ827IvFVm9xk6Sj091P5DHZDEOsWLZc2jYjtpUV3X38I4gSR7HiYnR4DcwcWkoJ3FhtxMCwYgPz6RVH0vzhLUmm1mgbzH6IH8Pf9DjLTZSxBikVO7S9s9jzhiZbTeeAl3FbNLxfj9Qkj+NoSfms7jGVTlBwvSXgjJs/ktGkT1cR5QcBMpU4bt41+l73MN8pXapCih9Awf1W+RY7imxpYOMFJ3AgMBAAFYQMq/hT4wod2w8xyoM7D86ctuLNX4ZRo+jRHf2sZfaO7QsvonG/ZYuNKF5fq8wpxMRjfoMvnY2TTShbgzLrW8BA4=";
const TEST_VECTOR_SIGNING_KEY_V2: &str = "7.g1gcowE6AAERbwMYZQRQAh1FKMmzQLiaGc6jTMc9m6EFWBhYePc2qkCruHAPXgbzXsIP1WVk11ArbLNYUBpifToURlwHKs1je2BwZ1C/5thz4nyNbL0wDaYkRWI9ex1wvB7KhdzC7ltStEd5QttboTSCaXQROSZaGBPNO5+Bu3sTY8F5qK1pBUo6AHNN";
#[allow(unused)]
const TEST_VECTOR_VERIFYING_KEY_V2: &str =
"pgEBAlAmkP0QgfdMVbIujX55W/yNAycEgQIgBiFYIEM6JxBmjWQTruAm3s6BTaJy1q6BzQetMBacNeRJ0kxR";
const TEST_VECTOR_SECURITY_STATE_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8CoFgkomhlbnRpdHlJZFBHOOw2BI9OQoNq+Vl1xZZKZ3ZlcnNpb24CWEAlchbJR0vmRfShG8On7Q2gknjkw4Dd6MYBLiH4u+/CmfQdmjNZdf6kozgW/6NXyKVNu8dAsKsin+xxXkDyVZoG";
const TEST_USER_EMAIL: &str = "test@bitwarden.com";
const TEST_USER_PASSWORD: &str = "asdfasdfasdf";
const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
const TEST_ACCOUNT_PRIVATE_KEY: &str = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=";
#[tokio::test]
async fn test_update_kdf() {
let client = Client::new_test(None);
let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap();
let kdf = Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
};
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: kdf.clone(),
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 { private_key: priv_key.to_owned() },
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: kdf.clone(),
master_key_wrapped_user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(),
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let new_kdf = Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
};
let new_kdf_response = make_update_kdf(&client, "123412341234", &new_kdf)
.await
.unwrap();
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: new_kdf.clone(),
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: priv_key.to_owned(),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "123412341234".to_string(),
master_password_unlock: MasterPasswordUnlockData {
kdf: new_kdf.clone(),
master_key_wrapped_user_key: new_kdf_response
.master_password_unlock_data
.master_key_wrapped_user_key,
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let new_hash = client2
.kdf()
.hash_password(
"test@bitwarden.com".into(),
"123412341234".into(),
new_kdf.clone(),
bitwarden_crypto::HashPurpose::ServerAuthorization,
)
.await
.unwrap();
assert_eq!(
new_hash,
new_kdf_response
.master_password_authentication_data
.master_password_authentication_hash
);
let client_key = {
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
let client2_key = {
let key_store = client2.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
assert_eq!(client_key, client2_key);
}
#[tokio::test]
async fn test_update_password() {
let client = Client::new_test(None);
let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap();
let kdf = Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
};
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: kdf.clone(),
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 { private_key: priv_key.to_owned() },
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".to_string(),
master_password_unlock: MasterPasswordUnlockData {
kdf: kdf.clone(),
master_key_wrapped_user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(),
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let new_password_response = make_update_password(&client, "123412341234".into())
.await
.unwrap();
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: kdf.clone(),
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: priv_key.to_owned(),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "123412341234".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: kdf.clone(),
master_key_wrapped_user_key: new_password_response.new_key,
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let new_hash = client2
.kdf()
.hash_password(
"test@bitwarden.com".into(),
"123412341234".into(),
kdf.clone(),
bitwarden_crypto::HashPurpose::ServerAuthorization,
)
.await
.unwrap();
assert_eq!(new_hash, new_password_response.password_hash);
let client_key = {
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
let client2_key = {
let key_store = client2.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
assert_eq!(client_key, client2_key);
}
#[tokio::test]
async fn test_initialize_user_crypto_pin() {
let client = Client::new_test(None);
let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap();
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 { private_key: priv_key.to_owned() },
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
master_key_wrapped_user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(),
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let pin_key = derive_pin_key(&client, "1234".into()).await.unwrap();
// Verify we can unlock with the pin
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: priv_key.to_owned(),
},
method: InitUserCryptoMethod::Pin {
pin: "1234".into(),
pin_protected_user_key: pin_key.pin_protected_user_key,
},
upgrade_token: None,
},
)
.await
.unwrap();
let client_key = {
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
let client2_key = {
let key_store = client2.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
assert_eq!(client_key, client2_key);
// Verify we can derive the pin protected user key from the encrypted pin
let pin_protected_user_key = derive_pin_user_key(&client, pin_key.encrypted_pin)
.await
.unwrap();
let client3 = Client::new_test(None);
initialize_user_crypto(
&client3,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: priv_key.to_owned(),
},
method: InitUserCryptoMethod::Pin {
pin: "1234".into(),
pin_protected_user_key,
},
upgrade_token: None,
},
)
.await
.unwrap();
let client_key = {
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
let client3_key = {
let key_store = client3.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
ctx.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap()
.to_base64()
};
assert_eq!(client_key, client3_key);
}
#[tokio::test]
async fn test_initialize_user_crypto_pin_envelope() {
let user_key = "5yKAZ4TSSEGje54MV5lc5ty6crkqUz4xvl+8Dm/piNLKf6OgRi2H0uzttNTXl9z6ILhkmuIXzGpAVc2YdorHgQ==";
let test_pin = "1234";
let client1 = Client::new_test(None);
initialize_user_crypto(
&client1,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: make_key_pair(user_key.try_into().unwrap())
.unwrap()
.user_key_encrypted_private_key,
},
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: user_key.to_string(),
},
upgrade_token: None,
},
)
.await
.unwrap();
let enroll_response = client1.crypto().enroll_pin(test_pin.to_string()).unwrap();
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
// NOTE: THIS CHANGES KDF SETTINGS. We ensure in this test that even with different
// KDF settings the pin can unlock the user key.
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: make_key_pair(user_key.try_into().unwrap())
.unwrap()
.user_key_encrypted_private_key,
},
method: InitUserCryptoMethod::PinEnvelope {
pin: test_pin.to_string(),
pin_protected_user_key_envelope: enroll_response
.pin_protected_user_key_envelope,
},
upgrade_token: None,
},
)
.await
.unwrap();
}
#[test]
fn test_enroll_admin_password_reset() {
let client = Client::new(None);
let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();
let private_key = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
client
.internal
.initialize_user_crypto_master_password_unlock(
"asdfasdfasdf".to_string(),
MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
master_key_wrapped_user_key: user_key,
salt: "test@bitwarden.com".to_string(),
},
WrappedAccountCryptographicState::V1 { private_key },
&None,
)
.unwrap();
let public_key: B64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB".parse().unwrap();
let encrypted = enroll_admin_password_reset(&client, public_key).unwrap();
let private_key: B64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ=".parse().unwrap();
let private_key = Pkcs8PrivateKeyBytes::from(private_key.as_bytes());
let private_key = PrivateKey::from_der(&private_key).unwrap();
#[expect(deprecated)]
let decrypted: SymmetricCryptoKey =
encrypted.decapsulate_key_unsigned(&private_key).unwrap();
let key_store = client.internal.get_key_store();
let ctx = key_store.context();
#[allow(deprecated)]
let expected = ctx
.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.unwrap();
assert_eq!(decrypted, *expected);
}
#[test]
fn test_derive_key_connector() {
let request = DeriveKeyConnectorRequest {
password: "asdfasdfasdf".to_string(),
email: "test@bitwarden.com".to_string(),
kdf: Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
user_key_encrypted: "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(),
};
let result = derive_key_connector(request).unwrap();
assert_eq!(
result.to_string(),
"ySXq1RVLKEaV1eoQE/ui9aFKIvXTl9PAXwp1MljfF50="
);
}
fn setup_asymmetric_keys_test() -> (UserKey, RsaKeyPair) {
let master_key = MasterKey::derive(
"asdfasdfasdf",
"test@bitwarden.com",
&Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
)
.unwrap();
let user_key = (master_key.make_user_key().unwrap()).0;
let key_pair = user_key.make_key_pair().unwrap();
(user_key, key_pair)
}
#[test]
fn test_make_key_pair() {
let (user_key, _) = setup_asymmetric_keys_test();
let response = make_key_pair(user_key.0.to_base64()).unwrap();
assert!(!response.user_public_key.to_string().is_empty());
let encrypted_private_key = response.user_key_encrypted_private_key;
let private_key: Vec<u8> = encrypted_private_key.decrypt_with_key(&user_key.0).unwrap();
assert!(!private_key.is_empty());
}
#[test]
fn test_verify_asymmetric_keys_success() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: key_pair.private,
};
let response = verify_asymmetric_keys(request).unwrap();
assert!(response.private_key_decryptable);
assert!(response.valid_private_key);
}
#[test]
fn test_verify_asymmetric_keys_decrypt_failed() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
let undecryptable_private_key = "2.cqD39M4erPZ3tWaz2Fng9w==|+Bsp/xvM30oo+HThKN12qirK0A63EjMadcwethCX7kEgfL5nEXgAFsSgRBMpByc1djgpGDMXzUTLOE+FejXRsrEHH/ICZ7jPMgSR+lV64Mlvw3fgvDPQdJ6w3MCmjPueGQtrlPj1K78BkRomN3vQwwRBFUIJhLAnLshTOIFrSghoyG78na7McqVMMD0gmC0zmRaSs2YWu/46ES+2Rp8V5OC4qdeeoJM9MQfaOtmaqv7NRVDeDM3DwoyTJAOcon8eovMKE4jbFPUboiXjNQBkBgjvLhco3lVJnFcQuYgmjqrwuUQRsfAtZjxFXg/RQSH2D+SI5uRaTNQwkL4iJqIw7BIKtI0gxDz6eCVdq/+DLhpImgCV/aaIhF/jkpGqLCceFsYMbuqdULMM1VYKgV+IAuyC65R+wxOaKS+1IevvPnNp7tgKAvT5+shFg8piusj+rQ49daX2SmV2OImwdWMmmX93bcVV0xJ/WYB1yrqmyRUcTwyvX3RQF25P5okIIzFasRp8jXFZe8C6f93yzkn1TPQbp95zF4OsWjfPFVH4hzca07ACt2HjbAB75JakWbFA5MbCF8aOIwIfeLVhVlquQXCldOHCsl22U/f3HTGLB9OS8F83CDAy7qZqpKha9Im8RUhHoyf+lXrky0gyd6un7Ky8NSkVOGd8CEG7bvZfutxv/qtAjEM9/lV78fh8TQIy9GNgioMzplpuzPIJOgMaY/ZFZj6a8H9OMPneN5Je0H/DwHEglSyWy7CMgwcbQgXYGXc8rXTTxL71GUAFHzDr4bAJvf40YnjndoL9tf+oBw8vVNUccoD4cjyOT5w8h7M3Liaxk9/0O8JR98PKxxpv1Xw6XjFCSEHeG2y9FgDUASFR4ZwG1qQBiiLMnJ7e9kvxsdnmasBux9H0tOdhDhAM16Afk3NPPKA8eztJVHJBAfQiaNiUA4LIJ48d8EpUAe2Tvz0WW/gQThplUINDTpvPf+FojLwc5lFwNIPb4CVN1Ui8jOJI5nsOw4BSWJvLzJLxawHxX/sBuK96iXza+4aMH+FqYKt/twpTJtiVXo26sPtHe6xXtp7uO4b+bL9yYUcaAci69L0W8aNdu8iF0lVX6kFn2lOL8dBLRleGvixX9gYEVEsiI7BQBjxEBHW/YMr5F4M4smqCpleZIAxkse1r2fQ33BSOJVQKInt4zzgdKwrxDzuVR7RyiIUuNXHsprKtRHNJrSc4x5kWFUeivahed2hON+Ir/ZvrxYN6nJJPeYYH4uEm1Nn4osUzzfWILlqpmDPK1yYy365T38W8wT0cbdcJrI87ycS37HeB8bzpFJZSY/Dzv48Yy19mDZJHLJLCRqyxNeIlBPsVC8fvxQhzr+ZyS3Wi8Dsa2Sgjt/wd0xPULLCJlb37s+1aWgYYylr9QR1uhXheYfkXFED+saGWwY1jlYL5e2Oo9n3sviBYwJxIZ+RTKFgwlXV5S+Jx/MbDpgnVHP1KaoU6vvzdWYwMChdHV/6PhZVbeT2txq7Qt+zQN59IGrOWf6vlMkHxfUzMTD58CE+xAaz/D05ljHMesLj9hb3MSrymw0PcwoFGWUMIzIQE73pUVYNE7fVHa8HqUOdoxZ5dRZqXRVox1xd9siIPE3e6CuVQIMabTp1YLno=|Y38qtTuCwNLDqFnzJ3Cgbjm1SE15OnhDm9iAMABaQBA=".parse().unwrap();
let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: undecryptable_private_key,
};
let response = verify_asymmetric_keys(request).unwrap();
assert!(!response.private_key_decryptable);
assert!(!response.valid_private_key);
}
#[test]
fn test_verify_asymmetric_keys_parse_failed() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
let invalid_private_key = "bad_key".to_string().encrypt_with_key(&user_key.0).unwrap();
let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: invalid_private_key,
};
let response = verify_asymmetric_keys(request).unwrap();
assert!(response.private_key_decryptable);
assert!(!response.valid_private_key);
}
#[test]
fn test_verify_asymmetric_keys_key_mismatch() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
let new_key_pair = user_key.make_key_pair().unwrap();
let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: new_key_pair.private,
};
let response = verify_asymmetric_keys(request).unwrap();
assert!(response.private_key_decryptable);
assert!(!response.valid_private_key);
}
#[tokio::test]
async fn test_make_v2_keys_for_v1_user() {
let client = Client::new_test(None);
let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap();
let encrypted_userkey: EncString = "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap();
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: priv_key.to_owned(),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
master_key_wrapped_user_key: encrypted_userkey.clone(),
salt: "test@bitwarden.com".into(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let master_key = MasterKey::derive(
"asdfasdfasdf",
"test@bitwarden.com",
&Kdf::PBKDF2 {
iterations: NonZeroU32::new(100_000).unwrap(),
},
)
.unwrap();
#[expect(deprecated)]
let enrollment_response = make_v2_keys_for_v1_user(&client).unwrap();
let encrypted_userkey_v2 = master_key
.encrypt_user_key(
&SymmetricCryptoKey::try_from(enrollment_response.clone().user_key).unwrap(),
)
.unwrap();
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V2 {
private_key: enrollment_response.private_key,
signing_key: enrollment_response.signing_key,
security_state: enrollment_response.security_state,
signed_public_key: Some(enrollment_response.signed_public_key),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
master_key_wrapped_user_key: encrypted_userkey_v2,
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
}
#[tokio::test]
async fn test_make_v2_keys_for_v1_user_with_v2_user_fails() {
let client = Client::new_test(None);
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V2 {
private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
signed_public_key: Some(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap()),
},
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(),
},
upgrade_token: None,
},
)
.await
.unwrap();
#[expect(deprecated)]
let result = make_v2_keys_for_v1_user(&client);
assert!(matches!(
result,
Err(StatefulCryptoError::WrongAccountCryptoVersion {
expected: _,
got: _
})
));
}
#[test]
fn test_get_v2_rotated_account_keys_non_v2_user() {
let client = Client::new(None);
let mut ctx = client.internal.get_key_store().context_mut();
let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
.unwrap();
drop(ctx);
#[expect(deprecated)]
let result = get_v2_rotated_account_keys(&client);
assert!(matches!(
result,
Err(StatefulCryptoError::WrongAccountCryptoVersion {
expected: _,
got: _
})
));
}
#[tokio::test]
async fn test_get_v2_rotated_account_keys() {
let client = Client::new_test(None);
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V2 {
private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
signed_public_key: Some(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap()),
},
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(),
},
upgrade_token: None,
},
)
.await
.unwrap();
#[expect(deprecated)]
let result = get_v2_rotated_account_keys(&client);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_initialize_user_crypto_master_password_unlock() {
let client = Client::new_test(None);
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: TEST_USER_EMAIL.to_string(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: TEST_USER_PASSWORD.to_string(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(),
salt: TEST_USER_EMAIL.to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let key_store = client.internal.get_key_store();
{
let context = key_store.context();
assert!(context.has_symmetric_key(SymmetricKeySlotId::User));
assert!(context.has_private_key(PrivateKeySlotId::UserPrivateKey));
}
let login_method = client.internal.get_login_method().await.unwrap();
if let UserLoginMethod::Username {
email,
kdf,
client_id,
..
} = login_method
{
assert_eq!(&email, TEST_USER_EMAIL);
assert_eq!(
kdf,
Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
}
);
assert_eq!(&client_id, "");
} else {
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 = PrivateKey::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_test(None);
let make_keys_response = registration_client
.crypto()
.make_user_tde_registration(org_public_key)
.expect("TDE registration should succeed");
// Initialize a new client using the TDE device key
let unlock_client = Client::new_test(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,
},
upgrade_token: None,
})
.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
#[expect(deprecated)]
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"
);
}
#[tokio::test]
async fn test_make_user_key_connector_registration_success() {
let user_id = UserId::new_v4();
let email = "test@bitwarden.com";
let registration_client = Client::new(None);
let make_keys_response = make_user_key_connector_registration(®istration_client);
assert!(make_keys_response.is_ok());
let make_keys_response = make_keys_response.unwrap();
// Initialize a new client using the key connector key
let unlock_client = Client::new_test(None);
unlock_client
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: Some(user_id),
kdf_params: Kdf::default_argon2(),
email: email.to_owned(),
account_cryptographic_state: make_keys_response.account_cryptographic_state,
method: InitUserCryptoMethod::KeyConnector {
user_key: make_keys_response
.key_connector_key_wrapped_user_key
.clone(),
master_key: make_keys_response.key_connector_key.clone().into(),
},
upgrade_token: None,
})
.await
.expect("initializing user crypto with key connector 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");
assert_eq!(retrieved_symmetric_key, make_keys_response.user_key);
let decrypted_user_key = make_keys_response
.key_connector_key
.decrypt_user_key(make_keys_response.key_connector_key_wrapped_user_key);
assert_eq!(retrieved_symmetric_key, decrypted_user_key.unwrap());
}
#[tokio::test]
async fn test_initialize_user_crypto_with_upgrade_token_upgrades_v1_to_v2() {
let client1 = Client::init_test_account(test_bitwarden_com_account()).await;
let expected_v2_key =
SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
let upgrade_token = {
let mut ctx = client1.internal.get_key_store().context_mut();
let v2_key_id = ctx.add_local_symmetric_key(expected_v2_key.clone());
V2UpgradeToken::create(SymmetricKeySlotId::User, v2_key_id, &ctx).unwrap()
};
let client2 = Client::new_test(None);
initialize_user_crypto(
&client2,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V2 {
private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
signed_public_key: Some(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap()),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(),
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: Some(upgrade_token),
},
)
.await
.unwrap();
// The active user key must now be V2 and match the test-vector V2 key.
let result_key =
SymmetricCryptoKey::try_from(get_user_encryption_key(&client2).await.unwrap()).unwrap();
assert!(
matches!(result_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)),
"User key should be upgraded to V2 after initialization with upgrade token"
);
assert_eq!(result_key, expected_v2_key);
}
#[tokio::test]
async fn test_initialize_user_crypto_with_upgrade_token_ignored_for_v2_key() {
let dummy_token = {
let key_store = KeyStore::<KeySlotIds>::default();
let mut ctx = key_store.context_mut();
let v1_id = ctx.generate_symmetric_key();
let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
V2UpgradeToken::create(v1_id, v2_id, &ctx).unwrap()
};
let client = Client::new_test(None);
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V2 {
private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(),
signing_key: TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap(),
security_state: TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap(),
signed_public_key: Some(TEST_VECTOR_SIGNED_PUBLIC_KEY_V2.parse().unwrap()),
},
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(),
},
upgrade_token: Some(dummy_token),
},
)
.await
.unwrap();
// The upgrade token must have been ignored; the original V2 key must still be active
let result_key =
SymmetricCryptoKey::try_from(get_user_encryption_key(&client).await.unwrap()).unwrap();
assert!(
matches!(result_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)),
"Upgrade token must be ignored for a V2 user key"
);
let expected_key =
SymmetricCryptoKey::try_from(TEST_VECTOR_USER_KEY_V2_B64.to_string()).unwrap();
assert_eq!(result_key, expected_key);
}
#[tokio::test]
async fn test_initialize_user_crypto_with_invalid_upgrade_token_fails() {
// Token built with a different V1 key — decryption with the test account's V1 key fails.
let mismatched_token = {
let key_store = KeyStore::<KeySlotIds>::default();
let mut ctx = key_store.context_mut();
let wrong_v1_id = ctx.generate_symmetric_key();
let v2_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
V2UpgradeToken::create(wrong_v1_id, v2_id, &ctx).unwrap()
};
let client = Client::new_test(None);
let result = initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: "test@bitwarden.com".into(),
account_cryptographic_state: WrappedAccountCryptographicState::V1 {
// The private key is never decrypted because the token fails first.
private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(),
},
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: "asdfasdfasdf".into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(),
salt: "test@bitwarden.com".to_string(),
},
},
upgrade_token: Some(mismatched_token),
},
)
.await;
assert!(
matches!(result, Err(EncryptionSettingsError::InvalidUpgradeToken)),
"Initialization with a mismatched upgrade token should fail"
);
}
#[tokio::test]
async fn test_initialize_user_local_data_key_sets_local_user_data_key_equal_to_user_key() {
let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
initialize_user_local_data_key(&client)
.await
.expect("initialize_user_local_data_key should succeed");
// Verify LocalUserData key equals the User key: data encrypted with User
// must be decryptable with LocalUserData.
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();
let plaintext = "test";
let ciphertext = plaintext
.encrypt(&mut ctx, SymmetricKeySlotId::User)
.expect("encryption with user key should succeed");
let decrypted: String = ciphertext
.decrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
.expect("decryption with local user data key should succeed");
assert_eq!(decrypted, plaintext);
}
#[tokio::test]
async fn test_initialize_org_crypto_persists_org_keys() {
use crate::{OrganizationId, client::persisted_state::OrganizationSharedKey};
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let org_id: OrganizationId = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap();
let repo = client
.internal
.state_registry
.get::<OrganizationSharedKey>()
.expect("OrganizationSharedKey repository should be available");
let persisted = repo
.get(org_id)
.await
.expect("repository get should not fail");
let entry = persisted.expect("org key should be persisted after initialize_org_crypto");
assert_eq!(entry.org_id, org_id);
}
#[tokio::test]
async fn test_initialize_user_crypto_persists_account_crypto_state() {
use crate::client::persisted_state::ACCOUNT_CRYPTO_STATE;
let account_crypto_state = WrappedAccountCryptographicState::V1 {
private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(),
};
let client = Client::new_test(None);
initialize_user_crypto(
&client,
InitUserCryptoRequest {
user_id: Some(UserId::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: TEST_USER_EMAIL.into(),
account_cryptographic_state: account_crypto_state.clone(),
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: TEST_USER_PASSWORD.into(),
master_password_unlock: MasterPasswordUnlockData {
kdf: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(),
salt: TEST_USER_EMAIL.to_string(),
},
},
upgrade_token: None,
},
)
.await
.unwrap();
let persisted = client
.internal
.state_registry
.setting(ACCOUNT_CRYPTO_STATE)
.expect("ACCOUNT_CRYPTO_STATE setting should be available")
.get()
.await
.expect("setting get should not fail");
assert_eq!(persisted, Some(account_crypto_state));
}
#[tokio::test]
async fn test_initialize_user_local_data_key_idempotent() {
let client = Client::init_test_account(test_bitwarden_com_account_v2()).await;
initialize_user_local_data_key(&client)
.await
.expect("first initialization should succeed");
// Encrypt something with the key established on the first call.
let ciphertext = {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();
"test"
.encrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
.expect("encryption should succeed")
};
initialize_user_local_data_key(&client)
.await
.expect("second initialization should succeed");
// The key must not have changed: data encrypted before the second call
// must still be decryptable.
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();
let decrypted: String = ciphertext
.decrypt(&mut ctx, SymmetricKeySlotId::LocalUserData)
.expect("decryption after second initialization should succeed");
assert_eq!(decrypted, "test");
}
#[tokio::test]
async fn test_make_user_password_registration() {
let user_id = UserId::new_v4();
let registration_client = Client::new(None);
let make_keys_response = registration_client
.crypto()
.make_user_password_registration(
TEST_USER_PASSWORD.to_string(),
TEST_USER_EMAIL.to_string(),
)
.expect("user password registration should succeed");
let unlock_client = Client::new_test(None);
unlock_client
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: Some(user_id),
kdf_params: Kdf::default_argon2(),
email: TEST_USER_EMAIL.to_string(),
account_cryptographic_state: make_keys_response.account_cryptographic_state,
method: InitUserCryptoMethod::MasterPasswordUnlock {
password: TEST_USER_PASSWORD.to_string(),
master_password_unlock: make_keys_response.master_password_unlock_data.clone(),
},
upgrade_token: None,
})
.await
.expect("initializing user crypto with master password should succeed");
let retrieved_key = unlock_client
.crypto()
.get_user_encryption_key()
.await
.expect("should be able to get user encryption key");
let retrieved_symmetric_key = SymmetricCryptoKey::try_from(retrieved_key)
.expect("retrieved key should be valid symmetric key");
let master_key = MasterKey::derive(
TEST_USER_PASSWORD,
TEST_USER_EMAIL,
&make_keys_response.master_password_unlock_data.kdf,
)
.expect("master key should derive");
let decrypted_user_key = master_key
.decrypt_user_key(
make_keys_response
.master_password_unlock_data
.master_key_wrapped_user_key
.clone(),
)
.expect("should decrypt user key");
assert_eq!(retrieved_symmetric_key, decrypted_user_key);
}
}