use std::sync::RwLock;
use bitwarden_api_api::models::{
AccountKeysRequestModel, PrivateKeysResponseModel, SecurityStateModel,
WrappedAccountCryptographicStateRequestModel,
};
use bitwarden_crypto::{
CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext,
PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm,
};
use bitwarden_encoding::B64;
use bitwarden_error::bitwarden_error;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{info, instrument};
#[cfg(feature = "wasm")]
use tsify::Tsify;
use crate::{
MissingFieldError,
key_management::{
KeySlotIds, PrivateKeySlotId, SecurityState, SignedSecurityState, SigningKeySlotId,
SymmetricKeySlotId,
},
require,
};
#[derive(Debug, Error)]
#[bitwarden_error(flat)]
pub enum AccountCryptographyInitializationError {
#[error("The encryption type of the user key does not match the account cryptographic state")]
WrongUserKeyType,
#[error("Wrong user key")]
WrongUserKey,
#[error("Decryption succeeded but produced corrupt data")]
CorruptData,
#[error("Signature or mac verification failed, the data may have been tampered with")]
TamperedData,
#[error("Key store is already initialized")]
KeyStoreAlreadyInitialized,
#[error("A generic cryptographic error occurred: {0}")]
GenericCrypto(CryptoError),
}
impl From<CryptoError> for AccountCryptographyInitializationError {
fn from(err: CryptoError) -> Self {
AccountCryptographyInitializationError::GenericCrypto(err)
}
}
#[derive(Debug, Error)]
#[bitwarden_error(flat)]
pub enum RotateCryptographyStateError {
#[error("The provided key is missing from the key store")]
KeyMissing,
#[error("The provided data was invalid")]
InvalidData,
}
#[derive(Debug, Error)]
pub enum AccountKeysResponseParseError {
#[error(transparent)]
MissingField(#[from] MissingFieldError),
#[error("Malformed field value in API response")]
MalformedField,
#[error("Inconsistent account cryptographic state in API response")]
InconsistentState,
}
#[derive(Clone, Serialize, Deserialize, PartialEq)]
#[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 WrappedAccountCryptographicState {
V1 {
private_key: EncString,
},
V2 {
private_key: EncString,
signed_public_key: Option<SignedPublicKey>,
signing_key: EncString,
security_state: SignedSecurityState,
},
}
impl std::fmt::Debug for WrappedAccountCryptographicState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WrappedAccountCryptographicState::V1 { .. } => f
.debug_struct("WrappedAccountCryptographicState::V1")
.finish(),
WrappedAccountCryptographicState::V2 { security_state, .. } => f
.debug_struct("WrappedAccountCryptographicState::V2")
.field("security_state", security_state)
.finish(),
}
}
}
impl TryFrom<&PrivateKeysResponseModel> for WrappedAccountCryptographicState {
type Error = AccountKeysResponseParseError;
fn try_from(response: &PrivateKeysResponseModel) -> Result<Self, Self::Error> {
let private_key: EncString =
require!(&response.public_key_encryption_key_pair.wrapped_private_key)
.parse()
.map_err(|_| AccountKeysResponseParseError::MalformedField)?;
let is_v2_encryption = matches!(private_key, EncString::Cose_Encrypt0_B64 { .. });
if is_v2_encryption {
let signature_key_pair = response
.signature_key_pair
.as_ref()
.ok_or(AccountKeysResponseParseError::InconsistentState)?;
let signing_key: EncString = require!(&signature_key_pair.wrapped_signing_key)
.parse()
.map_err(|_| AccountKeysResponseParseError::MalformedField)?;
let signed_public_key: Option<SignedPublicKey> = response
.public_key_encryption_key_pair
.signed_public_key
.as_ref()
.map(|spk| spk.parse())
.transpose()
.map_err(|_| AccountKeysResponseParseError::MalformedField)?;
let security_state_model = response
.security_state
.as_ref()
.ok_or(AccountKeysResponseParseError::InconsistentState)?;
let security_state: SignedSecurityState =
require!(&security_state_model.security_state)
.parse()
.map_err(|_| AccountKeysResponseParseError::MalformedField)?;
Ok(WrappedAccountCryptographicState::V2 {
private_key,
signed_public_key,
signing_key,
security_state,
})
} else {
if response.signature_key_pair.is_some() || response.security_state.is_some() {
return Err(AccountKeysResponseParseError::InconsistentState);
}
Ok(WrappedAccountCryptographicState::V1 { private_key })
}
}
}
impl WrappedAccountCryptographicState {
pub fn to_wrapped_request_model(
&self,
user_key: &SymmetricKeySlotId,
ctx: &mut KeyStoreContext<KeySlotIds>,
) -> Result<WrappedAccountCryptographicStateRequestModel, AccountCryptographyInitializationError>
{
match self {
WrappedAccountCryptographicState::V1 { .. } => {
Err(AccountCryptographyInitializationError::WrongUserKeyType)
}
WrappedAccountCryptographicState::V2 {
private_key,
signing_key,
security_state,
signed_public_key,
..
} => {
let private_key = private_key.clone();
let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
let public_key = ctx.get_public_key(private_key_tmp_id)?;
let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
Ok(WrappedAccountCryptographicStateRequestModel {
signature_key_pair: Box::new(
bitwarden_api_api::models::SignatureKeyPairRequestModel {
wrapped_signing_key: Some(signing_key.to_string()),
verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
signature_algorithm: Some(verifying_key.algorithm().to_string()),
},
),
public_key_encryption_key_pair: Box::new(
bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
wrapped_private_key: Some(private_key.to_string()),
public_key: Some(B64::from(public_key.to_der()?).to_string()),
signed_public_key: signed_public_key.clone().map(|spk| spk.into()),
},
),
security_state: Box::new(SecurityStateModel {
security_state: Some(security_state.into()),
security_version: security_state
.to_owned()
.verify_and_unwrap(&verifying_key)
.map_err(|_| AccountCryptographyInitializationError::TamperedData)?
.version() as i32,
}),
})
}
}
}
#[instrument(skip_all, err)]
pub fn to_request_model(
&self,
user_key: &SymmetricKeySlotId,
ctx: &mut KeyStoreContext<KeySlotIds>,
) -> Result<AccountKeysRequestModel, AccountCryptographyInitializationError> {
let private_key = match self {
WrappedAccountCryptographicState::V1 { private_key }
| WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(),
};
let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
let public_key = ctx.get_public_key(private_key_tmp_id)?;
let signature_keypair = match self {
WrappedAccountCryptographicState::V1 { .. } => None,
WrappedAccountCryptographicState::V2 { signing_key, .. } => {
let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
Some((signing_key.clone(), verifying_key))
}
};
Ok(AccountKeysRequestModel {
user_key_encrypted_account_private_key: Some(private_key.to_string()),
account_public_key: Some(B64::from(public_key.to_der()?).to_string()),
signature_key_pair: signature_keypair
.as_ref()
.map(|(signing_key, verifying_key)| {
Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel {
wrapped_signing_key: Some(signing_key.to_string()),
verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
signature_algorithm: Some(verifying_key.algorithm().to_string()),
})
}),
public_key_encryption_key_pair: Some(Box::new(
bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
wrapped_private_key: match self {
WrappedAccountCryptographicState::V1 { private_key }
| WrappedAccountCryptographicState::V2 { private_key, .. } => {
Some(private_key.to_string())
}
},
public_key: Some(B64::from(public_key.to_der()?).to_string()),
signed_public_key: match self.signed_public_key() {
Ok(Some(spk)) => Some(spk.clone().into()),
_ => None,
},
},
)),
security_state: match (self, signature_keypair.as_ref()) {
(_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None,
(
WrappedAccountCryptographicState::V2 { security_state, .. },
Some((_, verifying_key)),
) => {
Some(Box::new(SecurityStateModel {
security_state: Some(security_state.into()),
security_version: security_state
.to_owned()
.verify_and_unwrap(verifying_key)
.map_err(|_| AccountCryptographyInitializationError::TamperedData)?
.version() as i32,
}))
}
},
})
}
pub fn make(
ctx: &mut KeyStoreContext<KeySlotIds>,
) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
let signing_key = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
let signed_public_key = ctx.make_signed_public_key(private_key, signing_key)?;
let security_state = SecurityState::new();
let signed_security_state = security_state.sign(signing_key, ctx)?;
Ok((
user_key,
WrappedAccountCryptographicState::V2 {
private_key: ctx.wrap_private_key(user_key, private_key)?,
signed_public_key: Some(signed_public_key),
signing_key: ctx.wrap_signing_key(user_key, signing_key)?,
security_state: signed_security_state,
},
))
}
#[cfg(test)]
fn make_v1(
ctx: &mut KeyStoreContext<KeySlotIds>,
) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
Ok((
user_key,
WrappedAccountCryptographicState::V1 {
private_key: ctx.wrap_private_key(user_key, private_key)?,
},
))
}
#[instrument(skip(self, ctx), err)]
pub fn rotate(
&self,
current_user_key: &SymmetricKeySlotId,
new_user_key: &SymmetricKeySlotId,
ctx: &mut KeyStoreContext<KeySlotIds>,
) -> Result<Self, RotateCryptographyStateError> {
match self {
WrappedAccountCryptographicState::V1 { private_key } => {
let private_key_id = ctx
.unwrap_private_key(*current_user_key, private_key)
.map_err(|_| RotateCryptographyStateError::InvalidData)?;
let new_private_key = ctx
.wrap_private_key(*new_user_key, private_key_id)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
let new_signing_key = ctx
.wrap_signing_key(*new_user_key, signing_key_id)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let signed_public_key = ctx
.make_signed_public_key(private_key_id, signing_key_id)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let security_state = SecurityState::new();
let signed_security_state = security_state
.sign(signing_key_id, ctx)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
Ok(WrappedAccountCryptographicState::V2 {
private_key: new_private_key,
signed_public_key: Some(signed_public_key),
signing_key: new_signing_key,
security_state: signed_security_state,
})
}
WrappedAccountCryptographicState::V2 {
private_key,
signed_public_key,
signing_key,
security_state,
} => {
let private_key_id = ctx
.unwrap_private_key(*current_user_key, private_key)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let new_private_key = ctx
.wrap_private_key(*new_user_key, private_key_id)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let signing_key_id = ctx
.unwrap_signing_key(*current_user_key, signing_key)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
let new_signing_key = ctx
.wrap_signing_key(*new_user_key, signing_key_id)
.map_err(|_| RotateCryptographyStateError::KeyMissing)?;
Ok(WrappedAccountCryptographicState::V2 {
private_key: new_private_key,
signed_public_key: signed_public_key.clone(),
signing_key: new_signing_key,
security_state: security_state.clone(),
})
}
}
}
pub(crate) fn set_to_context(
&self,
security_state_rwlock: &RwLock<Option<SecurityState>>,
user_key: SymmetricKeySlotId,
store: &KeyStore<KeySlotIds>,
mut ctx: KeyStoreContext<KeySlotIds>,
) -> Result<(), AccountCryptographyInitializationError> {
if ctx.has_symmetric_key(SymmetricKeySlotId::User)
|| ctx.has_private_key(PrivateKeySlotId::UserPrivateKey)
|| ctx.has_signing_key(SigningKeySlotId::UserSigningKey)
{
return Err(AccountCryptographyInitializationError::KeyStoreAlreadyInitialized);
}
match self {
WrappedAccountCryptographicState::V1 { private_key } => {
info!(state = ?self, "Initializing V1 account cryptographic state");
if ctx.get_symmetric_key_algorithm(user_key)?
!= SymmetricKeyAlgorithm::Aes256CbcHmac
{
return Err(AccountCryptographyInitializationError::WrongUserKeyType);
}
if let Ok(private_key_id) = ctx.unwrap_private_key(user_key, private_key) {
ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
} else {
tracing::warn!(
"V1 private key could not be unwrapped, skipping setting private key"
);
}
ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
#[cfg(feature = "dangerous-crypto-debug")]
#[allow(deprecated)]
{
let user_key = ctx
.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.expect("User key should be set");
let private_key = ctx
.dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
.ok();
let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
info!(
?user_key,
?private_key,
?public_key,
"V1 account cryptographic state set to context"
);
}
}
WrappedAccountCryptographicState::V2 {
private_key,
signed_public_key,
signing_key,
security_state,
} => {
info!(state = ?self, "Initializing V2 account cryptographic state");
if ctx.get_symmetric_key_algorithm(user_key)?
!= SymmetricKeyAlgorithm::XChaCha20Poly1305
{
return Err(AccountCryptographyInitializationError::WrongUserKeyType);
}
let private_key_id = ctx
.unwrap_private_key(user_key, private_key)
.map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
let signing_key_id = ctx
.unwrap_signing_key(user_key, signing_key)
.map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
if let Some(signed_public_key) = signed_public_key {
signed_public_key
.to_owned()
.verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
.map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
}
let verifying_key = ctx.get_verifying_key(signing_key_id)?;
let security_state: SecurityState = security_state
.to_owned()
.verify_and_unwrap(&verifying_key)
.map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
info!(
security_state_version = security_state.version(),
verifying_key = ?verifying_key,
"V2 account cryptographic state verified"
);
ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
ctx.persist_signing_key(signing_key_id, SigningKeySlotId::UserSigningKey)?;
ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
#[cfg(feature = "dangerous-crypto-debug")]
#[allow(deprecated)]
{
let user_key = ctx
.dangerous_get_symmetric_key(SymmetricKeySlotId::User)
.expect("User key should be set");
let private_key = ctx
.dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
.ok();
let signing_key = ctx
.dangerous_get_signing_key(SigningKeySlotId::UserSigningKey)
.ok();
let verifying_key =
ctx.get_verifying_key(SigningKeySlotId::UserSigningKey).ok();
let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
info!(
?user_key,
?private_key,
?signing_key,
?verifying_key,
?public_key,
?signed_public_key,
?security_state,
"V2 account cryptographic state set to context."
);
}
drop(ctx);
store.set_security_state_version(security_state.version());
*security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
}
}
Ok(())
}
fn signed_public_key(
&self,
) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
match self {
WrappedAccountCryptographicState::V1 { .. } => Ok(None),
WrappedAccountCryptographicState::V2 {
signed_public_key, ..
} => Ok(signed_public_key.as_ref()),
}
}
}
#[cfg(test)]
mod tests {
use std::{str::FromStr, sync::RwLock};
use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
use super::*;
use crate::key_management::{PrivateKeySlotId, SigningKeySlotId, SymmetricKeySlotId};
#[test]
#[ignore = "Manual test to verify debug format"]
fn test_debug() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (_, v1) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
println!("{:?}", v1);
let v1 = format!("{v1:?}");
assert!(!v1.contains("private_key"));
let (_, v2) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
println!("{:?}", v2);
let v2 = format!("{v2:?}");
assert!(!v2.contains("private_key"));
assert!(!v2.contains("signed_public_key"));
assert!(!v2.contains("signing_key"));
}
#[test]
fn test_set_to_context_v1() {
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
let wrapped = WrappedAccountCryptographicState::V1 {
private_key: wrapped_private,
};
#[allow(deprecated)]
let user_key = temp_ctx
.dangerous_get_symmetric_key(user_key)
.unwrap()
.to_owned();
drop(temp_ctx);
drop(temp_store);
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let user_key = ctx.add_local_symmetric_key(user_key);
let security_state = RwLock::new(None);
wrapped
.set_to_context(&security_state, user_key, &store, ctx)
.unwrap();
let ctx = store.context();
assert!(ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
}
#[test]
fn test_set_to_context_v2() {
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
let signing_key_id = temp_ctx.make_signing_key(SignatureAlgorithm::Ed25519);
let signed_public_key = temp_ctx
.make_signed_public_key(private_key_id, signing_key_id)
.unwrap();
let security_state = SecurityState::new();
let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
let wrapped = WrappedAccountCryptographicState::V2 {
private_key: wrapped_private,
signed_public_key: Some(signed_public_key),
signing_key: wrapped_signing,
security_state: signed_security_state,
};
#[allow(deprecated)]
let user_key = temp_ctx
.dangerous_get_symmetric_key(user_key)
.unwrap()
.to_owned();
drop(temp_ctx);
drop(temp_store);
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let user_key = ctx.add_local_symmetric_key(user_key);
let security_state = RwLock::new(None);
wrapped
.set_to_context(&security_state, user_key, &store, ctx)
.unwrap();
assert!(store.context().has_symmetric_key(SymmetricKeySlotId::User));
assert!(
store
.context()
.has_private_key(PrivateKeySlotId::UserPrivateKey)
);
assert!(
store
.context()
.has_signing_key(SigningKeySlotId::UserSigningKey)
);
assert!(security_state.read().unwrap().is_some());
}
#[test]
fn test_to_private_keys_request_model_v2() {
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let (user_key, wrapped_account_cryptography_state) =
WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
wrapped_account_cryptography_state
.set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
.unwrap();
let mut ctx = temp_store.context_mut();
let model = wrapped_account_cryptography_state
.to_request_model(&SymmetricKeySlotId::User, &mut ctx)
.expect("to_private_keys_request_model should succeed");
drop(ctx);
let ctx = temp_store.context();
let sig_pair = model
.signature_key_pair
.expect("signature_key_pair present");
assert_eq!(
sig_pair.verifying_key.unwrap(),
B64::from(
ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
.unwrap()
.to_cose()
)
.to_string()
);
let pk_pair = model.public_key_encryption_key_pair.unwrap();
assert_eq!(
pk_pair.public_key.unwrap(),
B64::from(
ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
.unwrap()
.to_der()
.unwrap()
)
.to_string()
);
let signed_security_state = model
.security_state
.clone()
.expect("security_state present");
let security_state =
SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
.unwrap()
.verify_and_unwrap(
&ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
.unwrap(),
)
.expect("security state should verify");
assert_eq!(
security_state.version(),
model.security_state.unwrap().security_version as u64
);
}
#[test]
fn test_set_to_context_v1_corrupt_private_key() {
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
let corrupt_private_key = "not a private key"
.encrypt(&mut temp_ctx, user_key)
.unwrap();
let wrapped = WrappedAccountCryptographicState::V1 {
private_key: corrupt_private_key,
};
#[expect(deprecated)]
let user_key_material = temp_ctx
.dangerous_get_symmetric_key(user_key)
.unwrap()
.to_owned();
drop(temp_ctx);
drop(temp_store);
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let user_key = ctx.add_local_symmetric_key(user_key_material);
let security_state = RwLock::new(None);
wrapped
.set_to_context(&security_state, user_key, &store, ctx)
.unwrap();
let ctx = store.context();
assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
assert!(!ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
}
#[test]
fn test_try_from_response_v2_roundtrip() {
use bitwarden_api_api::models::{
PublicKeyEncryptionKeyPairResponseModel, SecurityStateModel,
SignatureKeyPairResponseModel,
};
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let (user_key, wrapped_state) =
WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
wrapped_state
.set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
.unwrap();
let mut ctx = temp_store.context_mut();
let request_model = wrapped_state
.to_request_model(&SymmetricKeySlotId::User, &mut ctx)
.unwrap();
drop(ctx);
let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
let sig_pair = request_model.signature_key_pair.unwrap();
let sec_state = request_model.security_state.unwrap();
let response = PrivateKeysResponseModel {
object: None,
public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
object: None,
wrapped_private_key: pk_pair.wrapped_private_key,
public_key: pk_pair.public_key,
signed_public_key: pk_pair.signed_public_key,
}),
signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
object: None,
wrapped_signing_key: sig_pair.wrapped_signing_key,
verifying_key: sig_pair.verifying_key,
})),
security_state: Some(Box::new(SecurityStateModel {
security_state: sec_state.security_state,
security_version: sec_state.security_version,
})),
};
let parsed = WrappedAccountCryptographicState::try_from(&response)
.expect("V2 response should parse successfully");
assert_eq!(parsed, wrapped_state);
}
#[test]
fn test_try_from_response_v1() {
use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let (_user_key, wrapped_state) =
WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
let wrapped_private_key = match &wrapped_state {
WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
_ => panic!("Expected V1"),
};
drop(temp_ctx);
let response = PrivateKeysResponseModel {
object: None,
public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
object: None,
wrapped_private_key: Some(wrapped_private_key),
public_key: None,
signed_public_key: None,
}),
signature_key_pair: None,
security_state: None,
};
let parsed = WrappedAccountCryptographicState::try_from(&response)
.expect("V1 response should parse successfully");
assert_eq!(parsed, wrapped_state);
}
#[test]
fn test_try_from_response_missing_private_key() {
use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
let response = PrivateKeysResponseModel {
object: None,
public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
object: None,
wrapped_private_key: None,
public_key: None,
signed_public_key: None,
}),
signature_key_pair: None,
security_state: None,
};
let result = WrappedAccountCryptographicState::try_from(&response);
assert!(result.is_err());
assert!(
matches!(
result.unwrap_err(),
AccountKeysResponseParseError::MissingField(_)
),
"Should return MissingField error"
);
}
#[test]
fn test_try_from_response_v2_encryption_missing_signature_key_pair() {
use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let (user_key, wrapped_state) =
WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
wrapped_state
.set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
.unwrap();
let mut ctx = temp_store.context_mut();
let request_model = wrapped_state
.to_request_model(&SymmetricKeySlotId::User, &mut ctx)
.unwrap();
drop(ctx);
let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
let response = PrivateKeysResponseModel {
object: None,
public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
object: None,
wrapped_private_key: pk_pair.wrapped_private_key,
public_key: pk_pair.public_key,
signed_public_key: None,
}),
signature_key_pair: None,
security_state: None,
};
let result = WrappedAccountCryptographicState::try_from(&response);
assert!(matches!(
result.unwrap_err(),
AccountKeysResponseParseError::InconsistentState
));
}
#[test]
fn test_try_from_response_v1_encryption_with_unexpected_v2_fields() {
use bitwarden_api_api::models::{
PublicKeyEncryptionKeyPairResponseModel, SignatureKeyPairResponseModel,
};
let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
let mut temp_ctx = temp_store.context_mut();
let (_user_key, wrapped_state) =
WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
let wrapped_private_key = match &wrapped_state {
WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
_ => panic!("Expected V1"),
};
drop(temp_ctx);
let response = PrivateKeysResponseModel {
object: None,
public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
object: None,
wrapped_private_key: Some(wrapped_private_key),
public_key: None,
signed_public_key: None,
}),
signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
object: None,
wrapped_signing_key: Some("bogus".to_string()),
verifying_key: None,
})),
security_state: None,
};
let result = WrappedAccountCryptographicState::try_from(&response);
assert!(matches!(
result.unwrap_err(),
AccountKeysResponseParseError::InconsistentState
));
}
#[test]
fn test_rotate_v1_to_v2() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (old_user_key_id, wrapped_state) =
WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
#[allow(deprecated)]
let new_user_key_owned = ctx
.dangerous_get_symmetric_key(new_user_key_id)
.unwrap()
.to_owned();
wrapped_state
.set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
.unwrap();
let mut ctx = store.context_mut();
let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
let rotated_state = wrapped_state
.rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
.unwrap();
match rotated_state {
WrappedAccountCryptographicState::V2 { .. } => {}
_ => panic!("Expected V2 after rotation from V1"),
}
let store_2 = KeyStore::<KeySlotIds>::default();
let mut ctx_2 = store_2.context_mut();
let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
rotated_state
.set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
.unwrap();
let ctx_2 = store_2.context();
let public_key_before_rotation = ctx
.get_public_key(PrivateKeySlotId::UserPrivateKey)
.expect("Private key should be present in context before rotation");
let public_key_after_rotation = ctx_2
.get_public_key(PrivateKeySlotId::UserPrivateKey)
.expect("Private key should be present in context after rotation");
assert_eq!(
public_key_before_rotation.to_der().unwrap(),
public_key_after_rotation.to_der().unwrap(),
"Private key should be preserved during rotation from V2 to V2"
);
}
#[test]
fn test_rotate_v2() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (old_user_key_id, wrapped_state) =
WrappedAccountCryptographicState::make(&mut ctx).unwrap();
let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
#[allow(deprecated)]
let new_user_key_owned = ctx
.dangerous_get_symmetric_key(new_user_key_id)
.unwrap()
.to_owned();
wrapped_state
.set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
.unwrap();
let mut ctx = store.context_mut();
let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
let rotated_state = wrapped_state
.rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
.unwrap();
match rotated_state {
WrappedAccountCryptographicState::V2 { .. } => {}
_ => panic!("Expected V2 after rotation from V2"),
}
let store_2 = KeyStore::<KeySlotIds>::default();
let mut ctx_2 = store_2.context_mut();
let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
rotated_state
.set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
.unwrap();
let ctx_2 = store_2.context();
let verifying_key_before_rotation = ctx
.get_verifying_key(SigningKeySlotId::UserSigningKey)
.expect("Signing key should be present in context before rotation");
let verifying_key_after_rotation = ctx_2
.get_verifying_key(SigningKeySlotId::UserSigningKey)
.expect("Signing key should be present in context after rotation");
assert_eq!(
verifying_key_before_rotation.to_cose(),
verifying_key_after_rotation.to_cose(),
"Signing key should be preserved during rotation from V2 to V2"
);
let public_key_before_rotation = ctx
.get_public_key(PrivateKeySlotId::UserPrivateKey)
.expect("Private key should be present in context before rotation");
let public_key_after_rotation = ctx_2
.get_public_key(PrivateKeySlotId::UserPrivateKey)
.expect("Private key should be present in context after rotation");
assert_eq!(
public_key_before_rotation.to_der().unwrap(),
public_key_after_rotation.to_der().unwrap(),
"Private key should be preserved during rotation from V2 to V2"
);
}
#[test]
fn test_to_wrapped_request_model_v1_returns_wrong_user_key_type() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (user_key_id, wrapped) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
let result = wrapped.to_wrapped_request_model(&user_key_id, &mut ctx);
assert!(matches!(
result.unwrap_err(),
AccountCryptographyInitializationError::WrongUserKeyType
));
}
#[test]
fn test_to_wrapped_request_model_v2() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
let result = wrapped
.to_wrapped_request_model(&user_key_id, &mut ctx)
.unwrap();
let wrapped_signing_key_str = result
.signature_key_pair
.wrapped_signing_key
.as_ref()
.unwrap();
assert!(!wrapped_signing_key_str.is_empty());
let enc_signing_key: EncString = wrapped_signing_key_str.parse().unwrap();
let signing_key_tmp = ctx
.unwrap_signing_key(user_key_id, &enc_signing_key)
.unwrap();
let verifying_key = ctx.get_verifying_key(signing_key_tmp).unwrap();
let expected = B64::from(verifying_key.to_cose()).to_string();
assert!(
result
.signature_key_pair
.verifying_key
.as_ref()
.is_some_and(|s| s == &expected),
"verifying_key should match expected value"
);
assert_eq!(
result.signature_key_pair.signature_algorithm.as_deref(),
Some("mldsa44")
);
assert!(
result
.public_key_encryption_key_pair
.wrapped_private_key
.as_ref()
.is_some_and(|s| !s.is_empty()),
"wrapped_private_key should be non-empty"
);
let wrapped_private_key_str = result
.public_key_encryption_key_pair
.wrapped_private_key
.as_ref()
.unwrap();
let enc_private_key: EncString = wrapped_private_key_str.parse().unwrap();
let private_key_tmp = ctx
.unwrap_private_key(user_key_id, &enc_private_key)
.unwrap();
let public_key = ctx.get_public_key(private_key_tmp).unwrap();
let expected = B64::from(public_key.to_der().unwrap()).to_string();
assert!(
result
.public_key_encryption_key_pair
.public_key
.as_ref()
.is_some_and(|s| s == &expected),
"public_key should match expected value"
);
assert!(
result
.public_key_encryption_key_pair
.signed_public_key
.is_some(),
"signed_public_key should be present"
);
assert!(
result
.security_state
.security_state
.as_ref()
.is_some_and(|s| !s.is_empty()),
"security_state string should be non-empty"
);
assert!(result.security_state.security_version == 2);
}
#[test]
fn test_to_wrapped_request_model_wrong_user_key_returns_error() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
let mut ctx = store.context_mut();
let (_user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
let wrong_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
let result = wrapped.to_wrapped_request_model(&wrong_user_key_id, &mut ctx);
assert!(result.is_err());
assert!(!matches!(
result.unwrap_err(),
AccountCryptographyInitializationError::WrongUserKeyType
));
}
}