From 7fa725fa4e3fb2365a24389d08fe6a42530730bd Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 3 Jul 2025 14:57:27 +0200 Subject: [PATCH 01/63] Implement security context --- .../bitwarden-core/src/auth/auth_request.rs | 28 ++- .../bitwarden-core/src/auth/login/api_key.rs | 1 + .../src/auth/login/auth_request.rs | 1 + .../bitwarden-core/src/auth/login/password.rs | 1 + .../src/auth/password/validate.rs | 2 + crates/bitwarden-core/src/auth/pin.rs | 1 + crates/bitwarden-core/src/auth/tde.rs | 4 +- .../src/client/encryption_settings.rs | 114 ++++++---- crates/bitwarden-core/src/client/internal.rs | 30 ++- .../src/client/test_accounts.rs | 2 + .../src/key_management/crypto.rs | 192 ++++++++++++++--- .../src/key_management/crypto_client.rs | 16 +- .../src/platform/generate_fingerprint.rs | 1 + crates/bitwarden-core/tests/register.rs | 1 + crates/bitwarden-crypto/src/error.rs | 6 + .../src/keys/signed_public_key.rs | 6 +- crates/bitwarden-crypto/src/lib.rs | 3 + crates/bitwarden-crypto/src/security_state.rs | 200 ++++++++++++++++++ .../bitwarden-crypto/src/signing/namespace.rs | 4 + crates/bitwarden-crypto/src/store/context.rs | 123 ++++++++++- crates/bitwarden-crypto/src/uniffi_support.rs | 13 +- .../bitwarden/myapplication/MainActivity.kt | 3 + .../swift/iOS/App/ContentView.swift | 3 + .../src/pure_crypto.rs | 5 +- 24 files changed, 656 insertions(+), 104 deletions(-) create mode 100644 crates/bitwarden-crypto/src/security_state.rs diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 3020222f8..0e9a50c03 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -164,7 +164,7 @@ mod tests { 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_key(master_key, user_key, private_key, None) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None, None) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; @@ -175,7 +175,7 @@ mod tests { let fingerprint = fingerprint("test@bitwarden.com", &pubkey).unwrap(); assert_eq!(fingerprint, "childless-unfair-prowler-dropbox-designate"); - approve_auth_request(&client, public_key.to_owned()).unwrap(); + approve_auth_request(&client, public_key.to_owned().into()).unwrap(); } #[tokio::test] @@ -183,7 +183,8 @@ mod tests { let private_key = "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="; let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); - let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); + let dec = + auth_request_decrypt_user_key(private_key.to_owned().into(), enc_user_key).unwrap(); assert_eq!( &dec.to_encoded().to_vec(), @@ -200,9 +201,12 @@ mod tests { let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); - let dec = - auth_request_decrypt_master_key(private_key.to_owned(), enc_master_key, enc_user_key) - .unwrap(); + let dec = auth_request_decrypt_master_key( + private_key.to_owned().into(), + enc_master_key, + enc_user_key, + ) + .unwrap(); assert_eq!( &dec.to_encoded().to_vec(), @@ -232,7 +236,13 @@ mod tests { existing_device .internal - .initialize_user_crypto_master_key(master_key, user_key, private_key.clone(), None) + .initialize_user_crypto_master_key( + master_key, + user_key, + private_key.clone(), + None, + None, + ) .unwrap(); // Initialize a new device which will request to be logged in @@ -240,7 +250,8 @@ mod tests { // Initialize an auth request, and approve it on the existing device let auth_req = new_auth_request(email).unwrap(); - let approved_req = approve_auth_request(&existing_device, auth_req.public_key).unwrap(); + let approved_req = + approve_auth_request(&existing_device, auth_req.public_key.into()).unwrap(); // Unlock the vault using the approved request new_device @@ -251,6 +262,7 @@ mod tests { email: email.to_owned(), private_key, signing_key: None, + security_state: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method: AuthRequestMethod::UserKey { diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 782a6df34..c6d4bae73 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -56,6 +56,7 @@ pub(crate) async fn login_api_key( user_key, private_key, None, + None, )?; } diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 363086fb1..52d20b6a7 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -121,6 +121,7 @@ pub(crate) async fn complete_auth_request( email: auth_req.email, private_key: require!(r.private_key).parse()?, signing_key: None, + security_state: None, method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, method, diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 76656d91b..450564c2e 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -55,6 +55,7 @@ pub(crate) async fn login_password( user_key, private_key, None, + None, )?; } diff --git a/crates/bitwarden-core/src/auth/password/validate.rs b/crates/bitwarden-core/src/auth/password/validate.rs index 229c7fde7..c10906fa4 100644 --- a/crates/bitwarden-core/src/auth/password/validate.rs +++ b/crates/bitwarden-core/src/auth/password/validate.rs @@ -145,6 +145,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); @@ -193,6 +194,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/pin.rs b/crates/bitwarden-core/src/auth/pin.rs index c337f9327..91ae64c9b 100644 --- a/crates/bitwarden-core/src/auth/pin.rs +++ b/crates/bitwarden-core/src/auth/pin.rs @@ -80,6 +80,7 @@ mod tests { user_key.parse().unwrap(), private_key, None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 9b8c5db9c..c276b95b4 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,9 +1,10 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; +use base64::{engine::general_purpose::STANDARD, Engine}; + use crate::{client::encryption_settings::EncryptionSettingsError, Client}; /// This function generates a new user key and key pair, initializes the client's crypto with the @@ -45,6 +46,7 @@ pub(super) fn make_register_tde_keys( // Note: Signing keys are not supported on registration yet. This needs to be changed as // soon as registration is supported. None, + None, )?; Ok(RegisterTdeKeyResponse { diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 3610f0054..ae2e62411 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,5 +1,6 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, UnsignedSharedKey}; +use bitwarden_crypto::{security_state::SignedSecurityState, EncString, UnsignedSharedKey}; +use bitwarden_crypto::{CryptoError, Pkcs8PrivateKeyBytes}; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; @@ -30,6 +31,9 @@ pub enum EncryptionSettingsError { #[error("Invalid signing key")] InvalidSigningKey, + #[error("Invalid security state")] + InvalidSecurityState, + #[error(transparent)] MissingPrivateKey(#[from] MissingPrivateKeyError), @@ -50,49 +54,85 @@ impl EncryptionSettings { user_key: SymmetricCryptoKey, private_key: EncString, signing_key: Option, + security_state: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{AsymmetricCryptoKey, CoseSerializable, KeyDecryptable, SigningKey}; + use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable}; use log::warn; - use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; - - let private_key = { - let dec: Vec = private_key.decrypt_with_key(&user_key)?; - // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a recovery - // process in place. - AsymmetricCryptoKey::from_der(&dec.into()) - .map_err(|_| { - warn!("Invalid private key"); - }) - .ok() - - // Some( - // AsymmetricCryptoKey::from_der(&dec) - // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, - // ) - }; - let signing_key = signing_key - .map(|key| { - use bitwarden_crypto::CryptoError; - - let dec: Vec = key.decrypt_with_key(&user_key)?; - SigningKey::from_cose(&dec.into()).map_err(Into::::into) - }) - .transpose()?; - - // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods - #[allow(deprecated)] - { - let mut ctx = store.context_mut(); - ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; - if let Some(private_key) = private_key { + use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; + + // This is an all-or-nothing check. The server cannot pretend a signing key or security + // state to be missing, because they are *always* present when the user key is an + // XChaCha20Poly1305Key. Thus, the server or network cannot lie about the presence of these, + // because otherwise the entire user account will fail to decrypt. + let is_v2_user = matches!(user_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)); + if is_v2_user { + // For v2 users, we mandate the signing key and security state to be present + // The private key must also be valid. + + use bitwarden_crypto::{ + security_state::SecurityState, CoseKeyBytes, CoseSerializable, SigningKey, + }; + + // Both of these are required for v2 users + let signing_key = signing_key.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade("Signing key is required for v2 users".to_string()), + ))?; + let security_state = security_state.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade( + "Security state is required for v2 users".to_string(), + ), + ))?; + + // Everything MUST decrypt. + let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; + let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(signing_key)) + .map_err(|_| EncryptionSettingsError::InvalidSigningKey)?; + let private_key: Vec = private_key.decrypt_with_key(&user_key)?; + let private_key = + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key)) + .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; + let _security_state: SecurityState = security_state + .verify_and_unwrap(&signing_key.to_verifying_key()) + .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; + + #[allow(deprecated)] + { + use crate::key_management::SigningKeyId; + + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; - } - - if let Some(signing_key) = signing_key { ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; } + } else { + let private_key = { + let dec: Vec = private_key.decrypt_with_key(&user_key)?; + + // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a + // recovery process in place. + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(dec)) + .map_err(|_| { + warn!("Invalid private key"); + }) + .ok() + + // Some( + // AsymmetricCryptoKey::from_der(&dec) + // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, + // ) + }; + + // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods + #[allow(deprecated)] + { + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; + if let Some(private_key) = private_key { + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; + } + } } Ok(()) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 5c0e085b5..42d7ccac6 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -4,7 +4,9 @@ use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey}; +use bitwarden_crypto::{ + security_state::SignedSecurityState, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey, +}; #[cfg(feature = "internal")] use bitwarden_state::registry::StateRegistry; use chrono::Utc; @@ -217,9 +219,16 @@ impl InternalClient { user_key: EncString, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { let user_key = master_key.decrypt_user_key(user_key)?; - EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key( + user_key, + private_key, + signing_key, + security_state, + &self.key_store, + )?; Ok(()) } @@ -230,8 +239,15 @@ impl InternalClient { user_key: SymmetricCryptoKey, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { - EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; + EncryptionSettings::new_decrypted_key( + user_key, + private_key, + signing_key, + security_state, + &self.key_store, + )?; Ok(()) } @@ -243,9 +259,15 @@ impl InternalClient { pin_protected_user_key: EncString, private_key: EncString, signing_key: Option, + security_state: Option, ) -> Result<(), EncryptionSettingsError> { let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?; - self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key, signing_key) + self.initialize_user_crypto_decrypted_key( + decrypted_user_key, + private_key, + signing_key, + security_state, + ) } #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-core/src/client/test_accounts.rs b/crates/bitwarden-core/src/client/test_accounts.rs index 48a5cdba9..07e96cecd 100644 --- a/crates/bitwarden-core/src/client/test_accounts.rs +++ b/crates/bitwarden-core/src/client/test_accounts.rs @@ -126,6 +126,7 @@ pub fn test_bitwarden_com_account() -> TestAccount { 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().to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), @@ -185,6 +186,7 @@ pub fn test_legacy_user_key_account() -> TestAccount { email: "legacy@bitwarden.com".to_owned(), private_key: "2.leBIE5u0aQUeXi++JzAnrA==|P8x+hs00RJx7epw+49qVtBhLJxE/JTL5dEHg6kq5pbZLdUY8ZvWK49v0EqgHbv1r298N9+msoO9hmdSIVIAZyycemYDSoc1rX4S1KpS/ZMA/Vd3VLFb+o13Ts62GFQ5ygHKgQZfzjU6jO5P/B/0igzFoxyJDomhW5NBC1P9+e/5qNRZN8loKvAaWc/7XtpRayPQqWx+AgYc2ntb1GF5hRVrW4M47bG5ZKllbJWtQKg2sXIy2lDBbKLRFWF4RFzNVcXQGMoPdWLY0f3uTwUH01dyGmFFMbOvfBEuYqmZyPdd93ve8zuFOEqkj46Ulpq2CVG8NvZARTwsdKl6XB0wGuHFoTsDJT2SJGl67pBBKsVRGxy059QW+9hAIB+emIV0T/7+0rvdeSXZ4AbG+oXGEXFTkHefwJKfeT0MBTAjYKr7ZRLgqvf7n39+nCEJU4l22kp8FmjcWIU7AgNipdGHC+UT2yfOcYlvgBgWDcMXcbVDMyus9105RgcW6PHozUj7yjbohI/A3XWmAFufP6BSnmEFCKoik78X/ry09xwiH2rN4KVXe/k9LpRNB2QBGIVsfgCrkxjeE8r0nA59Rvwrhny1z5BkvMW/N1KrGuafg/IYgegx72gJNuZPZlFu1Vs7HxySHmzYvm3DPV7bzCaAxxNtvZmQquNIEnsDQfjJO76iL1JCtDqNJVzGLHTMTr7S5hkOcydcH3kfKwZdA1ULVd2qu0SwOUEP/ECjU/cS5INy6WPYzNMAe/g2DISpQjNwBb5K17PIiGOR7/Q/A6E8pVnkHiAXuUFr9aLOYN9BWSu5Z+BPHH65na2FDmssix5WV09I2sUBfvdNCjkrUGdYgo8E+vOTn35x9GJHF45uhmgC1yAn/+/RSpORlrSVJ7NNP11dn3htUpSsIy/b7ituAu8Ry5mhicFU8CXJL4NeMlXThUt8P++wxs4wMkBvJ8J9NJAVKbAOA2o+GOdjbh6Ww3IRegkurWh4oL/dFSx0LpaXJuw6HFT/LzticPlSwHtUP11hZ81seMsXmkSZd8IugRFfwpPl7N6PVRWDOKxLf4gPqcnJ11TvfasXy1uolV2vZCPbrbbVzQMPdVwL/OzwfhqsIgQZI8rsDMK5D2EX8MaT8MDfGcsYcVTL9PmuZYLpOUnnHX0A1opAAa9iPw3d+eWB/GAyLvKPnMTUqVNos8HcCktXckCshihA8QuBJOwg3m0j2LPSZ5Jvf8gbXauBmt9I4IlJq0xfpgquYY1WNnO8IcWE4N9W+ASvOr9gnduA6CkDeAlyMUFmdpkeCjGMcsV741bTCPApSQlL3/TOT1cjK3iejWpz0OaVHXyg02hW2fNkOfYfr81GvnLvlHxIg4Prw89gKuWU+kQk82lFQo6QQpqbCbJC2FleurD8tYoSY0srhuioVInffvTxw2NMF7FQEqUcsK9AMKSEiDqzBi35Um/fiE3JL4XZBFw8Xzl7X3ab5nlg8X+xD5uSZY+oxD3sDVXjLaQ5JUoys+MCm0FkUj85l0zT6rvM4QLhU1RDK1U51T9HJhh8hsFJsqL4abRzwEWG7PSi859zN4UsgyuQfmBJv/n7QAFCbrJhVBlGB1TKLZRzvgmKoxTYTG3cJFkjetLcUTwrwC9naxAQRfF4=|ufHf73IzJ707dx44w4fjkuD7tDa50OwmmkxcypAT9uQ=".parse::().unwrap().to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".to_owned(), user_key: "0.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI=".parse().unwrap(), diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 83c98eb81..d1f76e395 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,10 +8,11 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, - EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, - PrimitiveEncryptable, RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey, - SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + dangerous_get_v2_rotated_account_keys, security_state::SignedSecurityState, + AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, + KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, RotatedUserKeys, SignatureAlgorithm, + SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, + UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -54,6 +55,8 @@ pub struct InitUserCryptoRequest { pub private_key: EncString, /// The user's signing key pub signing_key: Option, + /// The user's security state + pub security_state: Option, /// The initialization method to use pub method: InitUserCryptoMethod, } @@ -150,6 +153,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { @@ -158,6 +162,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::Pin { @@ -170,6 +175,7 @@ pub(super) async fn initialize_user_crypto( pin_protected_user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::AuthRequest { @@ -178,13 +184,13 @@ pub(super) async fn initialize_user_crypto( } => { let user_key = match method { AuthRequestMethod::UserKey { protected_user_key } => { - auth_request_decrypt_user_key(request_private_key, protected_user_key)? + auth_request_decrypt_user_key(request_private_key.into(), protected_user_key)? } AuthRequestMethod::MasterKey { protected_master_key, auth_request_key, } => auth_request_decrypt_master_key( - request_private_key, + request_private_key.into(), protected_master_key, auth_request_key, )?, @@ -193,6 +199,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::DeviceKey { @@ -208,6 +215,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } InitUserCryptoMethod::KeyConnector { @@ -224,6 +232,7 @@ pub(super) async fn initialize_user_crypto( user_key, req.private_key, req.signing_key, + req.security_state, )?; } } @@ -574,45 +583,97 @@ pub(super) fn verify_asymmetric_keys( }) } -/// A new signing key pair along with the signed public key +/// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user #[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 MakeUserSigningKeysResponse { - /// Base64 encoded verifying key - verifying_key: String, - /// Signing key, encrypted with the user's symmetric key - signing_key: EncString, +pub struct EnrollUserCryptoV2Response { + /// User key + user_key: String, + + /// Wrapped private key + private_key: EncString, + /// Public key + public_key: String, /// 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: String, + + /// The user's signed security state + security_state: SignedSecurityState, + /// The security state's version + security_version: u64, } -/// Makes a new set of signing keys for a user, which should only be done during -/// once. This also signs the public key with the signing key -/// and returns the signed public key. -pub fn make_user_signing_keys_for_enrollment( +/// Initializes the user's cryptographic state for v2 users. +/// If the client already contains a v1 user, then this user's private-key will be +/// re-used. +pub(crate) fn make_keys_for_user_crypto_v2( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); - // Make new keypair and sign the public key with it - let signature_keypair = SigningKey::make(SignatureAlgorithm::Ed25519); - let temporary_signature_keypair_id = SigningKeyId::Local("temporary_key_for_rotation"); + let temporary_user_key_id = SymmetricKeyId::Local("temporary_user_key"); + let temporary_signing_key_id = SigningKeyId::Local("temporary_signing_key"); + let temporary_private_key_id = AsymmetricKeyId::Local("temporary_private_key"); + + // If the user already has a private key, use it. Otherwise, create a temporary one. + let private_key_id = if ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { + AsymmetricKeyId::UserPrivateKey + } else { + ctx.make_asymmetric_key(temporary_private_key_id)? + }; + #[allow(deprecated)] + let private_key = ctx.dangerous_get_asymmetric_key(private_key_id)?.clone(); + + // New user key + let user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); #[allow(deprecated)] - ctx.set_signing_key(temporary_signature_keypair_id, signature_keypair.clone())?; - let signed_public_key = ctx.make_signed_public_key( - AsymmetricKeyId::UserPrivateKey, - temporary_signature_keypair_id, + ctx.set_symmetric_key(temporary_user_key_id, user_key.clone())?; + + // New signing key + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + #[allow(deprecated)] + ctx.set_signing_key(temporary_signing_key_id, signing_key.clone())?; + + // Sign existing public key + let signed_public_key = ctx.make_signed_public_key(private_key_id, temporary_signing_key_id)?; + #[allow(deprecated)] + let public_key = ctx + .dangerous_get_asymmetric_key(private_key_id)? + .to_public_key(); + + // Initialize security state for the user + let security_state = bitwarden_crypto::security_state::SecurityState::initialize_for_user( + client + .internal + .get_user_id() + .ok_or(CryptoError::UninitializedError)?, + ); + let signed_security_state = bitwarden_crypto::security_state::sign( + &security_state, + temporary_signing_key_id, + &mut ctx, )?; - Ok(MakeUserSigningKeysResponse { - verifying_key: STANDARD.encode(signature_keypair.to_verifying_key().to_cose()), - signing_key: signature_keypair - .to_cose() - .encrypt(&mut ctx, SymmetricKeyId::User)?, + Ok(EnrollUserCryptoV2Response { + user_key: user_key.to_base64(), + + private_key: private_key.to_der()?.encrypt_with_key(&user_key)?, + public_key: STANDARD.encode(public_key.to_der()?), signed_public_key, + + signing_key: signing_key.to_cose().encrypt_with_key(&user_key)?, + verifying_key: STANDARD.encode(signing_key.to_verifying_key().to_cose()), + + security_state: signed_security_state, + security_version: security_state.version(), }) } @@ -693,6 +754,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(), @@ -714,6 +776,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "123412341234".into(), user_key: new_password_response.new_key, @@ -773,6 +836,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: "asdfasdfasdf".into(), user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(), @@ -796,6 +860,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key: pin_key.pin_protected_user_key, @@ -840,6 +905,7 @@ mod tests { email: "test@bitwarden.com".into(), private_key: priv_key.to_owned(), signing_key: None, + security_state: None, method: InitUserCryptoMethod::Pin { pin: "1234".into(), pin_protected_user_key, @@ -870,10 +936,70 @@ mod tests { assert_eq!(client_key, client3_key); } + #[tokio::test] + async fn test_user_crypto_v2() { + let client = Client::new(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(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: priv_key, + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey.clone(), + }, + }, + ) + .await + .unwrap(); + + let master_key = MasterKey::derive( + "asdfasdfasdf", + "test@bitwarden.com", + &Kdf::PBKDF2 { + iterations: NonZeroU32::new(100_000).unwrap(), + }, + ) + .unwrap(); + let enrollment_response = make_keys_for_user_crypto_v2(&client).unwrap(); + let encrypted_userkey_v2 = master_key + .encrypt_user_key(&SymmetricCryptoKey::try_from(enrollment_response.user_key).unwrap()) + .unwrap(); + + let client2 = Client::new(None); + initialize_user_crypto( + &client2, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: enrollment_response.private_key, + signing_key: Some(enrollment_response.signing_key), + security_state: Some(enrollment_response.security_state), + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey_v2, + }, + }, + ) + .await + .unwrap(); + } + #[test] fn test_enroll_admin_password_reset() { - use base64::{engine::general_purpose::STANDARD, Engine}; - let client = Client::new(None); let master_key = MasterKey::derive( @@ -889,12 +1015,12 @@ mod tests { 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_key(master_key, user_key, private_key, None) + .initialize_user_crypto_master_key(master_key, user_key, private_key, None, None) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; - let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); + let encrypted = enroll_admin_password_reset(&client, public_key.to_owned().into()).unwrap(); let private_key = "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="; let private_key = STANDARD.decode(private_key).unwrap(); diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 5e88f3326..aecb6952e 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -5,9 +5,8 @@ use bitwarden_crypto::{EncString, UnsignedSharedKey}; use wasm_bindgen::prelude::*; use super::crypto::{ - derive_key_connector, make_key_pair, make_user_signing_keys_for_enrollment, - verify_asymmetric_keys, DeriveKeyConnectorError, DeriveKeyConnectorRequest, - EnrollAdminPasswordResetError, MakeKeyPairResponse, MakeUserSigningKeysResponse, + derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorError, + DeriveKeyConnectorRequest, EnrollAdminPasswordResetError, MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse, }; #[cfg(feature = "internal")] @@ -19,7 +18,8 @@ use crate::key_management::crypto::{ use crate::{ client::encryption_settings::EncryptionSettingsError, key_management::crypto::{ - get_v2_rotated_account_keys, CryptoClientError, RotateUserKeysResponse, + get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, + EnrollUserCryptoV2Response, RotateUserKeysResponse, }, Client, }; @@ -67,10 +67,8 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_user_signing_keys_for_enrollment( - &self, - ) -> Result { - make_user_signing_keys_for_enrollment(&self.client) + pub fn make_keys_for_user_crypto_v2(&self) -> Result { + make_keys_for_user_crypto_v2(&self.client) } /// Creates a rotated set of account keys for the current state @@ -120,7 +118,7 @@ impl CryptoClient { &self, public_key: String, ) -> Result { - enroll_admin_password_reset(&self.client, public_key) + enroll_admin_password_reset(&self.client, public_key.into()) } /// Derive the master key for migrating to the key connector diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index e6ff4cbf8..e469d8a9a 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -107,6 +107,7 @@ mod tests { user_key.parse().unwrap(), private_key.parse().unwrap(), None, + None, ) .unwrap(); diff --git a/crates/bitwarden-core/tests/register.rs b/crates/bitwarden-core/tests/register.rs index 2b93f8227..af4abd94a 100644 --- a/crates/bitwarden-core/tests/register.rs +++ b/crates/bitwarden-core/tests/register.rs @@ -34,6 +34,7 @@ async fn test_register_initialize_crypto() { email: email.to_owned(), private_key: register_response.keys.private, signing_key: None, + security_state: None, method: InitUserCryptoMethod::Password { password: password.to_owned(), user_key: register_response.encrypted_user_key, diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 544f95ce8..3eb6e0708 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -69,6 +69,12 @@ pub enum CryptoError { #[error("Encoding error, {0}")] EncodingError(#[from] EncodingError), + + #[error("Uninitialized error")] + UninitializedError, + + #[error("Attempted security downgrade {0}")] + SecurityDowngrade(String), } #[derive(Debug, Error)] diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 5951ccf0d..0b71ab64c 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -86,9 +86,7 @@ impl From for CoseSign1Bytes { impl TryFrom for SignedPublicKey { type Error = EncodingError; fn try_from(bytes: CoseSign1Bytes) -> Result { - Ok(SignedPublicKey(SignedObject::from_cose( - &CoseSign1Bytes::from(bytes), - )?)) + Ok(SignedPublicKey(SignedObject::from_cose(&bytes)?)) } } @@ -115,7 +113,7 @@ impl SignedPublicKey { ) { (PublicKeyEncryptionAlgorithm::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( - public_key_message.public_key.to_vec(), + public_key_message.public_key.into_vec(), )) .map_err(|_| EncodingError::InvalidValue("public key"))?, ), diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index d4cc15f45..f84993b22 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -36,6 +36,9 @@ pub use store::{ }; mod cose; pub use cose::CoseSerializable; +/// The `SecurityState` module provides functionality to cryptographically attest to which features +/// are allowed to be used for a user. +pub mod security_state; mod signing; pub use signing::*; mod traits; diff --git a/crates/bitwarden-crypto/src/security_state.rs b/crates/bitwarden-crypto/src/security_state.rs new file mode 100644 index 000000000..f158d2c5f --- /dev/null +++ b/crates/bitwarden-crypto/src/security_state.rs @@ -0,0 +1,200 @@ +//! Security state is a signed object that attests to a user's (or later an organization's) security +//! state. The security goal is to prevent downgrades of specific features within the user's account +//! by the server / a networked attacker with TLS introspection access. +//! +//! A security state contains a security version. Based on this version, features can be disabled. +//! Since the server cannot sign a security state, it can no longer downgrade the feature, because +//! it cannot produce an arbitrary valid signed security state. +//! +//! Note: A long-term compromised server can record the security state of a user, and then replay +//! this specific state, or the entire account to downgrade users to previous states. This can be +//! prevented per logged in session by the client, and for bootstrapping a client by +//! using an extended login-with-device protocol. +//! +//! To utilize the security state to disable a feature the following steps are taken: +//! 1. Assume: Feature with format version A is insecure, and cannot be changed by simple mutation +//! 2. A new, safe format version B is introduced, and an upgrade path created +//! 3. The upgrade path is made mandatory +//! 4. After upgrades are run, the sdk validates that all items are in format version B, and the +//! security state can be updated to contain the security version N+1 +//! 5. The client, given a security state with security version N+1 will reject all items that are +//! in format version A. + +use std::str::FromStr; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +use crate::{ + cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CoseSign1Bytes, + CryptoError, KeyIds, KeyStoreContext, SignedObject, SigningNamespace, VerifyingKey, +}; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_CUSTOM_TYPES: &'static str = r#" +export type SignedSecurityState = string; +"#; + +/// The security state is a signed object attesting to the security state of a user. +/// +/// It contains a version, which can only ever increment. Based on the version, old formats and +/// features are blocked. This prevents a server from downgrading a user's account features, because +/// only the user can create this signed object. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SecurityState { + /// The entity ID is a permanent, unchangeable, unique identifier for the object this security + /// state applies to. For users, this is the user ID, which never changes. + entity_id: ByteBuf, + /// The version of the security state gates feature availability. It can only ever be + /// incremented. Components can use it to gate format support of specific formats (like + /// item url hashes). + version: u64, +} + +impl SecurityState { + /// Initialize a new `SecurityState` for the given user ID, to the lowest version possible. + /// The user needs to be a v2 encryption user. + pub fn initialize_for_user(user_id: uuid::Uuid) -> Self { + SecurityState { + entity_id: user_id.as_bytes().to_vec().into(), + version: 2, + } + } + + /// Returns the version of the security state + pub fn version(&self) -> u64 { + self.version + } +} + +/// Signs the `SecurityState` with the provided signing key ID from the context. +pub fn sign( + security_state: &SecurityState, + signing_key_id: Ids::Signing, + ctx: &mut KeyStoreContext, +) -> Result { + Ok(SignedSecurityState(ctx.sign( + signing_key_id, + security_state, + &SigningNamespace::SecurityState, + )?)) +} + +impl SignedSecurityState { + /// Verifies the signature of the `SignedSecurityState` using the provided `VerifyingKey`. + pub fn verify_and_unwrap( + self, + verifying_key: &VerifyingKey, + ) -> Result { + self.0 + .verify_and_unwrap(verifying_key, &SigningNamespace::SecurityState) + } +} + +/// A signed and serialized `SecurityState` object. +#[derive(Clone, Debug)] +pub struct SignedSecurityState(pub(crate) SignedObject); + +impl From for CoseSign1Bytes { + fn from(val: SignedSecurityState) -> Self { + val.0.to_cose() + } +} + +impl TryFrom<&CoseSign1Bytes> for SignedSecurityState { + type Error = EncodingError; + fn try_from(bytes: &CoseSign1Bytes) -> Result { + Ok(SignedSecurityState(SignedObject::from_cose(bytes)?)) + } +} + +impl From for String { + fn from(val: SignedSecurityState) -> Self { + let bytes: CoseSign1Bytes = val.into(); + STANDARD.encode(&bytes) + } +} + +impl FromStr for SignedSecurityState { + type Err = EncodingError; + + fn from_str(s: &str) -> Result { + let bytes = STANDARD + .decode(s) + .map_err(|_| EncodingError::InvalidCborSerialization)?; + Self::try_from(&CoseSign1Bytes::from(bytes)) + } +} + +impl<'de> Deserialize<'de> for SignedSecurityState { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(FromStrVisitor::new()) + } +} + +impl serde::Serialize for SignedSecurityState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let b64_serialized_signed_public_key: String = self.clone().into(); + serializer.serialize_str(&b64_serialized_signed_public_key) + } +} + +impl schemars::JsonSchema for SignedSecurityState { + fn schema_name() -> String { + "SecurityState".to_string() + } + + fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + generator.subschema_for::() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::tests::{TestIds, TestSigningKey}, + KeyStore, SignatureAlgorithm, SigningKey, + }; + + #[test] + fn test_security_state_signing() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + + let user_id = uuid::Uuid::new_v4(); + let security_state = SecurityState::initialize_for_user(user_id); + let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); + #[allow(deprecated)] + ctx.set_signing_key(TestSigningKey::B, signing_key.clone()) + .unwrap(); + let signed_security_state = + crate::security_state::sign(&security_state, TestSigningKey::B, &mut ctx).unwrap(); + + let verifying_key = signing_key.to_verifying_key(); + let verified_security_state = signed_security_state + .verify_and_unwrap(&verifying_key) + .unwrap(); + + assert_eq!( + uuid::Uuid::from_bytes( + verified_security_state + .entity_id + .as_ref() + .try_into() + .unwrap() + ), + user_id + ); + assert_eq!(verified_security_state.version(), 2); + } +} diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index fd48bd2b9..7f5b0c2bb 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -12,6 +12,9 @@ pub enum SigningNamespace { /// The namespace for /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, + /// The namespace for + /// [`SignedSecurityState`](crate::security_state::SignedSecurityState). + SecurityState = 2, /// This namespace is only used in tests #[cfg(test)] ExampleNamespace = -1, @@ -33,6 +36,7 @@ impl TryFrom for SigningNamespace { fn try_from(value: i64) -> Result { match value { 1 => Ok(SigningNamespace::SignedPublicKey), + 2 => Ok(SigningNamespace::SecurityState), #[cfg(test)] -1 => Ok(SigningNamespace::ExampleNamespace), #[cfg(test)] diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 2df08a1f8..5e2e42825 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -10,8 +10,9 @@ use super::KeyStoreInner; use crate::{ derive_shareable_key, error::UnsupportedOperation, signing, store::backend::StoreBackend, AsymmetricCryptoKey, BitwardenLegacyKeyBytes, ContentFormat, CryptoError, EncString, KeyId, - KeyIds, Result, Signature, SignatureAlgorithm, SignedObject, SignedPublicKey, - SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, + KeyIds, PublicKeyEncryptionAlgorithm, Result, RotatedUserKeys, Signature, SignatureAlgorithm, + SignedObject, SignedPublicKey, SignedPublicKeyMessage, SigningKey, SymmetricCryptoKey, + UnsignedSharedKey, }; /// The context of a crypto operation using [super::KeyStore] @@ -308,6 +309,15 @@ impl KeyStoreContext<'_, Ids> { Ok(key_id) } + /// Makes a new asymmetric encryption key using the current default algorithm, and stores it in + /// the context + pub fn make_asymmetric_key(&mut self, key_id: Ids::Asymmetric) -> Result { + let key = AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1); + #[allow(deprecated)] + self.set_asymmetric_key(key_id, key)?; + Ok(key_id) + } + /// Generate a new signature key using the current default algorithm, and store it in the /// context pub fn make_signing_key(&mut self, key_id: Ids::Signing) -> Result { @@ -515,6 +525,21 @@ impl KeyStoreContext<'_, Ids> { ) -> Result<(Signature, signing::SerializedMessage)> { self.get_signing_key(key)?.sign_detached(message, namespace) } + + /// Re-encrypts the user's keys with the provided symmetric key for a v2 user. + pub fn dangerous_get_v2_rotated_account_keys( + &self, + new_user_key: &SymmetricCryptoKey, + current_user_private_key_id: Ids::Asymmetric, + current_user_signing_key_id: Ids::Signing, + ) -> Result { + crate::dangerous_get_v2_rotated_account_keys( + new_user_key, + current_user_private_key_id, + current_user_signing_key_id, + self, + ) + } } #[cfg(test)] @@ -527,8 +552,10 @@ mod tests { tests::{Data, DataView}, KeyStore, }, - traits::tests::{TestIds, TestSigningKey, TestSymmKey}, - CompositeEncryptable, CryptoError, Decryptable, SignatureAlgorithm, SigningKey, + traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CompositeEncryptable, CoseKeyBytes, + CoseSerializable, CryptoError, Decryptable, KeyDecryptable, Pkcs8PrivateKeyBytes, + PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SigningKey, SigningNamespace, SymmetricCryptoKey, }; @@ -719,4 +746,92 @@ mod tests { &SigningNamespace::ExampleNamespace )) } + + #[test] + fn test_account_key_rotation() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + + // Generate a new user key + let new_user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); + let current_user_private_key_id = TestAsymmKey::A(0); + let current_user_signing_key_id = TestSigningKey::A(0); + + // Make the keys + ctx.generate_symmetric_key(TestSymmKey::A(0)).unwrap(); + ctx.make_signing_key(current_user_signing_key_id).unwrap(); + ctx.set_asymmetric_key( + current_user_private_key_id, + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1), + ) + .unwrap(); + + // Get the rotated account keys + let rotated_keys = ctx + .dangerous_get_v2_rotated_account_keys( + &new_user_key, + current_user_private_key_id, + current_user_signing_key_id, + ) + .unwrap(); + + // Public/Private key + assert_eq!( + AsymmetricPublicCryptoKey::from_der(&rotated_keys.public_key) + .unwrap() + .to_der() + .unwrap(), + ctx.get_asymmetric_key(current_user_private_key_id) + .unwrap() + .to_public_key() + .to_der() + .unwrap() + ); + let decrypted_private_key: Vec = rotated_keys + .private_key + .decrypt_with_key(&new_user_key) + .unwrap(); + let private_key = + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key)) + .unwrap(); + assert_eq!( + private_key.to_der().unwrap(), + ctx.get_asymmetric_key(current_user_private_key_id) + .unwrap() + .to_der() + .unwrap() + ); + + // Signing Key + let decrypted_signing_key: Vec = rotated_keys + .signing_key + .decrypt_with_key(&new_user_key) + .unwrap(); + let signing_key = + SigningKey::from_cose(&CoseKeyBytes::from(decrypted_signing_key)).unwrap(); + assert_eq!( + signing_key.to_cose(), + ctx.get_signing_key(current_user_signing_key_id) + .unwrap() + .to_cose(), + ); + + // Signed Public Key + let signed_public_key = SignedPublicKey::try_from(rotated_keys.signed_public_key).unwrap(); + let unwrapped_key = signed_public_key + .verify_and_unwrap( + &ctx.get_signing_key(current_user_signing_key_id) + .unwrap() + .to_verifying_key(), + ) + .unwrap(); + assert_eq!( + unwrapped_key.to_der().unwrap(), + ctx.get_asymmetric_key(current_user_private_key_id) + .unwrap() + .to_public_key() + .to_der() + .unwrap() + ); + } } diff --git a/crates/bitwarden-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 8d66a0429..6c9c70885 100644 --- a/crates/bitwarden-crypto/src/uniffi_support.rs +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -1,6 +1,8 @@ use std::{num::NonZeroU32, str::FromStr}; -use crate::{CryptoError, EncString, SignedPublicKey, UnsignedSharedKey}; +use crate::{ + security_state::SignedSecurityState, CryptoError, EncString, SignedPublicKey, UnsignedSharedKey, +}; uniffi::custom_type!(NonZeroU32, u32, { remote, @@ -32,3 +34,12 @@ uniffi::custom_type!(SignedPublicKey, String, { }, lower: |obj| obj.into(), }); + +uniffi::custom_type!(SignedSecurityState, String, { + try_lift: |val| { + val.parse().map_err(|e| { + CryptoError::EncodingError(e).into() + }) + }, + lower: |obj| obj.into(), +}); diff --git a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt index dd759516b..6e61cc7ef 100644 --- a/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt +++ b/crates/bitwarden-uniffi/kotlin/app/src/main/java/com/bitwarden/myapplication/MainActivity.kt @@ -255,6 +255,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = loginBody.PrivateKey, signingKey = null, + securityState = null, method = InitUserCryptoMethod.Password( password = PASSWORD, userKey = loginBody.Key ) @@ -341,6 +342,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = privateKey!!, signingKey = null, + securityState = null, method = InitUserCryptoMethod.DecryptedKey(decryptedUserKey = key) ) ) @@ -380,6 +382,7 @@ class MainActivity : FragmentActivity() { email = EMAIL, privateKey = privateKey!!, signingKey = null, + securityState = null, method = InitUserCryptoMethod.Pin( pinProtectedUserKey = pinProtectedUserKey, pin = PIN ) diff --git a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift index 6651785d7..9ddaf6847 100644 --- a/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift +++ b/crates/bitwarden-uniffi/swift/iOS/App/ContentView.swift @@ -194,6 +194,7 @@ struct ContentView: View { email: EMAIL, privateKey: loginData.PrivateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.password( password: PASSWORD, userKey: loginData.Key @@ -253,6 +254,7 @@ struct ContentView: View { email: EMAIL, privateKey: privateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.decryptedKey( decryptedUserKey: key ) @@ -281,6 +283,7 @@ struct ContentView: View { email: EMAIL, privateKey: privateKey, signingKey: nil, + securityState: nil, method: InitUserCryptoMethod.pin(pin: PIN, pinProtectedUserKey: pinProtectedUserKey) )) } diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 20f8588a7..66c6cfd8b 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -251,9 +251,8 @@ impl PureCrypto { shared_key: Vec, encapsulation_key: Vec, ) -> Result { - let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( - encapsulation_key.as_slice(), - ))?; + let encapsulation_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(encapsulation_key))?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(shared_key))?, &encapsulation_key, From 0387b44817b1796067b7777b3aa34da79a60ca33 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 3 Jul 2025 15:04:00 +0200 Subject: [PATCH 02/63] Cargo fmt --- crates/bitwarden-core/src/auth/tde.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index c276b95b4..1f8f9dd2d 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,10 +1,9 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SpkiPublicKeyBytes, SymmetricCryptoKey, TrustDeviceResponse, UnsignedSharedKey, UserKey, }; -use base64::{engine::general_purpose::STANDARD, Engine}; - use crate::{client::encryption_settings::EncryptionSettingsError, Client}; /// This function generates a new user key and key pair, initializes the client's crypto with the From 6c1a57d5bec7068c79429a3ef923fb614f82757c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 3 Jul 2025 15:07:12 +0200 Subject: [PATCH 03/63] Apply clippy fixes --- crates/bitwarden-core/src/auth/auth_request.rs | 8 ++++---- crates/bitwarden-core/src/key_management/crypto.rs | 6 +++--- crates/bitwarden-core/src/key_management/crypto_client.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 0e9a50c03..e41f97eca 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -175,7 +175,7 @@ mod tests { let fingerprint = fingerprint("test@bitwarden.com", &pubkey).unwrap(); assert_eq!(fingerprint, "childless-unfair-prowler-dropbox-designate"); - approve_auth_request(&client, public_key.to_owned().into()).unwrap(); + approve_auth_request(&client, public_key.to_owned()).unwrap(); } #[tokio::test] @@ -184,7 +184,7 @@ mod tests { let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); let dec = - auth_request_decrypt_user_key(private_key.to_owned().into(), enc_user_key).unwrap(); + auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); assert_eq!( &dec.to_encoded().to_vec(), @@ -202,7 +202,7 @@ mod tests { let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); let dec = auth_request_decrypt_master_key( - private_key.to_owned().into(), + private_key.to_owned(), enc_master_key, enc_user_key, ) @@ -251,7 +251,7 @@ mod tests { // Initialize an auth request, and approve it on the existing device let auth_req = new_auth_request(email).unwrap(); let approved_req = - approve_auth_request(&existing_device, auth_req.public_key.into()).unwrap(); + approve_auth_request(&existing_device, auth_req.public_key).unwrap(); // Unlock the vault using the approved request new_device diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index d1f76e395..b286434a2 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -184,13 +184,13 @@ pub(super) async fn initialize_user_crypto( } => { let user_key = match method { AuthRequestMethod::UserKey { protected_user_key } => { - auth_request_decrypt_user_key(request_private_key.into(), 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.into(), + request_private_key, protected_master_key, auth_request_key, )?, @@ -1020,7 +1020,7 @@ mod tests { let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; - let encrypted = enroll_admin_password_reset(&client, public_key.to_owned().into()).unwrap(); + let encrypted = enroll_admin_password_reset(&client, public_key.to_owned()).unwrap(); let private_key = "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="; let private_key = STANDARD.decode(private_key).unwrap(); diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index aecb6952e..2cff61aa4 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -118,7 +118,7 @@ impl CryptoClient { &self, public_key: String, ) -> Result { - enroll_admin_password_reset(&self.client, public_key.into()) + enroll_admin_password_reset(&self.client, public_key) } /// Derive the master key for migrating to the key connector From f4bd1e2e0bdb88781ed782dd342d69b34ec28404 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 3 Jul 2025 15:08:40 +0200 Subject: [PATCH 04/63] Apply cargo fmt --- crates/bitwarden-core/src/auth/auth_request.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index e41f97eca..719cc0258 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -183,8 +183,7 @@ mod tests { let private_key = "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="; let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); - let dec = - auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); + let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); assert_eq!( &dec.to_encoded().to_vec(), @@ -201,12 +200,9 @@ mod tests { let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); - let dec = auth_request_decrypt_master_key( - private_key.to_owned(), - enc_master_key, - enc_user_key, - ) - .unwrap(); + let dec = + auth_request_decrypt_master_key(private_key.to_owned(), enc_master_key, enc_user_key) + .unwrap(); assert_eq!( &dec.to_encoded().to_vec(), @@ -250,8 +246,7 @@ mod tests { // Initialize an auth request, and approve it on the existing device let auth_req = new_auth_request(email).unwrap(); - let approved_req = - approve_auth_request(&existing_device, auth_req.public_key).unwrap(); + let approved_req = approve_auth_request(&existing_device, auth_req.public_key).unwrap(); // Unlock the vault using the approved request new_device From ae4590ea35506a94f57c3ee487b9113a01001f45 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 3 Jul 2025 15:17:05 +0200 Subject: [PATCH 05/63] Fix build --- crates/bitwarden-core/src/client/encryption_settings.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index ae2e62411..6e4681bc3 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,6 +1,8 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{security_state::SignedSecurityState, EncString, UnsignedSharedKey}; -use bitwarden_crypto::{CryptoError, Pkcs8PrivateKeyBytes}; +use bitwarden_crypto::{ + security_state::SignedSecurityState, CryptoError, EncString, Pkcs8PrivateKeyBytes, + UnsignedSharedKey, +}; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; From c7dc40a06061353157867fa803ab7c64da8ffb10 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:15:23 +0200 Subject: [PATCH 06/63] Cleanup --- crates/bitwarden-core/src/key_management/crypto.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index b286434a2..6cd961a19 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -644,10 +644,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( // Sign existing public key let signed_public_key = ctx.make_signed_public_key(private_key_id, temporary_signing_key_id)?; - #[allow(deprecated)] - let public_key = ctx - .dangerous_get_asymmetric_key(private_key_id)? - .to_public_key(); + let public_key = private_key.to_public_key(); // Initialize security state for the user let security_state = bitwarden_crypto::security_state::SecurityState::initialize_for_user( From eaf788e4b617c5cbd11f218ce8a8d94683f995be Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:15:28 +0200 Subject: [PATCH 07/63] Update crates/bitwarden-core/src/key_management/crypto.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel García --- crates/bitwarden-core/src/key_management/crypto.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index b286434a2..6cd961a19 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -644,10 +644,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( // Sign existing public key let signed_public_key = ctx.make_signed_public_key(private_key_id, temporary_signing_key_id)?; - #[allow(deprecated)] - let public_key = ctx - .dangerous_get_asymmetric_key(private_key_id)? - .to_public_key(); + let public_key = private_key.to_public_key(); // Initialize security state for the user let security_state = bitwarden_crypto::security_state::SecurityState::initialize_for_user( From 2c65c3d344b51cbc50c68b00e1fb7dc84f8670aa Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:23:39 +0200 Subject: [PATCH 08/63] Split init to two functions --- .../src/client/encryption_settings.rs | 157 ++++++++++-------- 1 file changed, 88 insertions(+), 69 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 6e4681bc3..319133d84 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -3,16 +3,21 @@ use bitwarden_crypto::{ security_state::SignedSecurityState, CryptoError, EncString, Pkcs8PrivateKeyBytes, UnsignedSharedKey, }; +use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable}; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; +use log::warn; use thiserror::Error; #[cfg(any(feature = "internal", feature = "secrets"))] use uuid::Uuid; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; -use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; +use crate::{ + error::UserIdAlreadySetError, key_management::AsymmetricKeyId, MissingPrivateKeyError, + VaultLockedError, +}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -59,87 +64,101 @@ impl EncryptionSettings { security_state: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable}; - use log::warn; - - use crate::key_management::{AsymmetricKeyId, SymmetricKeyId}; - // This is an all-or-nothing check. The server cannot pretend a signing key or security // state to be missing, because they are *always* present when the user key is an // XChaCha20Poly1305Key. Thus, the server or network cannot lie about the presence of these, // because otherwise the entire user account will fail to decrypt. let is_v2_user = matches!(user_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)); if is_v2_user { - // For v2 users, we mandate the signing key and security state to be present - // The private key must also be valid. - - use bitwarden_crypto::{ - security_state::SecurityState, CoseKeyBytes, CoseSerializable, SigningKey, - }; - - // Both of these are required for v2 users - let signing_key = signing_key.ok_or(EncryptionSettingsError::Crypto( - CryptoError::SecurityDowngrade("Signing key is required for v2 users".to_string()), - ))?; - let security_state = security_state.ok_or(EncryptionSettingsError::Crypto( - CryptoError::SecurityDowngrade( - "Security state is required for v2 users".to_string(), - ), - ))?; - - // Everything MUST decrypt. - let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; - let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(signing_key)) - .map_err(|_| EncryptionSettingsError::InvalidSigningKey)?; - let private_key: Vec = private_key.decrypt_with_key(&user_key)?; - let private_key = - AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key)) - .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; - let _security_state: SecurityState = security_state - .verify_and_unwrap(&signing_key.to_verifying_key()) - .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; - - #[allow(deprecated)] - { - use crate::key_management::SigningKeyId; - - let mut ctx = store.context_mut(); - ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; - ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; - ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; - } + Self::init_v2(user_key, private_key, signing_key, security_state, store)?; } else { - let private_key = { - let dec: Vec = private_key.decrypt_with_key(&user_key)?; - - // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a - // recovery process in place. - AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(dec)) - .map_err(|_| { - warn!("Invalid private key"); - }) - .ok() - - // Some( - // AsymmetricCryptoKey::from_der(&dec) - // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, - // ) - }; - - // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods - #[allow(deprecated)] - { - let mut ctx = store.context_mut(); - ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; - if let Some(private_key) = private_key { - ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; - } + Self::init_v1(user_key, private_key, store)?; + } + + Ok(()) + } + + fn init_v1( + user_key: SymmetricCryptoKey, + private_key: EncString, + store: &KeyStore, + ) -> Result<(), EncryptionSettingsError> { + let private_key = { + let dec: Vec = private_key.decrypt_with_key(&user_key)?; + + // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a + // recovery process in place. + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(dec)) + .map_err(|_| { + warn!("Invalid private key"); + }) + .ok() + + // Some( + // AsymmetricCryptoKey::from_der(&dec) + // .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?, + // ) + }; + + // FIXME: [PM-18098] When this is part of crypto we won't need to use deprecated methods + #[allow(deprecated)] + { + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; + if let Some(private_key) = private_key { + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; } } Ok(()) } + fn init_v2( + user_key: SymmetricCryptoKey, + private_key: EncString, + signing_key: Option, + security_state: Option, + store: &KeyStore, + ) -> Result<(), EncryptionSettingsError> { + // For v2 users, we mandate the signing key and security state to be present + // The private key must also be valid. + + use bitwarden_crypto::{ + security_state::SecurityState, CoseKeyBytes, CoseSerializable, SigningKey, + }; + + // Both of these are required for v2 users + let signing_key = signing_key.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade("Signing key is required for v2 users".to_string()), + ))?; + let security_state = security_state.ok_or(EncryptionSettingsError::Crypto( + CryptoError::SecurityDowngrade("Security state is required for v2 users".to_string()), + ))?; + + // Everything MUST decrypt. + let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; + let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(signing_key)) + .map_err(|_| EncryptionSettingsError::InvalidSigningKey)?; + let private_key: Vec = private_key.decrypt_with_key(&user_key)?; + let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key)) + .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; + let _security_state: SecurityState = security_state + .verify_and_unwrap(&signing_key.to_verifying_key()) + .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; + + #[allow(deprecated)] + { + use crate::key_management::SigningKeyId; + + let mut ctx = store.context_mut(); + ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; + ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; + } + + Ok(()) + } + /// Initialize the encryption settings with only a single decrypted organization key. /// This is used only for logging in Secrets Manager with an access token #[cfg(feature = "secrets")] From db94fd3cb517e0bd0e1d35a659347b31ca83e071 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:25:53 +0200 Subject: [PATCH 09/63] Replace `EncryptionSettingsError::Crypto` with into --- crates/bitwarden-core/src/client/encryption_settings.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 319133d84..784e41ee5 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -128,11 +128,11 @@ impl EncryptionSettings { }; // Both of these are required for v2 users - let signing_key = signing_key.ok_or(EncryptionSettingsError::Crypto( - CryptoError::SecurityDowngrade("Signing key is required for v2 users".to_string()), + let signing_key = signing_key.ok_or(CryptoError::SecurityDowngrade( + "Signing key is required for v2 users".to_string(), ))?; - let security_state = security_state.ok_or(EncryptionSettingsError::Crypto( - CryptoError::SecurityDowngrade("Security state is required for v2 users".to_string()), + let security_state = security_state.ok_or(CryptoError::SecurityDowngrade( + "Security state is required for v2 users".to_string(), ))?; // Everything MUST decrypt. From 77a2ebe7543311fc77747d2354351576b466a2e4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:33:34 +0200 Subject: [PATCH 10/63] Fix imports --- .../src/client/encryption_settings.rs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 784e41ee5..127a8d604 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,23 +1,24 @@ #[cfg(feature = "internal")] +use crate::key_management::{AsymmetricKeyId, SigningKeyId}; +#[cfg(feature = "internal")] use bitwarden_crypto::{ - security_state::SignedSecurityState, CryptoError, EncString, Pkcs8PrivateKeyBytes, - UnsignedSharedKey, + security_state::SecurityState, security_state::SignedSecurityState, AsymmetricCryptoKey, + CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, Pkcs8PrivateKeyBytes, + SigningKey, UnsignedSharedKey, }; -use bitwarden_crypto::{AsymmetricCryptoKey, KeyDecryptable}; +#[cfg(feature = "internal")] +use log::warn; + #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; -use log::warn; use thiserror::Error; #[cfg(any(feature = "internal", feature = "secrets"))] use uuid::Uuid; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; -use crate::{ - error::UserIdAlreadySetError, key_management::AsymmetricKeyId, MissingPrivateKeyError, - VaultLockedError, -}; +use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -78,6 +79,7 @@ impl EncryptionSettings { Ok(()) } + #[cfg(feature = "internal")] fn init_v1( user_key: SymmetricCryptoKey, private_key: EncString, @@ -106,6 +108,8 @@ impl EncryptionSettings { let mut ctx = store.context_mut(); ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; if let Some(private_key) = private_key { + use crate::key_management::AsymmetricKeyId; + ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; } } @@ -113,6 +117,7 @@ impl EncryptionSettings { Ok(()) } + #[cfg(feature = "internal")] fn init_v2( user_key: SymmetricCryptoKey, private_key: EncString, @@ -123,10 +128,6 @@ impl EncryptionSettings { // For v2 users, we mandate the signing key and security state to be present // The private key must also be valid. - use bitwarden_crypto::{ - security_state::SecurityState, CoseKeyBytes, CoseSerializable, SigningKey, - }; - // Both of these are required for v2 users let signing_key = signing_key.ok_or(CryptoError::SecurityDowngrade( "Signing key is required for v2 users".to_string(), @@ -148,8 +149,6 @@ impl EncryptionSettings { #[allow(deprecated)] { - use crate::key_management::SigningKeyId; - let mut ctx = store.context_mut(); ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; From 14f5d3f196ff536b867d4521afe3338c05aeecf8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:35:23 +0200 Subject: [PATCH 11/63] Cargo fmt --- crates/bitwarden-core/src/client/encryption_settings.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 127a8d604..81416cf7a 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,21 +1,20 @@ #[cfg(feature = "internal")] -use crate::key_management::{AsymmetricKeyId, SigningKeyId}; -#[cfg(feature = "internal")] use bitwarden_crypto::{ security_state::SecurityState, security_state::SignedSecurityState, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, Pkcs8PrivateKeyBytes, SigningKey, UnsignedSharedKey, }; -#[cfg(feature = "internal")] -use log::warn; - #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; +#[cfg(feature = "internal")] +use log::warn; use thiserror::Error; #[cfg(any(feature = "internal", feature = "secrets"))] use uuid::Uuid; +#[cfg(feature = "internal")] +use crate::key_management::{AsymmetricKeyId, SigningKeyId}; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; From 4fea3dc9f908b0b14f7266c09324c8fb00af13e7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 14:47:18 +0200 Subject: [PATCH 12/63] Cleanup --- .../src/client/encryption_settings.rs | 5 ++-- crates/bitwarden-core/src/client/internal.rs | 4 +-- .../src/key_management/crypto.rs | 17 +++++------- crates/bitwarden-crypto/src/lib.rs | 5 ++-- crates/bitwarden-crypto/src/security_state.rs | 27 +++++++++---------- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 81416cf7a..a7260f10f 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,8 +1,7 @@ #[cfg(feature = "internal")] use bitwarden_crypto::{ - security_state::SecurityState, security_state::SignedSecurityState, AsymmetricCryptoKey, - CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, Pkcs8PrivateKeyBytes, - SigningKey, UnsignedSharedKey, + AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, + Pkcs8PrivateKeyBytes, SecurityState, SignedSecurityState, SigningKey, UnsignedSharedKey, }; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 42d7ccac6..6691d16f5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -4,9 +4,7 @@ use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{ - security_state::SignedSecurityState, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey, -}; +use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, SignedSecurityState, UnsignedSharedKey}; #[cfg(feature = "internal")] use bitwarden_state::registry::StateRegistry; use chrono::Utc; diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 6cd961a19..6736fac37 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,11 +8,10 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - dangerous_get_v2_rotated_account_keys, security_state::SignedSecurityState, - AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, RotatedUserKeys, SignatureAlgorithm, - SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, - UserKey, + dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, + EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, + RotatedUserKeys, SecurityState, SignatureAlgorithm, SignedPublicKey, SignedSecurityState, + SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -647,17 +646,13 @@ pub(crate) fn make_keys_for_user_crypto_v2( let public_key = private_key.to_public_key(); // Initialize security state for the user - let security_state = bitwarden_crypto::security_state::SecurityState::initialize_for_user( + let security_state = SecurityState::initialize_for_user( client .internal .get_user_id() .ok_or(CryptoError::UninitializedError)?, ); - let signed_security_state = bitwarden_crypto::security_state::sign( - &security_state, - temporary_signing_key_id, - &mut ctx, - )?; + let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; Ok(EnrollUserCryptoV2Response { user_key: user_key.to_base64(), diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index f84993b22..42701b6b8 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -36,9 +36,8 @@ pub use store::{ }; mod cose; pub use cose::CoseSerializable; -/// The `SecurityState` module provides functionality to cryptographically attest to which features -/// are allowed to be used for a user. -pub mod security_state; +mod security_state; +pub use security_state::{SecurityState, SignedSecurityState}; mod signing; pub use signing::*; mod traits; diff --git a/crates/bitwarden-crypto/src/security_state.rs b/crates/bitwarden-crypto/src/security_state.rs index f158d2c5f..162256db8 100644 --- a/crates/bitwarden-crypto/src/security_state.rs +++ b/crates/bitwarden-crypto/src/security_state.rs @@ -68,19 +68,19 @@ impl SecurityState { pub fn version(&self) -> u64 { self.version } -} -/// Signs the `SecurityState` with the provided signing key ID from the context. -pub fn sign( - security_state: &SecurityState, - signing_key_id: Ids::Signing, - ctx: &mut KeyStoreContext, -) -> Result { - Ok(SignedSecurityState(ctx.sign( - signing_key_id, - security_state, - &SigningNamespace::SecurityState, - )?)) + /// Signs the `SecurityState` with the provided signing key ID from the context. + pub fn sign( + &self, + signing_key_id: Ids::Signing, + ctx: &mut KeyStoreContext, + ) -> Result { + Ok(SignedSecurityState(ctx.sign( + signing_key_id, + &self, + &SigningNamespace::SecurityState, + )?)) + } } impl SignedSecurityState { @@ -177,8 +177,7 @@ mod tests { #[allow(deprecated)] ctx.set_signing_key(TestSigningKey::B, signing_key.clone()) .unwrap(); - let signed_security_state = - crate::security_state::sign(&security_state, TestSigningKey::B, &mut ctx).unwrap(); + let signed_security_state = security_state.sign(TestSigningKey::B, &mut ctx).unwrap(); let verifying_key = signing_key.to_verifying_key(); let verified_security_state = signed_security_state From 46d22027d342f2460c0b91a74296d3726cd00897 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:05:15 +0200 Subject: [PATCH 13/63] Cleanup init with enum --- .../src/client/encryption_settings.rs | 53 +++++++++++++++---- crates/bitwarden-core/src/client/internal.rs | 46 +++++++++++----- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index a7260f10f..4d5f035fc 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::{Aes256CbcHmacKey, XChaCha20Poly1305Key}; #[cfg(feature = "internal")] use bitwarden_crypto::{ AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, @@ -47,6 +48,19 @@ pub enum EncryptionSettingsError { UserIdAlreadySetError(#[from] UserIdAlreadySetError), } +pub(crate) enum AccountEncryptionKeys { + V1 { + user_key: Aes256CbcHmacKey, + private_key: EncString, + }, + V2 { + user_key: XChaCha20Poly1305Key, + private_key: EncString, + signing_key: EncString, + security_state: SignedSecurityState, + }, +} + #[allow(missing_docs)] pub struct EncryptionSettings {} @@ -57,21 +71,34 @@ impl EncryptionSettings { /// discouraged #[cfg(feature = "internal")] pub(crate) fn new_decrypted_key( - user_key: SymmetricCryptoKey, - private_key: EncString, - signing_key: Option, - security_state: Option, + encryption_keys: AccountEncryptionKeys, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { // This is an all-or-nothing check. The server cannot pretend a signing key or security // state to be missing, because they are *always* present when the user key is an // XChaCha20Poly1305Key. Thus, the server or network cannot lie about the presence of these, // because otherwise the entire user account will fail to decrypt. - let is_v2_user = matches!(user_key, SymmetricCryptoKey::XChaCha20Poly1305Key(_)); - if is_v2_user { - Self::init_v2(user_key, private_key, signing_key, security_state, store)?; - } else { - Self::init_v1(user_key, private_key, store)?; + match encryption_keys { + AccountEncryptionKeys::V1 { + user_key, + private_key, + } => { + Self::init_v1(user_key, private_key, store)?; + } + AccountEncryptionKeys::V2 { + user_key, + private_key, + signing_key, + security_state, + } => { + Self::init_v2( + user_key, + private_key, + Some(signing_key), + Some(security_state), + store, + )?; + } } Ok(()) @@ -79,10 +106,12 @@ impl EncryptionSettings { #[cfg(feature = "internal")] fn init_v1( - user_key: SymmetricCryptoKey, + user_key: Aes256CbcHmacKey, private_key: EncString, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { + let user_key = SymmetricCryptoKey::Aes256CbcHmacKey(user_key); + let private_key = { let dec: Vec = private_key.decrypt_with_key(&user_key)?; @@ -117,12 +146,14 @@ impl EncryptionSettings { #[cfg(feature = "internal")] fn init_v2( - user_key: SymmetricCryptoKey, + user_key: XChaCha20Poly1305Key, private_key: EncString, signing_key: Option, security_state: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { + let user_key = SymmetricCryptoKey::XChaCha20Poly1305Key(user_key); + // For v2 users, we mandate the signing key and security state to be present // The private key must also be valid. diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 6691d16f5..bff1f45ec 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -1,10 +1,14 @@ use std::sync::{Arc, OnceLock, RwLock}; +#[cfg(feature = "internal")] +use crate::client::encryption_settings::AccountEncryptionKeys; use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, SignedSecurityState, UnsignedSharedKey}; +use bitwarden_crypto::{ + CryptoError, EncString, Kdf, MasterKey, PinKey, SignedSecurityState, UnsignedSharedKey, +}; #[cfg(feature = "internal")] use bitwarden_state::registry::StateRegistry; use chrono::Utc; @@ -220,14 +224,12 @@ impl InternalClient { security_state: Option, ) -> Result<(), EncryptionSettingsError> { let user_key = master_key.decrypt_user_key(user_key)?; - EncryptionSettings::new_decrypted_key( + self.initialize_user_crypto_decrypted_key( user_key, private_key, signing_key, security_state, - &self.key_store, - )?; - + ); Ok(()) } @@ -239,13 +241,33 @@ impl InternalClient { signing_key: Option, security_state: Option, ) -> Result<(), EncryptionSettingsError> { - EncryptionSettings::new_decrypted_key( - user_key, - private_key, - signing_key, - security_state, - &self.key_store, - )?; + match user_key { + SymmetricCryptoKey::Aes256CbcHmacKey(ref user_key) => { + EncryptionSettings::new_decrypted_key( + AccountEncryptionKeys::V1 { + user_key: user_key.clone(), + private_key, + }, + &self.key_store, + )?; + } + SymmetricCryptoKey::XChaCha20Poly1305Key(ref user_key) => { + EncryptionSettings::new_decrypted_key( + AccountEncryptionKeys::V2 { + user_key: user_key.clone(), + private_key, + signing_key: signing_key + .ok_or(EncryptionSettingsError::InvalidSigningKey)?, + security_state: security_state + .ok_or(EncryptionSettingsError::InvalidSecurityState)?, + }, + &self.key_store, + )?; + } + _ => { + return Err(CryptoError::InvalidKey.into()); + } + } Ok(()) } From b20e5a4b2863e1a66ca3d0533946a37dc76b3dfd Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:06:44 +0200 Subject: [PATCH 14/63] Cleanup --- crates/bitwarden-core/src/client/encryption_settings.rs | 7 ++++--- crates/bitwarden-core/src/client/internal.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 4d5f035fc..3320b221f 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,8 +1,8 @@ -use bitwarden_crypto::{Aes256CbcHmacKey, XChaCha20Poly1305Key}; #[cfg(feature = "internal")] use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyDecryptable, - Pkcs8PrivateKeyBytes, SecurityState, SignedSecurityState, SigningKey, UnsignedSharedKey, + Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, + KeyDecryptable, Pkcs8PrivateKeyBytes, SecurityState, SignedSecurityState, SigningKey, + UnsignedSharedKey, XChaCha20Poly1305Key, }; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; @@ -48,6 +48,7 @@ pub enum EncryptionSettingsError { UserIdAlreadySetError(#[from] UserIdAlreadySetError), } +#[cfg(feature = "internal")] pub(crate) enum AccountEncryptionKeys { V1 { user_key: Aes256CbcHmacKey, diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index bff1f45ec..7449b4e9f 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -1,7 +1,5 @@ use std::sync::{Arc, OnceLock, RwLock}; -#[cfg(feature = "internal")] -use crate::client::encryption_settings::AccountEncryptionKeys; use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; @@ -14,6 +12,8 @@ use bitwarden_state::registry::StateRegistry; use chrono::Utc; use uuid::Uuid; +#[cfg(feature = "internal")] +use crate::client::encryption_settings::AccountEncryptionKeys; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] From 566dc475f0c635ab698885d722b0ada73ef18c3d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:14:16 +0200 Subject: [PATCH 15/63] Fix clippy warnings --- crates/bitwarden-core/src/client/encryption_settings.rs | 1 + crates/bitwarden-core/src/client/internal.rs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 3320b221f..111f41390 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -48,6 +48,7 @@ pub enum EncryptionSettingsError { UserIdAlreadySetError(#[from] UserIdAlreadySetError), } +#[allow(clippy::large_enum_variant)] #[cfg(feature = "internal")] pub(crate) enum AccountEncryptionKeys { V1 { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 7449b4e9f..3d522f399 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -229,8 +229,7 @@ impl InternalClient { private_key, signing_key, security_state, - ); - Ok(()) + ) } #[cfg(feature = "internal")] From 678a9e3feabee32f2430b5a1d8ca7f4abe905cc3 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:36:33 +0200 Subject: [PATCH 16/63] Move security state to core --- Cargo.lock | 1 + crates/bitwarden-core/Cargo.toml | 1 + .../src/client/encryption_settings.rs | 7 +++--- crates/bitwarden-core/src/client/internal.rs | 10 ++++---- .../src/key_management/crypto.rs | 8 ++++--- .../bitwarden-core/src/key_management/mod.rs | 3 +++ .../src/key_management}/security_state.rs | 24 +++++++++---------- crates/bitwarden-core/src/uniffi_support.rs | 12 ++++++++++ crates/bitwarden-crypto/src/error.rs | 6 +++++ crates/bitwarden-crypto/src/lib.rs | 6 ++--- crates/bitwarden-crypto/src/store/context.rs | 3 +-- crates/bitwarden-crypto/src/uniffi_support.rs | 13 +--------- crates/bitwarden-crypto/src/util.rs | 6 +++-- 13 files changed, 57 insertions(+), 43 deletions(-) rename crates/{bitwarden-crypto/src => bitwarden-core/src/key_management}/security_state.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 82d1c09b0..4d322f6ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,7 @@ dependencies = [ "rustls-platform-verifier", "schemars 0.8.22", "serde", + "serde_bytes", "serde_json", "serde_qs", "serde_repr", diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index 170fa1dd1..b3d9c4165 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -45,6 +45,7 @@ rand = ">=0.8.5, <0.9" reqwest = { workspace = true } schemars = { workspace = true } serde = { workspace = true } +serde_bytes = "0.11.17" serde_json = { workspace = true } serde_qs = { workspace = true } serde_repr = { workspace = true } diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 111f41390..4dcc93852 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,8 +1,7 @@ #[cfg(feature = "internal")] use bitwarden_crypto::{ Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, - KeyDecryptable, Pkcs8PrivateKeyBytes, SecurityState, SignedSecurityState, SigningKey, - UnsignedSharedKey, XChaCha20Poly1305Key, + KeyDecryptable, Pkcs8PrivateKeyBytes, SigningKey, UnsignedSharedKey, XChaCha20Poly1305Key, }; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::{KeyStore, SymmetricCryptoKey}; @@ -14,7 +13,7 @@ use thiserror::Error; use uuid::Uuid; #[cfg(feature = "internal")] -use crate::key_management::{AsymmetricKeyId, SigningKeyId}; +use crate::key_management::{AsymmetricKeyId, SignedSecurityState, SigningKeyId}; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; @@ -154,6 +153,8 @@ impl EncryptionSettings { security_state: Option, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { + use crate::key_management::SecurityState; + let user_key = SymmetricCryptoKey::XChaCha20Poly1305Key(user_key); // For v2 users, we mandate the signing key and security state to be present diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 3d522f399..1e29b94d7 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -4,16 +4,12 @@ use bitwarden_crypto::KeyStore; #[cfg(any(feature = "internal", feature = "secrets"))] use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] -use bitwarden_crypto::{ - CryptoError, EncString, Kdf, MasterKey, PinKey, SignedSecurityState, UnsignedSharedKey, -}; +use bitwarden_crypto::{CryptoError, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey}; #[cfg(feature = "internal")] use bitwarden_state::registry::StateRegistry; use chrono::Utc; use uuid::Uuid; -#[cfg(feature = "internal")] -use crate::client::encryption_settings::AccountEncryptionKeys; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] @@ -23,6 +19,10 @@ use crate::{ key_management::KeyIds, DeviceType, }; #[cfg(feature = "internal")] +use crate::{ + client::encryption_settings::AccountEncryptionKeys, key_management::SignedSecurityState, +}; +#[cfg(feature = "internal")] use crate::{ client::encryption_settings::EncryptionSettingsError, client::{flags::Flags, login_method::UserLoginMethod}, diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 6736fac37..4430ba4c5 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -10,8 +10,8 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, - RotatedUserKeys, SecurityState, SignatureAlgorithm, SignedPublicKey, SignedSecurityState, - SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, + SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -21,7 +21,9 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, - key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}, + key_management::{ + AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, + }, Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError, }; diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 79bbf124a..0305fd970 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,6 +18,9 @@ mod crypto_client; #[cfg(feature = "internal")] pub use crypto_client::CryptoClient; +mod security_state; +pub use security_state::{SecurityState, SignedSecurityState}; + key_ids! { #[symmetric] pub enum SymmetricKeyId { diff --git a/crates/bitwarden-crypto/src/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs similarity index 92% rename from crates/bitwarden-crypto/src/security_state.rs rename to crates/bitwarden-core/src/key_management/security_state.rs index 162256db8..62e3cbabd 100644 --- a/crates/bitwarden-crypto/src/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -23,14 +23,13 @@ use std::str::FromStr; use base64::{engine::general_purpose::STANDARD, Engine}; +use bitwarden_crypto::{ + CoseSerializable, CoseSign1Bytes, CryptoError, EncodingError, FromStrVisitor, KeyIds, + KeyStoreContext, SignedObject, SigningNamespace, VerifyingKey, +}; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; -use crate::{ - cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CoseSign1Bytes, - CryptoError, KeyIds, KeyStoreContext, SignedObject, SigningNamespace, VerifyingKey, -}; - #[cfg(feature = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] const TS_CUSTOM_TYPES: &'static str = r#" @@ -160,24 +159,25 @@ impl schemars::JsonSchema for SignedSecurityState { #[cfg(test)] mod tests { + use bitwarden_crypto::{KeyStore, SignatureAlgorithm, SigningKey}; + use super::*; - use crate::{ - traits::tests::{TestIds, TestSigningKey}, - KeyStore, SignatureAlgorithm, SigningKey, - }; + use crate::key_management::{KeyIds, SigningKeyId}; #[test] fn test_security_state_signing() { - let store: KeyStore = KeyStore::default(); + let store: KeyStore = KeyStore::default(); let mut ctx = store.context_mut(); let user_id = uuid::Uuid::new_v4(); let security_state = SecurityState::initialize_for_user(user_id); let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519); #[allow(deprecated)] - ctx.set_signing_key(TestSigningKey::B, signing_key.clone()) + ctx.set_signing_key(SigningKeyId::Local(""), signing_key.clone()) + .unwrap(); + let signed_security_state = security_state + .sign(SigningKeyId::Local(""), &mut ctx) .unwrap(); - let signed_security_state = security_state.sign(TestSigningKey::B, &mut ctx).unwrap(); let verifying_key = signing_key.to_verifying_key(); let verified_security_state = signed_security_state diff --git a/crates/bitwarden-core/src/uniffi_support.rs b/crates/bitwarden-core/src/uniffi_support.rs index 8d4358707..059c7510e 100644 --- a/crates/bitwarden-core/src/uniffi_support.rs +++ b/crates/bitwarden-core/src/uniffi_support.rs @@ -2,8 +2,11 @@ use std::num::NonZeroU32; +use bitwarden_crypto::CryptoError; use uuid::Uuid; +use crate::key_management::SignedSecurityState; + uniffi::use_remote_type!(bitwarden_crypto::NonZeroU32); type DateTime = chrono::DateTime; @@ -23,3 +26,12 @@ struct UniffiConverterDummyRecord { uuid: Uuid, date: DateTime, } + +uniffi::custom_type!(SignedSecurityState, String, { + try_lift: |val| { + val.parse().map_err(|e| { + CryptoError::EncodingError(e).into() + }) + }, + lower: |obj| obj.into(), +}); diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 3eb6e0708..bd7dc0bd3 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -121,16 +121,22 @@ pub enum SignatureError { InvalidNamespace, } +/// Error type issues en- or de-coding values #[derive(Debug, Error)] pub enum EncodingError { + /// An error occurred while serializing or deserializing a value using COSE #[error("Invalid cose encoding")] InvalidCoseEncoding, + /// An error occurred while serializing or deserializing a value using CBOR #[error("Cbor serialization error")] InvalidCborSerialization, + /// A required value is missing from the serialized message #[error("Missing value {0}")] MissingValue(&'static str), + /// A value is invalid / outside the expected range #[error("Invalid value {0}")] InvalidValue(&'static str), + /// A value is unsupported but may be valid #[error("Unsupported value {0}")] UnsupportedValue(&'static str), } diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 42701b6b8..e5e86891b 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -18,8 +18,8 @@ pub use content_format::*; mod enc_string; pub use enc_string::{EncString, UnsignedSharedKey}; mod error; -pub use error::CryptoError; pub(crate) use error::Result; +pub use error::{CryptoError, EncodingError}; mod fingerprint; pub use fingerprint::fingerprint; mod keys; @@ -27,7 +27,7 @@ pub use keys::*; mod rsa; pub use crate::rsa::RsaKeyPair; mod util; -pub use util::{generate_random_alphanumeric, generate_random_bytes, pbkdf2}; +pub use util::{generate_random_alphanumeric, generate_random_bytes, pbkdf2, FromStrVisitor}; mod wordlist; pub use wordlist::EFF_LONG_WORD_LIST; mod store; @@ -36,8 +36,6 @@ pub use store::{ }; mod cose; pub use cose::CoseSerializable; -mod security_state; -pub use security_state::{SecurityState, SignedSecurityState}; mod signing; pub use signing::*; mod traits; diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 5e2e42825..89e65cb80 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -503,8 +503,7 @@ impl KeyStoreContext<'_, Ids> { /// Signs the given data using the specified signing key, for the given /// [crate::SigningNamespace] and returns the signature and the serialized message. See /// [crate::SigningKey::sign] - #[allow(unused)] - pub(crate) fn sign( + pub fn sign( &self, key: Ids::Signing, message: &Message, diff --git a/crates/bitwarden-crypto/src/uniffi_support.rs b/crates/bitwarden-crypto/src/uniffi_support.rs index 6c9c70885..8d66a0429 100644 --- a/crates/bitwarden-crypto/src/uniffi_support.rs +++ b/crates/bitwarden-crypto/src/uniffi_support.rs @@ -1,8 +1,6 @@ use std::{num::NonZeroU32, str::FromStr}; -use crate::{ - security_state::SignedSecurityState, CryptoError, EncString, SignedPublicKey, UnsignedSharedKey, -}; +use crate::{CryptoError, EncString, SignedPublicKey, UnsignedSharedKey}; uniffi::custom_type!(NonZeroU32, u32, { remote, @@ -34,12 +32,3 @@ uniffi::custom_type!(SignedPublicKey, String, { }, lower: |obj| obj.into(), }); - -uniffi::custom_type!(SignedSecurityState, String, { - try_lift: |val| { - val.parse().map_err(|e| { - CryptoError::EncodingError(e).into() - }) - }, - lower: |obj| obj.into(), -}); diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs index 5ab985f7c..4ac36a21d 100644 --- a/crates/bitwarden-crypto/src/util.rs +++ b/crates/bitwarden-crypto/src/util.rs @@ -53,9 +53,11 @@ pub fn pbkdf2(password: &[u8], salt: &[u8], rounds: u32) -> [u8; PBKDF_SHA256_HM .expect("hash is a valid fixed size") } -pub(crate) struct FromStrVisitor(std::marker::PhantomData); +/// A serde visitor that converts a string to a type that implements `FromStr`. +pub struct FromStrVisitor(std::marker::PhantomData); impl FromStrVisitor { - pub(crate) fn new() -> Self { + /// Create a new `FromStrVisitor` for the given type. + pub fn new() -> Self { Self(Default::default()) } } From b891ec4e4562700781e1a242da31521e320a2f9f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:37:41 +0200 Subject: [PATCH 17/63] Fix version range --- crates/bitwarden-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index b3d9c4165..2ddd33ef2 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -45,7 +45,7 @@ rand = ">=0.8.5, <0.9" reqwest = { workspace = true } schemars = { workspace = true } serde = { workspace = true } -serde_bytes = "0.11.17" +serde_bytes = ">=0.11.17, <0.12" serde_json = { workspace = true } serde_qs = { workspace = true } serde_repr = { workspace = true } From 3dd636621ae65547559aec4c814f28cdb1f87332 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:41:24 +0200 Subject: [PATCH 18/63] Fix clippy errors --- crates/bitwarden-crypto/src/util.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs index 4ac36a21d..a556f9434 100644 --- a/crates/bitwarden-crypto/src/util.rs +++ b/crates/bitwarden-crypto/src/util.rs @@ -58,6 +58,11 @@ pub struct FromStrVisitor(std::marker::PhantomData); impl FromStrVisitor { /// Create a new `FromStrVisitor` for the given type. pub fn new() -> Self { + Self::default() + } +} +impl Default for FromStrVisitor { + fn default() -> Self { Self(Default::default()) } } From 24ec20d7d5541ae2bfdd97b3cd3a7082c77e3601 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 4 Jul 2025 15:48:05 +0200 Subject: [PATCH 19/63] Fix comment --- crates/bitwarden-crypto/src/signing/namespace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index 7f5b0c2bb..cd805b496 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -12,8 +12,7 @@ pub enum SigningNamespace { /// The namespace for /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, - /// The namespace for - /// [`SignedSecurityState`](crate::security_state::SignedSecurityState). + /// The namespace for SignedSecurityState SecurityState = 2, /// This namespace is only used in tests #[cfg(test)] From 6f17cd4bf0d6987dac081c91f3ccb92ae000b0f0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 8 Jul 2025 12:43:04 +0200 Subject: [PATCH 20/63] Move serde bytes to workspace --- Cargo.toml | 1 + crates/bitwarden-core/Cargo.toml | 2 +- crates/bitwarden-crypto/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18668f7f6..9f4839012 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ reqwest = { version = ">=0.12.5, <0.13", features = [ ], default-features = false } schemars = { version = ">=0.8.9, <0.9", features = ["uuid1", "chrono"] } serde = { version = ">=1.0, <2.0", features = ["derive"] } +serde_bytes = { version = ">=0.11.17, <0.12.0" } serde_json = ">=1.0.96, <2.0" serde_qs = ">=0.12.0, <0.16" serde_repr = ">=0.1.12, <0.2" diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index 2ddd33ef2..3e9084c12 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -45,7 +45,7 @@ rand = ">=0.8.5, <0.9" reqwest = { workspace = true } schemars = { workspace = true } serde = { workspace = true } -serde_bytes = ">=0.11.17, <0.12" +serde_bytes = { workspace = true } serde_json = { workspace = true } serde_qs = { workspace = true } serde_repr = { workspace = true } diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 38623b463..72ae4fb91 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -46,7 +46,7 @@ rayon = ">=1.8.1, <2.0" rsa = ">=0.9.2, <0.10" schemars = { workspace = true } serde = { workspace = true } -serde_bytes = ">=0.11.17, <0.12.0" +serde_bytes = { workspace = true } serde_repr.workspace = true sha1 = ">=0.10.5, <0.11" sha2 = ">=0.10.6, <0.11" From 9922265789e6fe2fa82abc0d5c2b37f5ec847630 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 8 Jul 2025 12:46:25 +0200 Subject: [PATCH 21/63] Make signing key and security state non-optional --- .../src/client/encryption_settings.rs | 24 ++++--------------- crates/bitwarden-crypto/src/error.rs | 3 --- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 4dcc93852..554e76dd1 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -92,13 +92,7 @@ impl EncryptionSettings { signing_key, security_state, } => { - Self::init_v2( - user_key, - private_key, - Some(signing_key), - Some(security_state), - store, - )?; + Self::init_v2(user_key, private_key, signing_key, security_state, store)?; } } @@ -149,25 +143,15 @@ impl EncryptionSettings { fn init_v2( user_key: XChaCha20Poly1305Key, private_key: EncString, - signing_key: Option, - security_state: Option, + signing_key: EncString, + security_state: SignedSecurityState, store: &KeyStore, ) -> Result<(), EncryptionSettingsError> { use crate::key_management::SecurityState; let user_key = SymmetricCryptoKey::XChaCha20Poly1305Key(user_key); - // For v2 users, we mandate the signing key and security state to be present - // The private key must also be valid. - - // Both of these are required for v2 users - let signing_key = signing_key.ok_or(CryptoError::SecurityDowngrade( - "Signing key is required for v2 users".to_string(), - ))?; - let security_state = security_state.ok_or(CryptoError::SecurityDowngrade( - "Security state is required for v2 users".to_string(), - ))?; - + // For v2 users, we mandate the signing key and security state and the private key to be present and valid // Everything MUST decrypt. let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(signing_key)) diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index bd7dc0bd3..640dfcbb5 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -72,9 +72,6 @@ pub enum CryptoError { #[error("Uninitialized error")] UninitializedError, - - #[error("Attempted security downgrade {0}")] - SecurityDowngrade(String), } #[derive(Debug, Error)] From 5f98cab7506199b82dcd264b961332d487dc53ac Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 8 Jul 2025 12:47:48 +0200 Subject: [PATCH 22/63] Remove unused import --- crates/bitwarden-core/src/client/encryption_settings.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 554e76dd1..79e990108 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -130,8 +130,6 @@ impl EncryptionSettings { let mut ctx = store.context_mut(); ctx.set_symmetric_key(SymmetricKeyId::User, user_key)?; if let Some(private_key) = private_key { - use crate::key_management::AsymmetricKeyId; - ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; } } From 429066a8511f2a6babe29eb230c2f962b5c55edf Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 8 Jul 2025 12:49:22 +0200 Subject: [PATCH 23/63] Add comment --- crates/bitwarden-core/src/client/encryption_settings.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 79e990108..7acec95f1 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -149,14 +149,16 @@ impl EncryptionSettings { let user_key = SymmetricCryptoKey::XChaCha20Poly1305Key(user_key); - // For v2 users, we mandate the signing key and security state and the private key to be present and valid - // Everything MUST decrypt. + // For v2 users, we mandate the signing key and security state and the private key to be + // present and valid Everything MUST decrypt. let signing_key: Vec = signing_key.decrypt_with_key(&user_key)?; let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(signing_key)) .map_err(|_| EncryptionSettingsError::InvalidSigningKey)?; let private_key: Vec = private_key.decrypt_with_key(&user_key)?; let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key)) .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; + // This is not used further currently, but we still need to verify that it decrypts properly + // and is valid. let _security_state: SecurityState = security_state .verify_and_unwrap(&signing_key.to_verifying_key()) .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; From 162e2183a8e767df13c651496d41d4c98b3e0bc2 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 8 Jul 2025 12:51:42 +0200 Subject: [PATCH 24/63] Remove unused import --- crates/bitwarden-core/src/client/encryption_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 7acec95f1..02992b8e7 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,6 +1,6 @@ #[cfg(feature = "internal")] use bitwarden_crypto::{ - Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, EncString, + Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, EncString, KeyDecryptable, Pkcs8PrivateKeyBytes, SigningKey, UnsignedSharedKey, XChaCha20Poly1305Key, }; #[cfg(any(feature = "internal", feature = "secrets"))] From 381b54763ab105dc090012a12202a063b4f66292 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 9 Jul 2025 13:52:13 +0200 Subject: [PATCH 25/63] Tmp --- crates/bitwarden-core/src/client/client.rs | 1 + .../src/client/encryption_settings.rs | 17 ++++++- crates/bitwarden-core/src/client/internal.rs | 3 ++ .../src/key_management/crypto.rs | 48 ++++--------------- .../src/key_management/crypto_client.rs | 4 +- .../src/store/key_rotation.rs | 14 +++--- 6 files changed, 36 insertions(+), 51 deletions(-) diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index 6cd632eb4..a99346a78 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -104,6 +104,7 @@ impl Client { })), external_client, key_store: KeyStore::default(), + security_state: RwLock::new(Arc::new(None)), #[cfg(feature = "internal")] repository_map: StateRegistry::new(), }), diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 02992b8e7..cf3ce81b8 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -74,6 +74,7 @@ impl EncryptionSettings { pub(crate) fn new_decrypted_key( encryption_keys: AccountEncryptionKeys, store: &KeyStore, + security_state: &RwLock>, ) -> Result<(), EncryptionSettingsError> { // This is an all-or-nothing check. The server cannot pretend a signing key or security // state to be missing, because they are *always* present when the user key is an @@ -92,7 +93,14 @@ impl EncryptionSettings { signing_key, security_state, } => { - Self::init_v2(user_key, private_key, signing_key, security_state, store)?; + Self::init_v2( + user_key, + private_key, + signing_key, + security_state, + store, + security_state, + )?; } } @@ -144,6 +152,7 @@ impl EncryptionSettings { signing_key: EncString, security_state: SignedSecurityState, store: &KeyStore, + sdk_security_state: &RwLock>, ) -> Result<(), EncryptionSettingsError> { use crate::key_management::SecurityState; @@ -159,9 +168,13 @@ impl EncryptionSettings { .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; // This is not used further currently, but we still need to verify that it decrypts properly // and is valid. - let _security_state: SecurityState = security_state + let security_state: SecurityState = security_state .verify_and_unwrap(&signing_key.to_verifying_key()) .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; + sdk_security_state + .write() + .map_err(|_| EncryptionSettingsError::VaultLocked)? + .replace(Arc::new(security_state)); #[allow(deprecated)] { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 1e29b94d7..7c74cb3d5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -84,6 +84,7 @@ pub struct InternalClient { pub(crate) external_client: reqwest::Client, pub(super) key_store: KeyStore, + pub(crate) security_state: RwLock>>, #[cfg(feature = "internal")] pub(crate) repository_map: StateRegistry, @@ -248,6 +249,7 @@ impl InternalClient { private_key, }, &self.key_store, + &self.security_state, )?; } SymmetricCryptoKey::XChaCha20Poly1305Key(ref user_key) => { @@ -261,6 +263,7 @@ impl InternalClient { .ok_or(EncryptionSettingsError::InvalidSecurityState)?, }, &self.key_store, + &self.security_state, )?; } _ => { diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 4430ba4c5..095231bd6 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -589,7 +589,7 @@ pub(super) fn verify_asymmetric_keys( #[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 EnrollUserCryptoV2Response { +pub struct UserCryptoV2Response { /// User key user_key: String, @@ -616,7 +616,7 @@ pub struct EnrollUserCryptoV2Response { /// re-used. pub(crate) fn make_keys_for_user_crypto_v2( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); @@ -656,7 +656,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( ); let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; - Ok(EnrollUserCryptoV2Response { + Ok(UserCryptoV2Response { user_key: user_key.to_base64(), private_key: private_key.to_der()?.encrypt_with_key(&user_key)?, @@ -671,54 +671,22 @@ pub(crate) fn make_keys_for_user_crypto_v2( }) } -/// A rotated set of account keys for a user -#[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 RotateUserKeysResponse { - /// The verifying key - pub verifying_key: String, - /// Signing key, encrypted with a symmetric key (user key, org key) - pub signing_key: EncString, - /// The user's public key, signed by the signing key - pub signed_public_key: String, - /// The user's public key, without signature - pub public_key: String, - /// The user's private key, encrypted with the user key - pub private_key: EncString, -} - -impl From for RotateUserKeysResponse { - fn from(rotated: RotatedUserKeys) -> Self { - RotateUserKeysResponse { - verifying_key: STANDARD.encode(rotated.verifying_key.to_vec()), - signing_key: rotated.signing_key, - signed_public_key: STANDARD.encode(rotated.signed_public_key.to_vec()), - public_key: STANDARD.encode(rotated.public_key.to_vec()), - private_key: rotated.private_key, - } - } -} - /// 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 pub(crate) fn get_v2_rotated_account_keys( client: &Client, - user_key: String, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let ctx = key_store.context(); + let security_state = client.internal.security_state.read().unwrap().unwrap(); - dangerous_get_v2_rotated_account_keys( - &SymmetricCryptoKey::try_from(user_key)?, - AsymmetricKeyId::UserPrivateKey, + let rotated_keys = dangerous_get_v2_rotated_account_keys( + SymmetricKeyId::UserPrivateKey, SigningKeyId::UserSigningKey, &ctx, - ) - .map(Into::into) + ); } #[cfg(test)] diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 2cff61aa4..9e7dc196e 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -19,7 +19,7 @@ use crate::{ client::encryption_settings::EncryptionSettingsError, key_management::crypto::{ get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, - EnrollUserCryptoV2Response, RotateUserKeysResponse, + RotateUserKeysResponse, UserCryptoV2Response, }, Client, }; @@ -67,7 +67,7 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_keys_for_user_crypto_v2(&self) -> Result { + pub fn make_keys_for_user_crypto_v2(&self) -> Result { make_keys_for_user_crypto_v2(&self.client) } diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index a74edb2ec..8c1e32449 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -5,6 +5,8 @@ use crate::{ /// Rotated set of account keys pub struct RotatedUserKeys { + /// The user's user key + pub user_key: SymmetricCryptoKey, /// The verifying key pub verifying_key: CoseKeyBytes, /// Signing key, encrypted with a symmetric key (user key, org key) @@ -19,11 +21,12 @@ pub struct RotatedUserKeys { /// Re-encrypts the user's keys with the provided symmetric key for a v2 user. pub fn dangerous_get_v2_rotated_account_keys( - new_user_key: &SymmetricCryptoKey, current_user_private_key_id: Ids::Asymmetric, current_user_signing_key_id: Ids::Signing, ctx: &KeyStoreContext, ) -> Result { + let user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); + let current_private_key = ctx.get_asymmetric_key(current_user_private_key_id)?; let current_signing_key = ctx.get_signing_key(current_user_signing_key_id)?; @@ -31,15 +34,12 @@ pub fn dangerous_get_v2_rotated_account_keys( SignedPublicKeyMessage::from_public_key(¤t_private_key.to_public_key())? .sign(current_signing_key)?; Ok(RotatedUserKeys { + user_key: user_key.clone(), verifying_key: current_signing_key.to_verifying_key().to_cose(), - signing_key: current_signing_key - .to_cose() - .encrypt_with_key(new_user_key)?, + signing_key: current_signing_key.to_cose().encrypt_with_key(&user_key)?, signed_public_key: signed_public_key.into(), public_key: current_private_key.to_public_key().to_der()?, - private_key: current_private_key - .to_der()? - .encrypt_with_key(new_user_key)?, + private_key: current_private_key.to_der()?.encrypt_with_key(&user_key)?, }) } From d89a17d57ca1171cb19abed6a1e6e4e650b192f8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 9 Jul 2025 19:17:46 +0200 Subject: [PATCH 26/63] Simplify rotation usage, and store security state --- .../src/client/encryption_settings.rs | 19 ++++---- crates/bitwarden-core/src/client/internal.rs | 3 +- .../src/key_management/crypto.rs | 48 ++++++++++++++----- .../src/key_management/crypto_client.rs | 9 ++-- .../src/key_management/security_state.rs | 2 +- crates/bitwarden-crypto/src/error.rs | 12 ++++- crates/bitwarden-crypto/src/lib.rs | 2 +- crates/bitwarden-crypto/src/store/context.rs | 8 +--- .../src/store/key_rotation.rs | 10 ++-- 9 files changed, 70 insertions(+), 43 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index cf3ce81b8..a66a8662c 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "internal")] +use std::sync::{Arc, RwLock}; + #[cfg(feature = "internal")] use bitwarden_crypto::{ Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, EncString, @@ -13,7 +16,7 @@ use thiserror::Error; use uuid::Uuid; #[cfg(feature = "internal")] -use crate::key_management::{AsymmetricKeyId, SignedSecurityState, SigningKeyId}; +use crate::key_management::{AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId}; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; @@ -74,7 +77,7 @@ impl EncryptionSettings { pub(crate) fn new_decrypted_key( encryption_keys: AccountEncryptionKeys, store: &KeyStore, - security_state: &RwLock>, + security_state_rwlock: &RwLock>>, ) -> Result<(), EncryptionSettingsError> { // This is an all-or-nothing check. The server cannot pretend a signing key or security // state to be missing, because they are *always* present when the user key is an @@ -99,7 +102,7 @@ impl EncryptionSettings { signing_key, security_state, store, - security_state, + security_state_rwlock, )?; } } @@ -152,7 +155,7 @@ impl EncryptionSettings { signing_key: EncString, security_state: SignedSecurityState, store: &KeyStore, - sdk_security_state: &RwLock>, + sdk_security_state: &RwLock>>, ) -> Result<(), EncryptionSettingsError> { use crate::key_management::SecurityState; @@ -166,15 +169,11 @@ impl EncryptionSettings { let private_key: Vec = private_key.decrypt_with_key(&user_key)?; let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key)) .map_err(|_| EncryptionSettingsError::InvalidPrivateKey)?; - // This is not used further currently, but we still need to verify that it decrypts properly - // and is valid. + let security_state: SecurityState = security_state .verify_and_unwrap(&signing_key.to_verifying_key()) .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; - sdk_security_state - .write() - .map_err(|_| EncryptionSettingsError::VaultLocked)? - .replace(Arc::new(security_state)); + *sdk_security_state.write().expect("RwLock not poisoned") = Arc::new(Some(security_state)); #[allow(deprecated)] { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 7c74cb3d5..38f2d1cc4 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -14,6 +14,7 @@ use uuid::Uuid; use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use crate::client::login_method::ServiceAccountLoginMethod; +use crate::key_management::SecurityState; use crate::{ auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, key_management::KeyIds, DeviceType, @@ -84,7 +85,7 @@ pub struct InternalClient { pub(crate) external_client: reqwest::Client, pub(super) key_store: KeyStore, - pub(crate) security_state: RwLock>>, + pub(crate) security_state: RwLock>>, #[cfg(feature = "internal")] pub(crate) repository_map: StateRegistry, diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 095231bd6..cc1eaa874 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -9,8 +9,8 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, - EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, - RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, + CryptoStateError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, + Pkcs8PrivateKeyBytes, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; @@ -648,12 +648,9 @@ pub(crate) fn make_keys_for_user_crypto_v2( let public_key = private_key.to_public_key(); // Initialize security state for the user - let security_state = SecurityState::initialize_for_user( - client - .internal - .get_user_id() - .ok_or(CryptoError::UninitializedError)?, - ); + let security_state = SecurityState::initialize_for_user(client.internal.get_user_id().ok_or( + CryptoError::CryptoStateError(CryptoStateError::MissingSecurityState.into()), + )?); let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; Ok(UserCryptoV2Response { @@ -679,14 +676,41 @@ pub(crate) fn get_v2_rotated_account_keys( client: &Client, ) -> Result { let key_store = client.internal.get_key_store(); - let ctx = key_store.context(); - let security_state = client.internal.security_state.read().unwrap().unwrap(); + let mut ctx = key_store.context(); + + let signed_security_state = client + .internal + .security_state + .read() + .expect("RwLock is not poisoned") + .to_owned(); + let security_state = + signed_security_state + .as_ref() + .to_owned() + .ok_or(CryptoError::CryptoStateError( + CryptoStateError::MissingSecurityState, + ))?; let rotated_keys = dangerous_get_v2_rotated_account_keys( - SymmetricKeyId::UserPrivateKey, + AsymmetricKeyId::UserPrivateKey, SigningKeyId::UserSigningKey, &ctx, - ); + )?; + + Ok(UserCryptoV2Response { + user_key: rotated_keys.user_key.to_base64(), + + private_key: rotated_keys.private_key, + public_key: STANDARD.encode(rotated_keys.public_key), + signed_public_key: SignedPublicKey::from(rotated_keys.signed_public_key), + + signing_key: rotated_keys.signing_key, + verifying_key: STANDARD.encode(rotated_keys.verifying_key), + + security_state: security_state.sign(SigningKeyId::UserSigningKey, &mut ctx)?, + security_version: security_state.version(), + }) } #[cfg(test)] diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 9e7dc196e..0e96ebc99 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -19,7 +19,7 @@ use crate::{ client::encryption_settings::EncryptionSettingsError, key_management::crypto::{ get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, - RotateUserKeysResponse, UserCryptoV2Response, + UserCryptoV2Response, }, Client, }; @@ -72,11 +72,8 @@ impl CryptoClient { } /// Creates a rotated set of account keys for the current state - pub fn get_v2_rotated_account_keys( - &self, - user_key: String, - ) -> Result { - get_v2_rotated_account_keys(&self.client, user_key) + pub fn get_v2_rotated_account_keys(&self) -> Result { + get_v2_rotated_account_keys(&self.client) } } diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs index 62e3cbabd..6f620212b 100644 --- a/crates/bitwarden-core/src/key_management/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -41,7 +41,7 @@ export type SignedSecurityState = string; /// It contains a version, which can only ever increment. Based on the version, old formats and /// features are blocked. This prevents a server from downgrading a user's account features, because /// only the user can create this signed object. -#[derive(Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SecurityState { /// The entity ID is a permanent, unchangeable, unique identifier for the object this security diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 640dfcbb5..2dc3c3230 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -71,7 +71,7 @@ pub enum CryptoError { EncodingError(#[from] EncodingError), #[error("Uninitialized error")] - UninitializedError, + CryptoStateError(CryptoStateError), } #[derive(Debug, Error)] @@ -80,6 +80,16 @@ pub enum UnsupportedOperation { EncryptionNotImplementedForKey, } +/// Signifies that the state is invalid from a cryptographic perspective, such as a required security value missing, or being +/// invalid +#[derive(Debug, Error)] +pub enum CryptoStateError { + /// The security state is not present, but required for this user. V2 users must always + /// have a security state, V1 users cannot have a security state. + #[error("Security state is not set to state")] + MissingSecurityState, +} + #[derive(Debug, Error)] pub enum EncStringParseError { #[error("No type detected, missing '.' separator")] diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index e5e86891b..801a9f7ab 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -19,7 +19,7 @@ mod enc_string; pub use enc_string::{EncString, UnsignedSharedKey}; mod error; pub(crate) use error::Result; -pub use error::{CryptoError, EncodingError}; +pub use error::{CryptoError, CryptoStateError, EncodingError}; mod fingerprint; pub use fingerprint::fingerprint; mod keys; diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 89e65cb80..3feb82b5b 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -528,12 +528,10 @@ impl KeyStoreContext<'_, Ids> { /// Re-encrypts the user's keys with the provided symmetric key for a v2 user. pub fn dangerous_get_v2_rotated_account_keys( &self, - new_user_key: &SymmetricCryptoKey, current_user_private_key_id: Ids::Asymmetric, current_user_signing_key_id: Ids::Signing, ) -> Result { crate::dangerous_get_v2_rotated_account_keys( - new_user_key, current_user_private_key_id, current_user_signing_key_id, self, @@ -752,7 +750,6 @@ mod tests { let mut ctx = store.context_mut(); // Generate a new user key - let new_user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); let current_user_private_key_id = TestAsymmKey::A(0); let current_user_signing_key_id = TestSigningKey::A(0); @@ -768,7 +765,6 @@ mod tests { // Get the rotated account keys let rotated_keys = ctx .dangerous_get_v2_rotated_account_keys( - &new_user_key, current_user_private_key_id, current_user_signing_key_id, ) @@ -788,7 +784,7 @@ mod tests { ); let decrypted_private_key: Vec = rotated_keys .private_key - .decrypt_with_key(&new_user_key) + .decrypt_with_key(&rotated_keys.user_key) .unwrap(); let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key)) @@ -804,7 +800,7 @@ mod tests { // Signing Key let decrypted_signing_key: Vec = rotated_keys .signing_key - .decrypt_with_key(&new_user_key) + .decrypt_with_key(&rotated_keys.user_key) .unwrap(); let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(decrypted_signing_key)).unwrap(); diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 8c1e32449..88a953702 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -1,6 +1,7 @@ use crate::{ - CoseKeyBytes, CoseSerializable, CoseSign1Bytes, CryptoError, EncString, KeyEncryptable, KeyIds, - KeyStoreContext, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey, + CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyIds, + KeyStoreContext, SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, + SymmetricCryptoKey, }; /// Rotated set of account keys @@ -12,7 +13,7 @@ pub struct RotatedUserKeys { /// Signing key, encrypted with a symmetric key (user key, org key) pub signing_key: EncString, /// The user's public key, signed by the signing key - pub signed_public_key: CoseSign1Bytes, + pub signed_public_key: SignedPublicKey, /// The user's public key, without signature pub public_key: SpkiPublicKeyBytes, /// The user's private key, encrypted with the user key @@ -37,7 +38,7 @@ pub fn dangerous_get_v2_rotated_account_keys( user_key: user_key.clone(), verifying_key: current_signing_key.to_verifying_key().to_cose(), signing_key: current_signing_key.to_cose().encrypt_with_key(&user_key)?, - signed_public_key: signed_public_key.into(), + signed_public_key: signed_public_key, public_key: current_private_key.to_public_key().to_der()?, private_key: current_private_key.to_der()?.encrypt_with_key(&user_key)?, }) @@ -74,7 +75,6 @@ mod tests { // Get the rotated account keys let rotated_keys = dangerous_get_v2_rotated_account_keys( - &new_user_key, current_user_private_key_id, current_user_signing_key_id, &ctx, From 80fe0b72de2690feeddcc78a4e7ef5925ae4f0ca Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 13:45:46 +0200 Subject: [PATCH 27/63] Fix build --- crates/bitwarden-core/src/client/internal.rs | 9 ++++++--- crates/bitwarden-crypto/src/error.rs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 38f2d1cc4..3a2e96e6b 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -14,10 +14,12 @@ use uuid::Uuid; use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use crate::client::login_method::ServiceAccountLoginMethod; -use crate::key_management::SecurityState; use crate::{ - auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, - key_management::KeyIds, DeviceType, + auth::renew::renew_token, + client::login_method::LoginMethod, + error::UserIdAlreadySetError, + key_management::{KeyIds, SecurityState}, + DeviceType, }; #[cfg(feature = "internal")] use crate::{ @@ -85,6 +87,7 @@ pub struct InternalClient { pub(crate) external_client: reqwest::Client, pub(super) key_store: KeyStore, + #[cfg(feature = "internal")] pub(crate) security_state: RwLock>>, #[cfg(feature = "internal")] diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 2dc3c3230..974dfff1c 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -80,8 +80,8 @@ pub enum UnsupportedOperation { EncryptionNotImplementedForKey, } -/// Signifies that the state is invalid from a cryptographic perspective, such as a required security value missing, or being -/// invalid +/// Signifies that the state is invalid from a cryptographic perspective, such as a required +/// security value missing, or being invalid #[derive(Debug, Error)] pub enum CryptoStateError { /// The security state is not present, but required for this user. V2 users must always From 3885e36065d4b3a72ad175da08f3346d716d83e4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 13:47:25 +0200 Subject: [PATCH 28/63] Fix sm build --- crates/bitwarden-core/src/client/client.rs | 1 + crates/bitwarden-core/src/client/internal.rs | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index a99346a78..ce667941f 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -104,6 +104,7 @@ impl Client { })), external_client, key_store: KeyStore::default(), + #[cfg(feature = "internal")] security_state: RwLock::new(Arc::new(None)), #[cfg(feature = "internal")] repository_map: StateRegistry::new(), diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 3a2e96e6b..a3cef4710 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -14,12 +14,11 @@ use uuid::Uuid; use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use crate::client::login_method::ServiceAccountLoginMethod; +#[cfg(feature = "internal")] +use crate::key_management::SecurityState; use crate::{ - auth::renew::renew_token, - client::login_method::LoginMethod, - error::UserIdAlreadySetError, - key_management::{KeyIds, SecurityState}, - DeviceType, + auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, + key_management::KeyIds, DeviceType, }; #[cfg(feature = "internal")] use crate::{ From e6816f241cd2ccaf8220ba58cd901755dcce0d7f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 13:53:53 +0200 Subject: [PATCH 29/63] Apply clippy fixes --- crates/bitwarden-core/src/key_management/crypto.rs | 4 ++-- crates/bitwarden-crypto/src/store/key_rotation.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index cc1eaa874..136cdd5ce 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -649,7 +649,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( // Initialize security state for the user let security_state = SecurityState::initialize_for_user(client.internal.get_user_id().ok_or( - CryptoError::CryptoStateError(CryptoStateError::MissingSecurityState.into()), + CryptoError::CryptoStateError(CryptoStateError::MissingSecurityState), )?); let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; @@ -703,7 +703,7 @@ pub(crate) fn get_v2_rotated_account_keys( private_key: rotated_keys.private_key, public_key: STANDARD.encode(rotated_keys.public_key), - signed_public_key: SignedPublicKey::from(rotated_keys.signed_public_key), + signed_public_key: rotated_keys.signed_public_key, signing_key: rotated_keys.signing_key, verifying_key: STANDARD.encode(rotated_keys.verifying_key), diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 88a953702..51468704a 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -38,7 +38,7 @@ pub fn dangerous_get_v2_rotated_account_keys( user_key: user_key.clone(), verifying_key: current_signing_key.to_verifying_key().to_cose(), signing_key: current_signing_key.to_cose().encrypt_with_key(&user_key)?, - signed_public_key: signed_public_key, + signed_public_key, public_key: current_private_key.to_public_key().to_der()?, private_key: current_private_key.to_der()?.encrypt_with_key(&user_key)?, }) From 96548c24fbce5484c35fefe327444d36029c6815 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 13:55:07 +0200 Subject: [PATCH 30/63] Apply clippy fixes --- crates/bitwarden-crypto/src/store/context.rs | 6 +++--- crates/bitwarden-crypto/src/store/key_rotation.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 3feb82b5b..312cc30ab 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -552,8 +552,8 @@ mod tests { traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CompositeEncryptable, CoseKeyBytes, CoseSerializable, CryptoError, Decryptable, KeyDecryptable, Pkcs8PrivateKeyBytes, - PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SigningKey, - SigningNamespace, SymmetricCryptoKey, + PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SigningKey, SigningNamespace, + SymmetricCryptoKey, }; #[test] @@ -812,7 +812,7 @@ mod tests { ); // Signed Public Key - let signed_public_key = SignedPublicKey::try_from(rotated_keys.signed_public_key).unwrap(); + let signed_public_key = rotated_keys.signed_public_key; let unwrapped_key = signed_public_key .verify_and_unwrap( &ctx.get_signing_key(current_user_signing_key_id) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 51468704a..ebbe33af7 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -50,7 +50,7 @@ mod tests { use crate::{ traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, - PublicKeyEncryptionAlgorithm, SignedPublicKey, SigningKey, + PublicKeyEncryptionAlgorithm, SigningKey, }; #[test] @@ -120,7 +120,7 @@ mod tests { ); // Signed Public Key - let signed_public_key = SignedPublicKey::try_from(rotated_keys.signed_public_key).unwrap(); + let signed_public_key = rotated_keys.signed_public_key; let unwrapped_key = signed_public_key .verify_and_unwrap( &ctx.get_signing_key(current_user_signing_key_id) From 620d3c26e348b5ea9facd11ce4a2b2156a0732ca Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 14:06:21 +0200 Subject: [PATCH 31/63] Fix test --- crates/bitwarden-crypto/src/store/key_rotation.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index ebbe33af7..5e5e39bae 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -49,7 +49,7 @@ mod tests { use super::*; use crate::{ traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, - AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, + AsymmetricCryptoKey, Decryptable, KeyStore, Pkcs8PrivateKeyBytes, PublicKeyEncryptionAlgorithm, SigningKey, }; @@ -59,7 +59,6 @@ mod tests { let mut ctx = store.context_mut(); // Generate a new user key - let new_user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); let current_user_private_key_id = TestAsymmKey::A(0); let current_user_signing_key_id = TestSigningKey::A(0); @@ -92,7 +91,7 @@ mod tests { ); let decrypted_private_key: Vec = rotated_keys .private_key - .decrypt_with_key(&new_user_key) + .decrypt(&mut ctx, TestSymmKey::A(0)) .unwrap(); let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key)) @@ -108,7 +107,7 @@ mod tests { // Signing Key let decrypted_signing_key: Vec = rotated_keys .signing_key - .decrypt_with_key(&new_user_key) + .decrypt(&mut ctx, TestSymmKey::A(0)) .unwrap(); let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(decrypted_signing_key)).unwrap(); From 959b2e7232148f30855d1b4585a151ce77af4212 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 14:13:23 +0200 Subject: [PATCH 32/63] Fix tests --- crates/bitwarden-crypto/src/store/key_rotation.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 5e5e39bae..e0fc76e1f 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -49,7 +49,7 @@ mod tests { use super::*; use crate::{ traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, - AsymmetricCryptoKey, Decryptable, KeyStore, Pkcs8PrivateKeyBytes, + AsymmetricCryptoKey, Decryptable, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, PublicKeyEncryptionAlgorithm, SigningKey, }; @@ -91,7 +91,7 @@ mod tests { ); let decrypted_private_key: Vec = rotated_keys .private_key - .decrypt(&mut ctx, TestSymmKey::A(0)) + .decrypt_with_key(&rotated_keys.user_key) .unwrap(); let private_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decrypted_private_key)) @@ -107,7 +107,7 @@ mod tests { // Signing Key let decrypted_signing_key: Vec = rotated_keys .signing_key - .decrypt(&mut ctx, TestSymmKey::A(0)) + .decrypt_with_key(&rotated_keys.user_key) .unwrap(); let signing_key = SigningKey::from_cose(&CoseKeyBytes::from(decrypted_signing_key)).unwrap(); From bf269f61e56f67b51271244a7cb2a56f941b02d8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 14:15:56 +0200 Subject: [PATCH 33/63] Remove unused import --- crates/bitwarden-crypto/src/store/key_rotation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index e0fc76e1f..ea58d6a68 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -49,7 +49,7 @@ mod tests { use super::*; use crate::{ traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, - AsymmetricCryptoKey, Decryptable, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, + AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, PublicKeyEncryptionAlgorithm, SigningKey, }; From 5902ea2ee19274f47f8a1ba04f10a314534aadee Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 10 Jul 2025 14:41:20 +0200 Subject: [PATCH 34/63] Rename to "UserCryptoV2KeysResponse" --- crates/bitwarden-core/src/key_management/crypto.rs | 10 +++++----- .../bitwarden-core/src/key_management/crypto_client.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 136cdd5ce..10186e435 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -589,7 +589,7 @@ pub(super) fn verify_asymmetric_keys( #[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 UserCryptoV2Response { +pub struct UserCryptoV2KeysResponse { /// User key user_key: String, @@ -616,7 +616,7 @@ pub struct UserCryptoV2Response { /// re-used. pub(crate) fn make_keys_for_user_crypto_v2( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); @@ -653,7 +653,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( )?); let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; - Ok(UserCryptoV2Response { + Ok(UserCryptoV2KeysResponse { user_key: user_key.to_base64(), private_key: private_key.to_der()?.encrypt_with_key(&user_key)?, @@ -674,7 +674,7 @@ pub(crate) fn make_keys_for_user_crypto_v2( /// user to be a v2 user; that is, they have a signing key, a cose user-key, and a private key pub(crate) fn get_v2_rotated_account_keys( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); @@ -698,7 +698,7 @@ pub(crate) fn get_v2_rotated_account_keys( &ctx, )?; - Ok(UserCryptoV2Response { + Ok(UserCryptoV2KeysResponse { user_key: rotated_keys.user_key.to_base64(), private_key: rotated_keys.private_key, diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 0e96ebc99..fd7faf31b 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -19,7 +19,7 @@ use crate::{ client::encryption_settings::EncryptionSettingsError, key_management::crypto::{ get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, - UserCryptoV2Response, + UserCryptoV2KeysResponse, }, Client, }; @@ -67,12 +67,12 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_keys_for_user_crypto_v2(&self) -> Result { + pub fn make_keys_for_user_crypto_v2(&self) -> Result { make_keys_for_user_crypto_v2(&self.client) } /// Creates a rotated set of account keys for the current state - pub fn get_v2_rotated_account_keys(&self) -> Result { + pub fn get_v2_rotated_account_keys(&self) -> Result { get_v2_rotated_account_keys(&self.client) } } From cd8077c9735326005a9bae9ddab88c1fce966bcd Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:35:28 +0200 Subject: [PATCH 35/63] Update crates/bitwarden-core/src/key_management/mod.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel García --- crates/bitwarden-core/src/key_management/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 0305fd970..10be377a8 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,7 +18,9 @@ mod crypto_client; #[cfg(feature = "internal")] pub use crypto_client::CryptoClient; +#[cfg(feature = "internal")] mod security_state; +#[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; key_ids! { From e063963daf1b09aad574c2a05105a074c39a7134 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:38:05 +0200 Subject: [PATCH 36/63] Fix renames --- crates/bitwarden-crypto/src/error.rs | 6 +++--- crates/bitwarden-ipc/src/crypto_provider/mod.rs | 0 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 crates/bitwarden-ipc/src/crypto_provider/mod.rs diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 974dfff1c..1ad38ef55 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -70,8 +70,8 @@ pub enum CryptoError { #[error("Encoding error, {0}")] EncodingError(#[from] EncodingError), - #[error("Uninitialized error")] - CryptoStateError(CryptoStateError), + #[error("Crypto state error, {0}")] + CryptoStateError(#[from] CryptoStateError), } #[derive(Debug, Error)] @@ -86,7 +86,7 @@ pub enum UnsupportedOperation { pub enum CryptoStateError { /// The security state is not present, but required for this user. V2 users must always /// have a security state, V1 users cannot have a security state. - #[error("Security state is not set to state")] + #[error("Security state is required, but missing")] MissingSecurityState, } diff --git a/crates/bitwarden-ipc/src/crypto_provider/mod.rs b/crates/bitwarden-ipc/src/crypto_provider/mod.rs new file mode 100644 index 000000000..e69de29bb From a0ee4d71dad009bdf23534493d2d971a3ffbaa29 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:40:54 +0200 Subject: [PATCH 37/63] Add error for base64 deserialization --- crates/bitwarden-core/src/key_management/security_state.rs | 2 +- crates/bitwarden-crypto/src/error.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs index 6f620212b..c8b18675d 100644 --- a/crates/bitwarden-core/src/key_management/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -123,7 +123,7 @@ impl FromStr for SignedSecurityState { fn from_str(s: &str) -> Result { let bytes = STANDARD .decode(s) - .map_err(|_| EncodingError::InvalidCborSerialization)?; + .map_err(|_| EncodingError::InvalidBase64Encoding)?; Self::try_from(&CoseSign1Bytes::from(bytes)) } } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 1ad38ef55..4a802481e 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -137,6 +137,9 @@ pub enum EncodingError { /// An error occurred while serializing or deserializing a value using CBOR #[error("Cbor serialization error")] InvalidCborSerialization, + /// An error occurred while serializing or deserializing a value using Base64 + #[error("Invalid base64 encoding")] + InvalidBase64Encoding, /// A required value is missing from the serialized message #[error("Missing value {0}")] MissingValue(&'static str), From d10af3d86f5fa8400a23e2247a547458087c892a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:43:53 +0200 Subject: [PATCH 38/63] Clean up key rotation --- crates/bitwarden-crypto/src/store/key_rotation.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index ea58d6a68..17db55302 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -31,16 +31,17 @@ pub fn dangerous_get_v2_rotated_account_keys( let current_private_key = ctx.get_asymmetric_key(current_user_private_key_id)?; let current_signing_key = ctx.get_signing_key(current_user_signing_key_id)?; + let current_public_key = ¤t_private_key.to_public_key(); let signed_public_key = - SignedPublicKeyMessage::from_public_key(¤t_private_key.to_public_key())? - .sign(current_signing_key)?; + SignedPublicKeyMessage::from_public_key(current_public_key)?.sign(current_signing_key)?; + Ok(RotatedUserKeys { - user_key: user_key.clone(), verifying_key: current_signing_key.to_verifying_key().to_cose(), signing_key: current_signing_key.to_cose().encrypt_with_key(&user_key)?, signed_public_key, - public_key: current_private_key.to_public_key().to_der()?, + public_key: current_public_key.to_der()?, private_key: current_private_key.to_der()?.encrypt_with_key(&user_key)?, + user_key, }) } From 5d6ae55fba517fe9b38e46cf04627fee5bd9d03c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:44:56 +0200 Subject: [PATCH 39/63] Cleanup --- crates/bitwarden-core/src/key_management/crypto.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 10186e435..e0885349c 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -683,14 +683,10 @@ pub(crate) fn get_v2_rotated_account_keys( .security_state .read() .expect("RwLock is not poisoned") - .to_owned(); - let security_state = - signed_security_state - .as_ref() - .to_owned() - .ok_or(CryptoError::CryptoStateError( - CryptoStateError::MissingSecurityState, - ))?; + .to_owned() + .ok_or(CryptoError::CryptoStateError( + CryptoStateError::MissingSecurityState, + ))?; let rotated_keys = dangerous_get_v2_rotated_account_keys( AsymmetricKeyId::UserPrivateKey, From 16c6248c4aa19523da41a862a2bd9699fd6a57f4 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:46:08 +0200 Subject: [PATCH 40/63] Cleanup get_v2_rotated_account_keys --- crates/bitwarden-core/src/key_management/crypto.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index e0885349c..2bb217673 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -678,11 +678,12 @@ pub(crate) fn get_v2_rotated_account_keys( let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); - let signed_security_state = client + let security_state = client .internal .security_state .read() .expect("RwLock is not poisoned") + .as_ref() .to_owned() .ok_or(CryptoError::CryptoStateError( CryptoStateError::MissingSecurityState, From d7dea458905b92cd9fa07ac383b41e47a20b6ed6 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:48:35 +0200 Subject: [PATCH 41/63] Remove arc --- crates/bitwarden-core/src/client/encryption_settings.rs | 6 +++--- crates/bitwarden-core/src/client/internal.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index a66a8662c..7cb77d192 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -77,7 +77,7 @@ impl EncryptionSettings { pub(crate) fn new_decrypted_key( encryption_keys: AccountEncryptionKeys, store: &KeyStore, - security_state_rwlock: &RwLock>>, + security_state_rwlock: &RwLock>, ) -> Result<(), EncryptionSettingsError> { // This is an all-or-nothing check. The server cannot pretend a signing key or security // state to be missing, because they are *always* present when the user key is an @@ -155,7 +155,7 @@ impl EncryptionSettings { signing_key: EncString, security_state: SignedSecurityState, store: &KeyStore, - sdk_security_state: &RwLock>>, + sdk_security_state: &RwLock>, ) -> Result<(), EncryptionSettingsError> { use crate::key_management::SecurityState; @@ -173,7 +173,7 @@ impl EncryptionSettings { let security_state: SecurityState = security_state .verify_and_unwrap(&signing_key.to_verifying_key()) .map_err(|_| EncryptionSettingsError::InvalidSecurityState)?; - *sdk_security_state.write().expect("RwLock not poisoned") = Arc::new(Some(security_state)); + *sdk_security_state.write().expect("RwLock not poisoned") = Some(security_state); #[allow(deprecated)] { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a3cef4710..8ba76a0bc 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -87,7 +87,7 @@ pub struct InternalClient { pub(super) key_store: KeyStore, #[cfg(feature = "internal")] - pub(crate) security_state: RwLock>>, + pub(crate) security_state: RwLock>, #[cfg(feature = "internal")] pub(crate) repository_map: StateRegistry, From 8a00fff7c68363dce493d0c9fc10797e36c76027 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 11:50:41 +0200 Subject: [PATCH 42/63] Remove remaining arc references --- crates/bitwarden-core/src/client/client.rs | 2 +- crates/bitwarden-core/src/client/encryption_settings.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index ce667941f..4a2db6a80 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -105,7 +105,7 @@ impl Client { external_client, key_store: KeyStore::default(), #[cfg(feature = "internal")] - security_state: RwLock::new(Arc::new(None)), + security_state: RwLock::new(None), #[cfg(feature = "internal")] repository_map: StateRegistry::new(), }), diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 7cb77d192..64e692fcf 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,5 +1,5 @@ #[cfg(feature = "internal")] -use std::sync::{Arc, RwLock}; +use std::sync::RwLock; #[cfg(feature = "internal")] use bitwarden_crypto::{ From 4f2b05acfc5f600274a6acd35a4bb46cfefceaaf Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 12:01:40 +0200 Subject: [PATCH 43/63] Fix build --- crates/bitwarden-core/src/key_management/crypto.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 2bb217673..0b93ce9dc 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -683,7 +683,6 @@ pub(crate) fn get_v2_rotated_account_keys( .security_state .read() .expect("RwLock is not poisoned") - .as_ref() .to_owned() .ok_or(CryptoError::CryptoStateError( CryptoStateError::MissingSecurityState, From f76daa8f1bfc8e3adc72be17765b8e3b287ca3ec Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 17:52:49 +0200 Subject: [PATCH 44/63] Re-sort impl after definition --- .../bitwarden-core/src/key_management/security_state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs index c8b18675d..98313e236 100644 --- a/crates/bitwarden-core/src/key_management/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -82,6 +82,10 @@ impl SecurityState { } } +/// A signed and serialized `SecurityState` object. +#[derive(Clone, Debug)] +pub struct SignedSecurityState(pub(crate) SignedObject); + impl SignedSecurityState { /// Verifies the signature of the `SignedSecurityState` using the provided `VerifyingKey`. pub fn verify_and_unwrap( @@ -93,10 +97,6 @@ impl SignedSecurityState { } } -/// A signed and serialized `SecurityState` object. -#[derive(Clone, Debug)] -pub struct SignedSecurityState(pub(crate) SignedObject); - impl From for CoseSign1Bytes { fn from(val: SignedSecurityState) -> Self { val.0.to_cose() From 9e44be8b8ab3820ca332ab511e08db4978ce4603 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 17:58:17 +0200 Subject: [PATCH 45/63] Update comment --- crates/bitwarden-crypto/src/store/key_rotation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 17db55302..60509c69b 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -20,7 +20,7 @@ pub struct RotatedUserKeys { pub private_key: EncString, } -/// Re-encrypts the user's keys with the provided symmetric key for a v2 user. +/// Generates a new user key and re-encrypts the current private and signing keys with it. pub fn dangerous_get_v2_rotated_account_keys( current_user_private_key_id: Ids::Asymmetric, current_user_signing_key_id: Ids::Signing, From 2c64b9c7a54c5a587a5a1649b96fc68f5c205d81 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 18:13:45 +0200 Subject: [PATCH 46/63] Remove json schema --- crates/bitwarden-core/src/key_management/crypto.rs | 2 +- .../src/key_management/security_state.rs | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 0b93ce9dc..80ed644d6 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -585,7 +585,7 @@ pub(super) fn verify_asymmetric_keys( } /// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[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))] diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs index 98313e236..80b0fd562 100644 --- a/crates/bitwarden-core/src/key_management/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -147,16 +147,6 @@ impl serde::Serialize for SignedSecurityState { } } -impl schemars::JsonSchema for SignedSecurityState { - fn schema_name() -> String { - "SecurityState".to_string() - } - - fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { - generator.subschema_for::() - } -} - #[cfg(test)] mod tests { use bitwarden_crypto::{KeyStore, SignatureAlgorithm, SigningKey}; From 35821d9d988866c9e5c44b10ad91017783261fff Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 18:16:31 +0200 Subject: [PATCH 47/63] Clarify comment --- crates/bitwarden-core/src/key_management/crypto.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 80ed644d6..b04309ea0 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -611,7 +611,8 @@ pub struct UserCryptoV2KeysResponse { security_version: u64, } -/// Initializes the user's cryptographic state for v2 users. +/// Creates the user's cryptographic state for v2 users. This includes ensuring signature keypair is present, +/// a signed public key is present, a security state is present and signed, and the user key is a Cose key. /// If the client already contains a v1 user, then this user's private-key will be /// re-used. pub(crate) fn make_keys_for_user_crypto_v2( From d01b953b39801b0242466e3cbee762ce17d0088e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 11 Jul 2025 18:39:19 +0200 Subject: [PATCH 48/63] Add test for v1 user trying to rotate using v2 rotation method --- .../src/key_management/crypto.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index b04309ea0..c3a5cd7fd 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -1128,4 +1128,27 @@ mod tests { assert!(response.private_key_decryptable); assert!(!response.valid_private_key); } + + #[test] + fn test_get_v2_rotated_account_keys_non_v2_user() { + let client = Client::new(None); + #[allow(deprecated)] + client + .internal + .get_key_store() + .context_mut() + .set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ) + .unwrap(); + + let result = get_v2_rotated_account_keys(&client); + assert!(matches!( + result, + Err(CryptoError::CryptoStateError( + CryptoStateError::MissingSecurityState + )) + )); + } } From e3dea12f963595d18c036b5790df60a174c96489 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 10:59:50 +0200 Subject: [PATCH 49/63] Cargo fmt --- crates/bitwarden-core/src/key_management/crypto.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index c3a5cd7fd..fc901fff5 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -611,10 +611,10 @@ pub struct UserCryptoV2KeysResponse { security_version: u64, } -/// Creates the user's cryptographic state for v2 users. This includes ensuring signature keypair is present, -/// a signed public key is present, a security state is present and signed, and the user key is a Cose key. -/// If the client already contains a v1 user, then this user's private-key will be -/// re-used. +/// Creates the user's cryptographic state for v2 users. This includes ensuring signature keypair is +/// present, a signed public key is present, a security state is present and signed, and the user +/// key is a Cose key. If the client already contains a v1 user, then this user's private-key will +/// be re-used. pub(crate) fn make_keys_for_user_crypto_v2( client: &Client, ) -> Result { From da0a0d8a41af007892620b1a19769c1891d98dfe Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 11:29:46 +0200 Subject: [PATCH 50/63] Extract individual user crypto state values into UserKeyState struct --- .../bitwarden-core/src/auth/auth_request.rs | 27 +++++-- .../bitwarden-core/src/auth/login/api_key.rs | 10 ++- .../bitwarden-core/src/auth/login/password.rs | 10 ++- .../src/auth/password/validate.rs | 27 ++++--- crates/bitwarden-core/src/auth/pin.rs | 10 ++- crates/bitwarden-core/src/auth/tde.rs | 17 ++-- crates/bitwarden-core/src/client/internal.rs | 43 +++++----- .../src/key_management/crypto.rs | 78 ++++++++++--------- .../src/platform/generate_fingerprint.rs | 10 ++- 9 files changed, 132 insertions(+), 100 deletions(-) diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 719cc0258..1ee0576c9 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -115,9 +115,12 @@ mod tests { use bitwarden_crypto::{BitwardenLegacyKeyBytes, Kdf, MasterKey, SpkiPublicKeyBytes}; use super::*; - use crate::key_management::{ - crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, - SymmetricKeyId, + use crate::{ + client::internal::UserKeyState, + key_management::{ + crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + SymmetricKeyId, + }, }; #[test] @@ -164,7 +167,15 @@ mod tests { 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_key(master_key, user_key, private_key, None, None) + .initialize_user_crypto_master_key( + master_key, + user_key, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, + ) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; @@ -235,9 +246,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key, - private_key.clone(), - None, - None, + UserKeyState { + private_key: private_key.clone(), + signing_key: None, + security_state: None, + }, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index c6d4bae73..408347485 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -8,7 +8,7 @@ use crate::{ login::{response::two_factor::TwoFactorProviders, LoginError, PasswordLoginResponse}, JwtToken, }, - client::{LoginMethod, UserLoginMethod}, + client::{internal::UserKeyState, LoginMethod, UserLoginMethod}, require, Client, }; @@ -54,9 +54,11 @@ pub(crate) async fn login_api_key( client.internal.initialize_user_crypto_master_key( master_key, user_key, - private_key, - None, - None, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, )?; } diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 450564c2e..772ede819 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -34,6 +34,8 @@ pub(crate) async fn login_password( let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { + use crate::client::internal::UserKeyState; + client.internal.set_tokens( r.access_token.clone(), r.refresh_token.clone(), @@ -53,9 +55,11 @@ pub(crate) async fn login_password( client.internal.initialize_user_crypto_master_key( master_key, user_key, - private_key, - None, - None, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, )?; } diff --git a/crates/bitwarden-core/src/auth/password/validate.rs b/crates/bitwarden-core/src/auth/password/validate.rs index c10906fa4..d8c9a9ac9 100644 --- a/crates/bitwarden-core/src/auth/password/validate.rs +++ b/crates/bitwarden-core/src/auth/password/validate.rs @@ -80,9 +80,12 @@ pub(crate) fn validate_password_user_key( #[cfg(test)] mod tests { - use bitwarden_crypto::Kdf; + use bitwarden_crypto::{EncString, Kdf}; - use crate::auth::password::{validate::validate_password_user_key, validate_password}; + use crate::{ + auth::password::{validate::validate_password_user_key, validate_password}, + client::internal::UserKeyState, + }; #[test] fn test_validate_password() { @@ -135,17 +138,19 @@ mod tests { let master_key = MasterKey::derive(password, email, &kdf).unwrap(); - let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + let user_key: EncString = "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_key( master_key, - user_key.parse().unwrap(), - private_key, - None, - None, + user_key.clone(), + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, ) .unwrap(); @@ -192,9 +197,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key, - None, - None, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/pin.rs b/crates/bitwarden-core/src/auth/pin.rs index 91ae64c9b..63e490d59 100644 --- a/crates/bitwarden-core/src/auth/pin.rs +++ b/crates/bitwarden-core/src/auth/pin.rs @@ -49,7 +49,7 @@ mod tests { use bitwarden_crypto::{Kdf, MasterKey}; use super::*; - use crate::client::{Client, LoginMethod, UserLoginMethod}; + use crate::client::{internal::UserKeyState, Client, LoginMethod, UserLoginMethod}; fn init_client() -> Client { let client = Client::new(None); @@ -78,9 +78,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key, - None, - None, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, ) .unwrap(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 1f8f9dd2d..1c43d8e38 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -4,7 +4,10 @@ use bitwarden_crypto::{ TrustDeviceResponse, UnsignedSharedKey, UserKey, }; -use crate::{client::encryption_settings::EncryptionSettingsError, Client}; +use crate::{ + client::{encryption_settings::EncryptionSettingsError, internal::UserKeyState}, + Client, +}; /// This function generates a new user key and key pair, initializes the client's crypto with the /// generated user key, and encrypts the user key with the organization public key for admin @@ -41,11 +44,13 @@ pub(super) fn make_register_tde_keys( )); client.internal.initialize_user_crypto_decrypted_key( user_key.0, - key_pair.private.clone(), - // Note: Signing keys are not supported on registration yet. This needs to be changed as - // soon as registration is supported. - None, - None, + UserKeyState { + private_key: key_pair.private.clone(), + // Note: Signing keys are not supported on registration yet. This needs to be changed as + // soon as registration is supported. + signing_key: None, + security_state: None, + }, )?; Ok(RegisterTdeKeyResponse { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 8ba76a0bc..a18e78b5c 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -31,6 +31,13 @@ use crate::{ error::NotAuthenticatedError, }; +/// Represents the user's keys, that are encrypted by the user key, and the signed security state. +pub(crate) struct UserKeyState { + pub(crate) private_key: EncString, + pub(crate) signing_key: Option, + pub(crate) security_state: Option, +} + #[allow(missing_docs)] #[derive(Debug, Clone)] pub struct ApiConfigurations { @@ -223,33 +230,24 @@ impl InternalClient { &self, master_key: MasterKey, user_key: EncString, - private_key: EncString, - signing_key: Option, - security_state: Option, + key_state: UserKeyState, ) -> Result<(), EncryptionSettingsError> { let user_key = master_key.decrypt_user_key(user_key)?; - self.initialize_user_crypto_decrypted_key( - user_key, - private_key, - signing_key, - security_state, - ) + self.initialize_user_crypto_decrypted_key(user_key, key_state) } #[cfg(feature = "internal")] pub(crate) fn initialize_user_crypto_decrypted_key( &self, user_key: SymmetricCryptoKey, - private_key: EncString, - signing_key: Option, - security_state: Option, + key_state: UserKeyState, ) -> Result<(), EncryptionSettingsError> { match user_key { SymmetricCryptoKey::Aes256CbcHmacKey(ref user_key) => { EncryptionSettings::new_decrypted_key( AccountEncryptionKeys::V1 { user_key: user_key.clone(), - private_key, + private_key: key_state.private_key, }, &self.key_store, &self.security_state, @@ -259,10 +257,12 @@ impl InternalClient { EncryptionSettings::new_decrypted_key( AccountEncryptionKeys::V2 { user_key: user_key.clone(), - private_key, - signing_key: signing_key + private_key: key_state.private_key, + signing_key: key_state + .signing_key .ok_or(EncryptionSettingsError::InvalidSigningKey)?, - security_state: security_state + security_state: key_state + .security_state .ok_or(EncryptionSettingsError::InvalidSecurityState)?, }, &self.key_store, @@ -282,17 +282,10 @@ impl InternalClient { &self, pin_key: PinKey, pin_protected_user_key: EncString, - private_key: EncString, - signing_key: Option, - security_state: Option, + key_state: UserKeyState, ) -> Result<(), EncryptionSettingsError> { let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?; - self.initialize_user_crypto_decrypted_key( - decrypted_user_key, - private_key, - signing_key, - security_state, - ) + self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_state) } #[cfg(feature = "secrets")] diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index fc901fff5..6f1e9cd1f 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -20,7 +20,10 @@ use serde::{Deserialize, Serialize}; use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ - client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, + client::{ + encryption_settings::EncryptionSettingsError, internal::UserKeyState, LoginMethod, + UserLoginMethod, + }, key_management::{ AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, }, @@ -62,6 +65,16 @@ pub struct InitUserCryptoRequest { pub method: InitUserCryptoMethod, } +impl From<&InitUserCryptoRequest> for UserKeyState { + fn from(req: &InitUserCryptoRequest) -> Self { + UserKeyState { + private_key: req.private_key.clone(), + signing_key: req.signing_key.clone(), + security_state: req.security_state.clone(), + } + } +} + /// The crypto method used to initialize the user cryptographic state. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -146,25 +159,20 @@ pub(super) async fn initialize_user_crypto( client.internal.init_user_id(user_id)?; } + let key_state = (&req).into(); + match req.method { InitUserCryptoMethod::Password { password, user_key } => { let master_key = MasterKey::derive(&password, &req.email, &req.kdf_params)?; - client.internal.initialize_user_crypto_master_key( - master_key, - user_key, - req.private_key, - req.signing_key, - req.security_state, - )?; + client + .internal + .initialize_user_crypto_master_key(master_key, user_key, key_state)?; } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?; - client.internal.initialize_user_crypto_decrypted_key( - user_key, - req.private_key, - req.signing_key, - req.security_state, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::Pin { pin, @@ -174,9 +182,7 @@ pub(super) async fn initialize_user_crypto( client.internal.initialize_user_crypto_pin( pin_key, pin_protected_user_key, - req.private_key, - req.signing_key, - req.security_state, + key_state, )?; } InitUserCryptoMethod::AuthRequest { @@ -196,12 +202,9 @@ pub(super) async fn initialize_user_crypto( auth_request_key, )?, }; - client.internal.initialize_user_crypto_decrypted_key( - user_key, - req.private_key, - req.signing_key, - req.security_state, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -212,12 +215,9 @@ pub(super) async fn initialize_user_crypto( 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, - req.private_key, - req.signing_key, - req.security_state, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::KeyConnector { master_key, @@ -228,13 +228,9 @@ pub(super) async fn initialize_user_crypto( .map_err(|_| CryptoError::InvalidKey)?; let master_key = MasterKey::try_from(master_key_bytes.as_mut_slice())?; - client.internal.initialize_user_crypto_master_key( - master_key, - user_key, - req.private_key, - req.signing_key, - req.security_state, - )?; + client + .internal + .initialize_user_crypto_master_key(master_key, user_key, key_state)?; } } @@ -998,7 +994,15 @@ mod tests { 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_key(master_key, user_key, private_key, None, None) + .initialize_user_crypto_master_key( + master_key, + user_key, + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, + ) .unwrap(); let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsy7RFHcX3C8Q4/OMmhhbFReYWfB45W9PDTEA8tUZwZmtOiN2RErIS2M1c+K/4HoDJ/TjpbX1f2MZcr4nWvKFuqnZXyewFc+jmvKVewYi+NAu2++vqKq2kKcmMNhwoQDQdQIVy/Uqlp4Cpi2cIwO6ogq5nHNJGR3jm+CpyrafYlbz1bPvL3hbyoGDuG2tgADhyhXUdFuef2oF3wMvn1lAJAvJnPYpMiXUFmj1ejmbwtlxZDrHgUJvUcp7nYdwUKaFoi+sOttHn3u7eZPtNvxMjhSS/X/1xBIzP/mKNLdywH5LoRxniokUk+fV3PYUxJsiU3lV0Trc/tH46jqd8ZGjmwIDAQAB"; diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index e469d8a9a..7b4d1b129 100644 --- a/crates/bitwarden-core/src/platform/generate_fingerprint.rs +++ b/crates/bitwarden-core/src/platform/generate_fingerprint.rs @@ -81,7 +81,7 @@ mod tests { use bitwarden_crypto::{Kdf, MasterKey}; use super::*; - use crate::Client; + use crate::{client::internal::UserKeyState, Client}; #[test] fn test_generate_user_fingerprint() { @@ -105,9 +105,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key.parse().unwrap(), - None, - None, + UserKeyState { + private_key: private_key.parse().unwrap(), + signing_key: None, + security_state: None, + }, ) .unwrap(); From 4bc12142d2e3d778eca2224154e133be737ec24d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 11:35:17 +0200 Subject: [PATCH 51/63] Fix build --- crates/bitwarden-core/src/client/internal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a18e78b5c..0f66a4d2d 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -32,6 +32,7 @@ use crate::{ }; /// Represents the user's keys, that are encrypted by the user key, and the signed security state. +#[cfg(feature = "internal")] pub(crate) struct UserKeyState { pub(crate) private_key: EncString, pub(crate) signing_key: Option, From 7c4f5623d56f0b9168afab5149e617f8bd97101a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 11:57:59 +0200 Subject: [PATCH 52/63] Swap to uuid --- .../src/key_management/security_state.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs index 80b0fd562..9a423ebb5 100644 --- a/crates/bitwarden-core/src/key_management/security_state.rs +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -28,7 +28,7 @@ use bitwarden_crypto::{ KeyStoreContext, SignedObject, SigningNamespace, VerifyingKey, }; use serde::{Deserialize, Serialize}; -use serde_bytes::ByteBuf; +use uuid::Uuid; #[cfg(feature = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] @@ -46,7 +46,7 @@ export type SignedSecurityState = string; pub struct SecurityState { /// The entity ID is a permanent, unchangeable, unique identifier for the object this security /// state applies to. For users, this is the user ID, which never changes. - entity_id: ByteBuf, + entity_id: Uuid, /// The version of the security state gates feature availability. It can only ever be /// incremented. Components can use it to gate format support of specific formats (like /// item url hashes). @@ -58,7 +58,7 @@ impl SecurityState { /// The user needs to be a v2 encryption user. pub fn initialize_for_user(user_id: uuid::Uuid) -> Self { SecurityState { - entity_id: user_id.as_bytes().to_vec().into(), + entity_id: user_id, version: 2, } } @@ -174,16 +174,7 @@ mod tests { .verify_and_unwrap(&verifying_key) .unwrap(); - assert_eq!( - uuid::Uuid::from_bytes( - verified_security_state - .entity_id - .as_ref() - .try_into() - .unwrap() - ), - user_id - ); + assert_eq!(verified_security_state.entity_id, user_id); assert_eq!(verified_security_state.version(), 2); } } From ee78cb7a28ebc7887c4f5558a73eda04ded3fe4b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 12:09:57 +0200 Subject: [PATCH 53/63] Move impl to struct --- crates/bitwarden-core/src/client/internal.rs | 17 +++++++++++++++-- .../bitwarden-core/src/key_management/crypto.rs | 17 ++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 0f66a4d2d..594b8e681 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -17,8 +17,11 @@ use crate::client::login_method::ServiceAccountLoginMethod; #[cfg(feature = "internal")] use crate::key_management::SecurityState; use crate::{ - auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, - key_management::KeyIds, DeviceType, + auth::renew::renew_token, + client::login_method::LoginMethod, + error::UserIdAlreadySetError, + key_management::{crypto::InitUserCryptoRequest, KeyIds}, + DeviceType, }; #[cfg(feature = "internal")] use crate::{ @@ -38,6 +41,16 @@ pub(crate) struct UserKeyState { pub(crate) signing_key: Option, pub(crate) security_state: Option, } +#[cfg(feature = "internal")] +impl From<&InitUserCryptoRequest> for UserKeyState { + fn from(req: &InitUserCryptoRequest) -> Self { + UserKeyState { + private_key: req.private_key.clone(), + signing_key: req.signing_key.clone(), + security_state: req.security_state.clone(), + } + } +} #[allow(missing_docs)] #[derive(Debug, Clone)] diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 6f1e9cd1f..0cd0379d4 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -20,10 +20,7 @@ use serde::{Deserialize, Serialize}; use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ - client::{ - encryption_settings::EncryptionSettingsError, internal::UserKeyState, LoginMethod, - UserLoginMethod, - }, + client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, key_management::{ AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, }, @@ -65,16 +62,6 @@ pub struct InitUserCryptoRequest { pub method: InitUserCryptoMethod, } -impl From<&InitUserCryptoRequest> for UserKeyState { - fn from(req: &InitUserCryptoRequest) -> Self { - UserKeyState { - private_key: req.private_key.clone(), - signing_key: req.signing_key.clone(), - security_state: req.security_state.clone(), - } - } -} - /// The crypto method used to initialize the user cryptographic state. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -713,7 +700,7 @@ mod tests { use bitwarden_crypto::RsaKeyPair; use super::*; - use crate::Client; + use crate::{client::internal::UserKeyState, Client}; #[tokio::test] async fn test_update_password() { From f4007c1e65fa5ff008337996e46c572d1d0bb849 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 12:36:14 +0200 Subject: [PATCH 54/63] Clean up imports --- crates/bitwarden-core/src/client/internal.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 594b8e681..cf9443e6a 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -25,13 +25,13 @@ use crate::{ }; #[cfg(feature = "internal")] use crate::{ - client::encryption_settings::AccountEncryptionKeys, key_management::SignedSecurityState, -}; -#[cfg(feature = "internal")] -use crate::{ - client::encryption_settings::EncryptionSettingsError, - client::{flags::Flags, login_method::UserLoginMethod}, + client::{ + encryption_settings::{AccountEncryptionKeys, EncryptionSettingsError}, + flags::Flags, + login_method::UserLoginMethod, + }, error::NotAuthenticatedError, + key_management::SignedSecurityState, }; /// Represents the user's keys, that are encrypted by the user key, and the signed security state. From 1e25d0ef18c87e975e2bdec9d43bb2dd480f0ed9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 12:37:05 +0200 Subject: [PATCH 55/63] Clean up imports --- crates/bitwarden-core/src/client/internal.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index cf9443e6a..a6c670073 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -14,8 +14,6 @@ use uuid::Uuid; use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use crate::client::login_method::ServiceAccountLoginMethod; -#[cfg(feature = "internal")] -use crate::key_management::SecurityState; use crate::{ auth::renew::renew_token, client::login_method::LoginMethod, @@ -31,7 +29,7 @@ use crate::{ login_method::UserLoginMethod, }, error::NotAuthenticatedError, - key_management::SignedSecurityState, + key_management::{SecurityState, SignedSecurityState}, }; /// Represents the user's keys, that are encrypted by the user key, and the signed security state. From 17758a4b2a504bf6eab9e6335b5aa2672c5d8d75 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 12:45:48 +0200 Subject: [PATCH 56/63] Fix build --- crates/bitwarden-core/src/client/internal.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a6c670073..6db803a32 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -15,11 +15,8 @@ use crate::client::encryption_settings::EncryptionSettings; #[cfg(feature = "secrets")] use crate::client::login_method::ServiceAccountLoginMethod; use crate::{ - auth::renew::renew_token, - client::login_method::LoginMethod, - error::UserIdAlreadySetError, - key_management::{crypto::InitUserCryptoRequest, KeyIds}, - DeviceType, + auth::renew::renew_token, client::login_method::LoginMethod, error::UserIdAlreadySetError, + key_management::KeyIds, DeviceType, }; #[cfg(feature = "internal")] use crate::{ @@ -29,7 +26,7 @@ use crate::{ login_method::UserLoginMethod, }, error::NotAuthenticatedError, - key_management::{SecurityState, SignedSecurityState}, + key_management::{crypto::InitUserCryptoRequest, SecurityState, SignedSecurityState}, }; /// Represents the user's keys, that are encrypted by the user key, and the signed security state. From a85f84f743df7f15bbc1ecaf814dcb198c96a399 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 13:27:32 +0200 Subject: [PATCH 57/63] Add test vectors and tests --- crates/bitwarden-core/src/client/internal.rs | 12 + .../src/key_management/crypto.rs | 267 +++++++++++++----- .../src/key_management/crypto_client.rs | 4 +- crates/bitwarden-crypto/src/error.rs | 8 + 4 files changed, 214 insertions(+), 77 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 6db803a32..847ef16d5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -215,6 +215,18 @@ impl InternalClient { &self.key_store } + /// Returns the security version of the user. + /// `1` is returned for V1 users that do not have a signed security state. + /// `2` or greater is returned for V2 users that have a signed security state. + #[cfg(feature = "internal")] + pub fn get_security_version(&self) -> u64 { + self.security_state + .read() + .expect("RwLock is not poisoned") + .as_ref() + .map_or(1, |state| state.version()) + } + #[allow(missing_docs)] pub fn init_user_id(&self, user_id: Uuid) -> Result<(), UserIdAlreadySetError> { let set_uuid = self.user_id.get_or_init(|| user_id); diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 0cd0379d4..fb2ed6b9d 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -568,7 +568,7 @@ pub(super) fn verify_asymmetric_keys( } /// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user -#[derive(Serialize, Deserialize, Debug)] +#[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))] @@ -594,11 +594,10 @@ pub struct UserCryptoV2KeysResponse { security_version: u64, } -/// Creates the user's cryptographic state for v2 users. This includes ensuring signature keypair is +/// 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. If the client already contains a v1 user, then this user's private-key will -/// be re-used. -pub(crate) fn make_keys_for_user_crypto_v2( +/// key is a Cose key. +pub(crate) fn make_v2_keys_for_v1_user( client: &Client, ) -> Result { let key_store = client.internal.get_key_store(); @@ -606,14 +605,25 @@ pub(crate) fn make_keys_for_user_crypto_v2( let temporary_user_key_id = SymmetricKeyId::Local("temporary_user_key"); let temporary_signing_key_id = SigningKeyId::Local("temporary_signing_key"); - let temporary_private_key_id = AsymmetricKeyId::Local("temporary_private_key"); + // Re-use existing private key + let private_key_id = AsymmetricKeyId::UserPrivateKey; + + // Ensure that the function is only called for a V1 user. + if client.internal.get_security_version() != 1 { + return Err(CryptoError::CryptoStateError( + CryptoStateError::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_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { + return Err(CryptoError::MissingKeyId("UserPrivateKey".to_string())); + } - // If the user already has a private key, use it. Otherwise, create a temporary one. - let private_key_id = if ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { - AsymmetricKeyId::UserPrivateKey - } else { - ctx.make_asymmetric_key(temporary_private_key_id)? - }; #[allow(deprecated)] let private_key = ctx.dangerous_get_asymmetric_key(private_key_id)?.clone(); @@ -662,12 +672,24 @@ pub(crate) fn get_v2_rotated_account_keys( 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(CryptoError::CryptoStateError( + CryptoStateError::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(CryptoError::CryptoStateError( CryptoStateError::MissingSecurityState, ))?; @@ -701,6 +723,17 @@ mod tests { use super::*; use crate::{client::internal::UserKeyState, Client}; + 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"; #[tokio::test] async fn test_update_password() { @@ -902,68 +935,6 @@ mod tests { assert_eq!(client_key, client3_key); } - #[tokio::test] - async fn test_user_crypto_v2() { - let client = Client::new(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(uuid::Uuid::new_v4()), - kdf_params: Kdf::PBKDF2 { - iterations: 100_000.try_into().unwrap(), - }, - email: "test@bitwarden.com".into(), - private_key: priv_key, - signing_key: None, - security_state: None, - method: InitUserCryptoMethod::Password { - password: "asdfasdfasdf".into(), - user_key: encrypted_userkey.clone(), - }, - }, - ) - .await - .unwrap(); - - let master_key = MasterKey::derive( - "asdfasdfasdf", - "test@bitwarden.com", - &Kdf::PBKDF2 { - iterations: NonZeroU32::new(100_000).unwrap(), - }, - ) - .unwrap(); - let enrollment_response = make_keys_for_user_crypto_v2(&client).unwrap(); - let encrypted_userkey_v2 = master_key - .encrypt_user_key(&SymmetricCryptoKey::try_from(enrollment_response.user_key).unwrap()) - .unwrap(); - - let client2 = Client::new(None); - initialize_user_crypto( - &client2, - InitUserCryptoRequest { - user_id: Some(uuid::Uuid::new_v4()), - kdf_params: Kdf::PBKDF2 { - iterations: 100_000.try_into().unwrap(), - }, - email: "test@bitwarden.com".into(), - private_key: enrollment_response.private_key, - signing_key: Some(enrollment_response.signing_key), - security_state: Some(enrollment_response.security_state), - method: InitUserCryptoMethod::Password { - password: "asdfasdfasdf".into(), - user_key: encrypted_userkey_v2, - }, - }, - ) - .await - .unwrap(); - } - #[test] fn test_enroll_admin_password_reset() { let client = Client::new(None); @@ -1120,6 +1091,114 @@ mod tests { assert!(!response.valid_private_key); } + #[tokio::test] + async fn test_make_v2_keys_for_v1_user() { + let client = Client::new(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(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: priv_key, + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey.clone(), + }, + }, + ) + .await + .unwrap(); + + let master_key = MasterKey::derive( + "asdfasdfasdf", + "test@bitwarden.com", + &Kdf::PBKDF2 { + iterations: NonZeroU32::new(100_000).unwrap(), + }, + ) + .unwrap(); + 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(None); + initialize_user_crypto( + &client2, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: enrollment_response.private_key, + signing_key: Some(enrollment_response.signing_key), + security_state: Some(enrollment_response.security_state), + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: encrypted_userkey_v2, + }, + }, + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_make_v2_keys_for_v1_user_with_v2_user_fails() { + let client = Client::new(None); + #[allow(deprecated)] + client + .internal + .get_key_store() + .context_mut() + .set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ) + .unwrap(); + initialize_user_crypto( + &client, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(), + signing_key: Some(TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap()), + security_state: Some(TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap()), + method: InitUserCryptoMethod::DecryptedKey { + decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(), + }, + }, + ) + .await + .unwrap(); + + let result = make_v2_keys_for_v1_user(&client); + assert!(matches!( + result, + Err(CryptoError::CryptoStateError( + CryptoStateError::WrongAccountCryptoVersion { + expected: _, + got: _ + } + )) + )); + } + #[test] fn test_get_v2_rotated_account_keys_non_v2_user() { let client = Client::new(None); @@ -1138,8 +1217,46 @@ mod tests { assert!(matches!( result, Err(CryptoError::CryptoStateError( - CryptoStateError::MissingSecurityState + CryptoStateError::WrongAccountCryptoVersion { + expected: _, + got: _ + } )) )); } + + #[tokio::test] + async fn test_get_v2_rotated_account_keys() { + let client = Client::new(None); + #[allow(deprecated)] + client + .internal + .get_key_store() + .context_mut() + .set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ) + .unwrap(); + initialize_user_crypto( + &client, + InitUserCryptoRequest { + user_id: Some(uuid::Uuid::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(), + signing_key: Some(TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap()), + security_state: Some(TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap()), + method: InitUserCryptoMethod::DecryptedKey { + decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(), + }, + }, + ) + .await + .unwrap(); + + assert!(get_v2_rotated_account_keys(&client).is_ok()); + } } diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index fd7faf31b..8b3ebdeac 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -18,7 +18,7 @@ use crate::key_management::crypto::{ use crate::{ client::encryption_settings::EncryptionSettingsError, key_management::crypto::{ - get_v2_rotated_account_keys, make_keys_for_user_crypto_v2, CryptoClientError, + get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError, UserCryptoV2KeysResponse, }, Client, @@ -68,7 +68,7 @@ impl CryptoClient { /// Makes a new signing key pair and signs the public key for the user pub fn make_keys_for_user_crypto_v2(&self) -> Result { - make_keys_for_user_crypto_v2(&self.client) + make_v2_keys_for_v1_user(&self.client) } /// Creates a rotated set of account keys for the current state diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 4a802481e..091e25421 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -88,6 +88,14 @@ pub enum CryptoStateError { /// have a security state, V1 users cannot have a security state. #[error("Security state is required, but missing")] MissingSecurityState, + /// The function expected a user in a account cryptography version, but got a different one. + #[error("Expected user in account cryptography version {expected}, but got {got}")] + WrongAccountCryptoVersion { + /// The expected account cryptography version. This can include a range, such as `2+`. + expected: String, + /// The actual account cryptography version. + got: u32, + }, } #[derive(Debug, Error)] From 932f9b804eddfb9c295d59f54ded8c024ddde7b5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 13:36:24 +0200 Subject: [PATCH 58/63] Add comment for signature keys on registration --- crates/bitwarden-core/src/auth/tde.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 1c43d8e38..20da1208e 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -46,7 +46,7 @@ pub(super) fn make_register_tde_keys( user_key.0, UserKeyState { private_key: key_pair.private.clone(), - // Note: Signing keys are not supported on registration yet. This needs to be changed as + // TODO (https://bitwarden.atlassian.net/browse/PM-21771) Signing keys are not supported on registration yet. This needs to be changed as // soon as registration is supported. signing_key: None, security_state: None, From 2511017fb1d3c0bf32db49e569b86fb846317473 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 13:37:14 +0200 Subject: [PATCH 59/63] Lift up use --- crates/bitwarden-core/src/auth/login/password.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 772ede819..9bc4d6e85 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -23,7 +23,10 @@ pub(crate) async fn login_password( ) -> Result { use bitwarden_crypto::{EncString, HashPurpose, MasterKey}; - use crate::{client::UserLoginMethod, require}; + use crate::{ + client::{internal::UserKeyState, UserLoginMethod}, + require, + }; info!("password logging in"); @@ -34,8 +37,6 @@ pub(crate) async fn login_password( let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { - use crate::client::internal::UserKeyState; - client.internal.set_tokens( r.access_token.clone(), r.refresh_token.clone(), From 8ce7e267d0f8823992cd2c5005e20eafb2c87e9b Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 13:43:19 +0200 Subject: [PATCH 60/63] Cargo fmt --- crates/bitwarden-core/src/key_management/crypto.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index fb2ed6b9d..33ec4806a 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -594,8 +594,8 @@ pub struct UserCryptoV2KeysResponse { 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 +/// 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. pub(crate) fn make_v2_keys_for_v1_user( client: &Client, @@ -619,7 +619,8 @@ pub(crate) fn make_v2_keys_for_v1_user( } // 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. + // V1 user must have a private key to upgrade. This should be ensured by the client before + // calling the upgrade function. if !ctx.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { return Err(CryptoError::MissingKeyId("UserPrivateKey".to_string())); } @@ -689,7 +690,8 @@ pub(crate) fn get_v2_rotated_account_keys( .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. + // This cannot occur since the security version check above already ensures that the + // security state is present. .ok_or(CryptoError::CryptoStateError( CryptoStateError::MissingSecurityState, ))?; From 95827df45438beb9e9a436b6f3a713d9d4ef646c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 13:56:19 +0200 Subject: [PATCH 61/63] Move stateful crypto error to core crate --- crates/bitwarden-core/src/error.rs | 22 ++++++ .../src/key_management/crypto.rs | 68 +++++++++---------- .../src/key_management/crypto_client.rs | 9 ++- crates/bitwarden-crypto/src/error.rs | 21 ------ crates/bitwarden-crypto/src/lib.rs | 2 +- 5 files changed, 62 insertions(+), 60 deletions(-) diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index cafd2d24a..f8207a920 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -4,6 +4,7 @@ use std::fmt::Debug; use bitwarden_api_api::apis::Error as ApiApisError; use bitwarden_api_identity::apis::Error as IdentityError; +use bitwarden_error::bitwarden_error; use reqwest::StatusCode; use thiserror::Error; @@ -73,6 +74,27 @@ pub struct WrongPasswordError; #[error("Missing private key")] pub struct MissingPrivateKeyError; +/// Signifies that the state is invalid from a cryptographic perspective, such as a required +/// security value missing, or being invalid +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum StatefulCryptoError { + /// The security state is not present, but required for this user. V2 users must always + /// have a security state, V1 users cannot have a security state. + #[error("Security state is required, but missing")] + MissingSecurityState, + /// The function expected a user in a account cryptography version, but got a different one. + #[error("Expected user in account cryptography version {expected}, but got {got}")] + WrongAccountCryptoVersion { + /// The expected account cryptography version. This can include a range, such as `2+`. + expected: String, + /// The actual account cryptography version. + got: u32, + }, + #[error("Crypto error, {0}")] + CryptoError(#[from] bitwarden_crypto::CryptoError), +} + /// This macro is used to require that a value is present or return an error otherwise. /// It is equivalent to using `val.ok_or(Error::MissingFields)?`, but easier to use and /// with a more descriptive error message. diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 33ec4806a..1f1e4e211 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -9,9 +9,9 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, - CryptoStateError, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, - Pkcs8PrivateKeyBytes, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, - SymmetricCryptoKey, UnsignedSharedKey, UserKey, + EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, + SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -21,6 +21,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, + error::StatefulCryptoError, key_management::{ AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, }, @@ -599,7 +600,7 @@ pub struct UserCryptoV2KeysResponse { /// key is a Cose key. pub(crate) fn make_v2_keys_for_v1_user( client: &Client, -) -> Result { +) -> Result { let key_store = client.internal.get_key_store(); let mut ctx = key_store.context(); @@ -610,19 +611,19 @@ pub(crate) fn make_v2_keys_for_v1_user( // Ensure that the function is only called for a V1 user. if client.internal.get_security_version() != 1 { - return Err(CryptoError::CryptoStateError( - CryptoStateError::WrongAccountCryptoVersion { - expected: "1".to_string(), - got: 2, - }, - )); + 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_asymmetric_key(AsymmetricKeyId::UserPrivateKey) { - return Err(CryptoError::MissingKeyId("UserPrivateKey".to_string())); + return Err(StatefulCryptoError::CryptoError(CryptoError::MissingKeyId( + "UserPrivateKey".to_string(), + ))); } #[allow(deprecated)] @@ -643,9 +644,12 @@ pub(crate) fn make_v2_keys_for_v1_user( let public_key = private_key.to_public_key(); // Initialize security state for the user - let security_state = SecurityState::initialize_for_user(client.internal.get_user_id().ok_or( - CryptoError::CryptoStateError(CryptoStateError::MissingSecurityState), - )?); + let security_state = SecurityState::initialize_for_user( + client + .internal + .get_user_id() + .ok_or(StatefulCryptoError::MissingSecurityState)?, + ); let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; Ok(UserCryptoV2KeysResponse { @@ -669,19 +673,17 @@ pub(crate) fn make_v2_keys_for_v1_user( /// user to be a v2 user; that is, they have a signing key, a cose user-key, and a private key pub(crate) fn get_v2_rotated_account_keys( client: &Client, -) -> Result { +) -> Result { 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(CryptoError::CryptoStateError( - CryptoStateError::WrongAccountCryptoVersion { - expected: "2+".to_string(), - got: 1, - }, - )); + return Err(StatefulCryptoError::WrongAccountCryptoVersion { + expected: "2+".to_string(), + got: 1, + }); } let security_state = client @@ -692,9 +694,7 @@ pub(crate) fn get_v2_rotated_account_keys( .to_owned() // This cannot occur since the security version check above already ensures that the // security state is present. - .ok_or(CryptoError::CryptoStateError( - CryptoStateError::MissingSecurityState, - ))?; + .ok_or(StatefulCryptoError::MissingSecurityState)?; let rotated_keys = dangerous_get_v2_rotated_account_keys( AsymmetricKeyId::UserPrivateKey, @@ -1192,12 +1192,10 @@ mod tests { let result = make_v2_keys_for_v1_user(&client); assert!(matches!( result, - Err(CryptoError::CryptoStateError( - CryptoStateError::WrongAccountCryptoVersion { - expected: _, - got: _ - } - )) + Err(StatefulCryptoError::WrongAccountCryptoVersion { + expected: _, + got: _ + }) )); } @@ -1218,12 +1216,10 @@ mod tests { let result = get_v2_rotated_account_keys(&client); assert!(matches!( result, - Err(CryptoError::CryptoStateError( - CryptoStateError::WrongAccountCryptoVersion { - expected: _, - got: _ - } - )) + Err(StatefulCryptoError::WrongAccountCryptoVersion { + expected: _, + got: _ + }) )); } diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 8b3ebdeac..961b3d19b 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -17,6 +17,7 @@ use crate::key_management::crypto::{ }; use crate::{ client::encryption_settings::EncryptionSettingsError, + error::StatefulCryptoError, key_management::crypto::{ get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError, UserCryptoV2KeysResponse, @@ -67,12 +68,16 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_keys_for_user_crypto_v2(&self) -> Result { + pub fn make_keys_for_user_crypto_v2( + &self, + ) -> Result { make_v2_keys_for_v1_user(&self.client) } /// Creates a rotated set of account keys for the current state - pub fn get_v2_rotated_account_keys(&self) -> Result { + pub fn get_v2_rotated_account_keys( + &self, + ) -> Result { get_v2_rotated_account_keys(&self.client) } } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 091e25421..f572cec94 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -69,9 +69,6 @@ pub enum CryptoError { #[error("Encoding error, {0}")] EncodingError(#[from] EncodingError), - - #[error("Crypto state error, {0}")] - CryptoStateError(#[from] CryptoStateError), } #[derive(Debug, Error)] @@ -80,24 +77,6 @@ pub enum UnsupportedOperation { EncryptionNotImplementedForKey, } -/// Signifies that the state is invalid from a cryptographic perspective, such as a required -/// security value missing, or being invalid -#[derive(Debug, Error)] -pub enum CryptoStateError { - /// The security state is not present, but required for this user. V2 users must always - /// have a security state, V1 users cannot have a security state. - #[error("Security state is required, but missing")] - MissingSecurityState, - /// The function expected a user in a account cryptography version, but got a different one. - #[error("Expected user in account cryptography version {expected}, but got {got}")] - WrongAccountCryptoVersion { - /// The expected account cryptography version. This can include a range, such as `2+`. - expected: String, - /// The actual account cryptography version. - got: u32, - }, -} - #[derive(Debug, Error)] pub enum EncStringParseError { #[error("No type detected, missing '.' separator")] diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 801a9f7ab..e5e86891b 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -19,7 +19,7 @@ mod enc_string; pub use enc_string::{EncString, UnsignedSharedKey}; mod error; pub(crate) use error::Result; -pub use error::{CryptoError, CryptoStateError, EncodingError}; +pub use error::{CryptoError, EncodingError}; mod fingerprint; pub use fingerprint::fingerprint; mod keys; From 6fa7effd2e7bb0137e4535568e85e3487c47a6d7 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 14:05:10 +0200 Subject: [PATCH 62/63] Fix build on non-internal feature flag --- crates/bitwarden-core/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index f8207a920..f2deb775d 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -76,6 +76,7 @@ pub struct MissingPrivateKeyError; /// Signifies that the state is invalid from a cryptographic perspective, such as a required /// security value missing, or being invalid +#[cfg(feature = "internal")] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum StatefulCryptoError { From dfaaca9a800c35a9e35c5db2b7d0f643d02eb95a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Jul 2025 14:08:41 +0200 Subject: [PATCH 63/63] Filter import to internal feature only --- crates/bitwarden-core/src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-core/src/error.rs b/crates/bitwarden-core/src/error.rs index f2deb775d..996d7d43d 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -4,6 +4,7 @@ use std::fmt::Debug; use bitwarden_api_api::apis::Error as ApiApisError; use bitwarden_api_identity::apis::Error as IdentityError; +#[cfg(feature = "internal")] use bitwarden_error::bitwarden_error; use reqwest::StatusCode; use thiserror::Error;