diff --git a/Cargo.lock b/Cargo.lock index 4e3412708..46932f01b 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/Cargo.toml b/Cargo.toml index c6bd4f340..26d1ac664 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 472167b79..9f0915938 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 = { workspace = true } serde_json = { workspace = true } serde_qs = { workspace = true } serde_repr = { workspace = true } diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 3020222f8..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) + .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"; @@ -232,7 +243,15 @@ 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, + UserKeyState { + private_key: private_key.clone(), + signing_key: None, + security_state: None, + }, + ) .unwrap(); // Initialize a new device which will request to be logged in @@ -251,6 +270,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..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,8 +54,11 @@ pub(crate) async fn login_api_key( client.internal.initialize_user_crypto_master_key( master_key, user_key, - private_key, - None, + UserKeyState { + private_key, + signing_key: None, + security_state: 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..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"); @@ -53,8 +56,11 @@ pub(crate) async fn login_password( client.internal.initialize_user_crypto_master_key( master_key, user_key, - private_key, - 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 229c7fde7..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,16 +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, + user_key.clone(), + UserKeyState { + private_key, + signing_key: None, + security_state: None, + }, ) .unwrap(); @@ -191,8 +197,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key, - 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 c337f9327..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,8 +78,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key, - 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 9b8c5db9c..20da1208e 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,10 +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, + UserKeyState { + private_key: key_pair.private.clone(), + // 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, + }, )?; Ok(RegisterTdeKeyResponse { diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index 6cd632eb4..4a2db6a80 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -105,6 +105,8 @@ impl Client { external_client, key_store: KeyStore::default(), #[cfg(feature = "internal")] + 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 3610f0054..64e692fcf 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -1,12 +1,22 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::{EncString, UnsignedSharedKey}; +use std::sync::RwLock; + +#[cfg(feature = "internal")] +use bitwarden_crypto::{ + Aes256CbcHmacKey, AsymmetricCryptoKey, CoseKeyBytes, CoseSerializable, EncString, + KeyDecryptable, Pkcs8PrivateKeyBytes, SigningKey, UnsignedSharedKey, XChaCha20Poly1305Key, +}; #[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, SecurityState, SignedSecurityState, SigningKeyId}; #[cfg(any(feature = "internal", feature = "secrets"))] use crate::key_management::{KeyIds, SymmetricKeyId}; use crate::{error::UserIdAlreadySetError, MissingPrivateKeyError, VaultLockedError}; @@ -30,6 +40,9 @@ pub enum EncryptionSettingsError { #[error("Invalid signing key")] InvalidSigningKey, + #[error("Invalid security state")] + InvalidSecurityState, + #[error(transparent)] MissingPrivateKey(#[from] MissingPrivateKeyError), @@ -37,6 +50,21 @@ pub enum EncryptionSettingsError { UserIdAlreadySetError(#[from] UserIdAlreadySetError), } +#[allow(clippy::large_enum_variant)] +#[cfg(feature = "internal")] +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 {} @@ -47,21 +75,55 @@ impl EncryptionSettings { /// discouraged #[cfg(feature = "internal")] pub(crate) fn new_decrypted_key( - user_key: SymmetricCryptoKey, - private_key: EncString, - signing_key: Option, + encryption_keys: AccountEncryptionKeys, store: &KeyStore, + security_state_rwlock: &RwLock>, ) -> Result<(), EncryptionSettingsError> { - use bitwarden_crypto::{AsymmetricCryptoKey, CoseSerializable, KeyDecryptable, SigningKey}; - use log::warn; + // 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. + 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, + signing_key, + security_state, + store, + security_state_rwlock, + )?; + } + } + + Ok(()) + } - use crate::key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}; + #[cfg(feature = "internal")] + fn init_v1( + 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)?; - // FIXME: [PM-11690] - Temporarily ignore invalid private keys until we have a recovery - // process in place. - AsymmetricCryptoKey::from_der(&dec.into()) + + // 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"); }) @@ -72,14 +134,6 @@ impl EncryptionSettings { // .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)] @@ -89,10 +143,44 @@ impl EncryptionSettings { if let Some(private_key) = private_key { ctx.set_asymmetric_key(AsymmetricKeyId::UserPrivateKey, private_key)?; } + } - if let Some(signing_key) = signing_key { - ctx.set_signing_key(SigningKeyId::UserSigningKey, signing_key)?; - } + Ok(()) + } + + #[cfg(feature = "internal")] + fn init_v2( + user_key: XChaCha20Poly1305Key, + private_key: EncString, + signing_key: EncString, + security_state: SignedSecurityState, + store: &KeyStore, + sdk_security_state: &RwLock>, + ) -> 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 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)?; + + 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") = Some(security_state); + + #[allow(deprecated)] + { + 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(()) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 5c0e085b5..847ef16d5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -4,7 +4,7 @@ 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::{CryptoError, EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey}; #[cfg(feature = "internal")] use bitwarden_state::registry::StateRegistry; use chrono::Utc; @@ -20,11 +20,33 @@ use crate::{ }; #[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::{crypto::InitUserCryptoRequest, SecurityState, SignedSecurityState}, }; +/// 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, + 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)] pub struct ApiConfigurations { @@ -80,6 +102,8 @@ 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")] pub(crate) repository_map: StateRegistry, @@ -191,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); @@ -215,23 +251,49 @@ impl InternalClient { &self, master_key: MasterKey, user_key: EncString, - private_key: EncString, - signing_key: Option, + key_state: UserKeyState, ) -> 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)?; - - Ok(()) + 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, + key_state: UserKeyState, ) -> Result<(), EncryptionSettingsError> { - EncryptionSettings::new_decrypted_key(user_key, private_key, signing_key, &self.key_store)?; + match user_key { + SymmetricCryptoKey::Aes256CbcHmacKey(ref user_key) => { + EncryptionSettings::new_decrypted_key( + AccountEncryptionKeys::V1 { + user_key: user_key.clone(), + private_key: key_state.private_key, + }, + &self.key_store, + &self.security_state, + )?; + } + SymmetricCryptoKey::XChaCha20Poly1305Key(ref user_key) => { + EncryptionSettings::new_decrypted_key( + AccountEncryptionKeys::V2 { + user_key: user_key.clone(), + private_key: key_state.private_key, + signing_key: key_state + .signing_key + .ok_or(EncryptionSettingsError::InvalidSigningKey)?, + security_state: key_state + .security_state + .ok_or(EncryptionSettingsError::InvalidSecurityState)?, + }, + &self.key_store, + &self.security_state, + )?; + } + _ => { + return Err(CryptoError::InvalidKey.into()); + } + } Ok(()) } @@ -241,11 +303,10 @@ impl InternalClient { &self, pin_key: PinKey, pin_protected_user_key: EncString, - private_key: EncString, - signing_key: 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) + self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_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/error.rs b/crates/bitwarden-core/src/error.rs index cafd2d24a..996d7d43d 100644 --- a/crates/bitwarden-core/src/error.rs +++ b/crates/bitwarden-core/src/error.rs @@ -4,6 +4,8 @@ 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; @@ -73,6 +75,28 @@ 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 +#[cfg(feature = "internal")] +#[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 d9d9b78f3..800ce8085 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, - PrimitiveEncryptable, RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey, - SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -21,7 +21,10 @@ use {tsify::Tsify, wasm_bindgen::prelude::*}; use crate::{ client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod}, - key_management::{AsymmetricKeyId, SigningKeyId, SymmetricKeyId}, + error::StatefulCryptoError, + key_management::{ + AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId, + }, Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError, }; @@ -54,6 +57,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, } @@ -142,23 +147,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, - )?; + 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, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::Pin { pin, @@ -168,8 +170,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, + key_state, )?; } InitUserCryptoMethod::AuthRequest { @@ -189,11 +190,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, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::DeviceKey { device_key, @@ -204,11 +203,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, - )?; + client + .internal + .initialize_user_crypto_decrypted_key(user_key, key_state)?; } InitUserCryptoMethod::KeyConnector { master_key, @@ -219,12 +216,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, - )?; + client + .internal + .initialize_user_crypto_master_key(master_key, user_key, key_state)?; } } @@ -574,76 +568,103 @@ pub(super) fn verify_asymmetric_keys( }) } -/// A new signing key pair along with the signed public key -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +/// Response for the `make_keys_for_user_crypto_v2`, containing a set of keys for a user +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct MakeUserSigningKeysResponse { - /// Base64 encoded verifying key - verifying_key: String, - /// Signing key, encrypted with the user's symmetric key - signing_key: EncString, +pub struct UserCryptoV2KeysResponse { + /// 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( +/// 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, -) -> 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"); + // 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(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(StatefulCryptoError::CryptoError(CryptoError::MissingKeyId( + "UserPrivateKey".to_string(), + ))); + } + #[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, - )?; + let private_key = ctx.dangerous_get_asymmetric_key(private_key_id)?.clone(); - Ok(MakeUserSigningKeysResponse { - verifying_key: STANDARD.encode(signature_keypair.to_verifying_key().to_cose()), - signing_key: signature_keypair - .to_cose() - .encrypt(&mut ctx, SymmetricKeyId::User)?, + // New user key + let user_key = SymmetricCryptoKey::make_xchacha20_poly1305_key(); + #[allow(deprecated)] + 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)?; + 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(StatefulCryptoError::MissingSecurityState)?, + ); + let signed_security_state = security_state.sign(temporary_signing_key_id, &mut ctx)?; + + Ok(UserCryptoV2KeysResponse { + user_key: user_key.to_base64(), + + private_key: private_key.to_der()?.encrypt_with_key(&user_key)?, + public_key: STANDARD.encode(public_key.to_der()?), signed_public_key, - }) -} -/// 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, -} + signing_key: signing_key.to_cose().encrypt_with_key(&user_key)?, + verifying_key: STANDARD.encode(signing_key.to_verifying_key().to_cose()), -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, - } - } + security_state: signed_security_state, + security_version: security_state.version(), + }) } /// Gets a set of new wrapped account keys for a user, given a new user key. @@ -652,18 +673,48 @@ impl From for RotateUserKeysResponse { /// 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 mut ctx = key_store.context(); + + // Ensure that the function is only called for a V2 user. + // V2 users have a security version 2 or higher. + if client.internal.get_security_version() == 1 { + return Err(StatefulCryptoError::WrongAccountCryptoVersion { + expected: "2+".to_string(), + got: 1, + }); + } - dangerous_get_v2_rotated_account_keys( - &SymmetricCryptoKey::try_from(user_key)?, + let security_state = client + .internal + .security_state + .read() + .expect("RwLock is not poisoned") + .to_owned() + // This cannot occur since the security version check above already ensures that the + // security state is present. + .ok_or(StatefulCryptoError::MissingSecurityState)?; + + let rotated_keys = dangerous_get_v2_rotated_account_keys( AsymmetricKeyId::UserPrivateKey, SigningKeyId::UserSigningKey, &ctx, - ) - .map(Into::into) + )?; + + Ok(UserCryptoV2KeysResponse { + 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: 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)] @@ -673,7 +724,18 @@ mod tests { use bitwarden_crypto::RsaKeyPair; use super::*; - use crate::Client; + 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() { @@ -693,6 +755,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 +777,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 +837,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 +861,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 +906,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, @@ -872,8 +939,6 @@ mod tests { #[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,7 +954,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) + .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"; @@ -1019,4 +1092,169 @@ mod tests { assert!(response.private_key_decryptable); assert!(!response.valid_private_key); } + + #[tokio::test] + async fn test_make_v2_keys_for_v1_user() { + let client = Client::new(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(StatefulCryptoError::WrongAccountCryptoVersion { + expected: _, + got: _ + }) + )); + } + + #[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(StatefulCryptoError::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 5e88f3326..961b3d19b 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")] @@ -18,8 +17,10 @@ use crate::key_management::crypto::{ }; use crate::{ client::encryption_settings::EncryptionSettingsError, + error::StatefulCryptoError, key_management::crypto::{ - get_v2_rotated_account_keys, CryptoClientError, RotateUserKeysResponse, + get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError, + UserCryptoV2KeysResponse, }, Client, }; @@ -67,18 +68,17 @@ impl CryptoClient { } /// Makes a new signing key pair and signs the public key for the user - pub fn make_user_signing_keys_for_enrollment( + pub fn make_keys_for_user_crypto_v2( &self, - ) -> Result { - make_user_signing_keys_for_enrollment(&self.client) + ) -> 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, - user_key: String, - ) -> Result { - get_v2_rotated_account_keys(&self.client, user_key) + ) -> Result { + get_v2_rotated_account_keys(&self.client) } } diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 79bbf124a..10be377a8 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,6 +18,11 @@ 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! { #[symmetric] pub enum SymmetricKeyId { diff --git a/crates/bitwarden-core/src/key_management/security_state.rs b/crates/bitwarden-core/src/key_management/security_state.rs new file mode 100644 index 000000000..9a423ebb5 --- /dev/null +++ b/crates/bitwarden-core/src/key_management/security_state.rs @@ -0,0 +1,180 @@ +//! 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 bitwarden_crypto::{ + CoseSerializable, CoseSign1Bytes, CryptoError, EncodingError, FromStrVisitor, KeyIds, + KeyStoreContext, SignedObject, SigningNamespace, VerifyingKey, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[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(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 + /// state applies to. For users, this is the user ID, which never changes. + 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). + 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, + 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( + &self, + signing_key_id: Ids::Signing, + ctx: &mut KeyStoreContext, + ) -> Result { + Ok(SignedSecurityState(ctx.sign( + signing_key_id, + &self, + &SigningNamespace::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( + self, + verifying_key: &VerifyingKey, + ) -> Result { + self.0 + .verify_and_unwrap(verifying_key, &SigningNamespace::SecurityState) + } +} + +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::InvalidBase64Encoding)?; + 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) + } +} + +#[cfg(test)] +mod tests { + use bitwarden_crypto::{KeyStore, SignatureAlgorithm, SigningKey}; + + use super::*; + use crate::key_management::{KeyIds, SigningKeyId}; + + #[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(SigningKeyId::Local(""), signing_key.clone()) + .unwrap(); + let signed_security_state = security_state + .sign(SigningKeyId::Local(""), &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!(verified_security_state.entity_id, user_id); + assert_eq!(verified_security_state.version(), 2); + } +} diff --git a/crates/bitwarden-core/src/platform/generate_fingerprint.rs b/crates/bitwarden-core/src/platform/generate_fingerprint.rs index e6ff4cbf8..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,8 +105,11 @@ mod tests { .initialize_user_crypto_master_key( master_key, user_key.parse().unwrap(), - private_key.parse().unwrap(), - None, + UserKeyState { + private_key: private_key.parse().unwrap(), + signing_key: None, + security_state: None, + }, ) .unwrap(); 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-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/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 3f279d143..db8633b29 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" diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 544f95ce8..f572cec94 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -115,16 +115,25 @@ 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, + /// 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), + /// 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/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..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; diff --git a/crates/bitwarden-crypto/src/signing/namespace.rs b/crates/bitwarden-crypto/src/signing/namespace.rs index fd48bd2b9..cd805b496 100644 --- a/crates/bitwarden-crypto/src/signing/namespace.rs +++ b/crates/bitwarden-crypto/src/signing/namespace.rs @@ -12,6 +12,8 @@ pub enum SigningNamespace { /// The namespace for /// [`SignedPublicKey`](crate::keys::SignedPublicKey). SignedPublicKey = 1, + /// The namespace for SignedSecurityState + SecurityState = 2, /// This namespace is only used in tests #[cfg(test)] ExampleNamespace = -1, @@ -33,6 +35,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..312cc30ab 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 { @@ -493,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, @@ -515,6 +524,19 @@ 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, + current_user_private_key_id: Ids::Asymmetric, + current_user_signing_key_id: Ids::Signing, + ) -> Result { + crate::dangerous_get_v2_rotated_account_keys( + current_user_private_key_id, + current_user_signing_key_id, + self, + ) + } } #[cfg(test)] @@ -527,9 +549,11 @@ mod tests { tests::{Data, DataView}, KeyStore, }, - traits::tests::{TestIds, TestSigningKey, TestSymmKey}, - CompositeEncryptable, CryptoError, Decryptable, SignatureAlgorithm, SigningKey, - SigningNamespace, SymmetricCryptoKey, + traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CompositeEncryptable, CoseKeyBytes, + CoseSerializable, CryptoError, Decryptable, KeyDecryptable, Pkcs8PrivateKeyBytes, + PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SigningKey, SigningNamespace, + SymmetricCryptoKey, }; #[test] @@ -719,4 +743,90 @@ 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 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( + 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(&rotated_keys.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(&rotated_keys.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 = rotated_keys.signed_public_key; + 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/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index a74edb2ec..60509c69b 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -1,45 +1,47 @@ 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 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) 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 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( - 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)?; + 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 { verifying_key: current_signing_key.to_verifying_key().to_cose(), - signing_key: current_signing_key - .to_cose() - .encrypt_with_key(new_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)?, + signing_key: current_signing_key.to_cose().encrypt_with_key(&user_key)?, + signed_public_key, + public_key: current_public_key.to_der()?, + private_key: current_private_key.to_der()?.encrypt_with_key(&user_key)?, + user_key, }) } @@ -49,7 +51,7 @@ mod tests { use crate::{ traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, - PublicKeyEncryptionAlgorithm, SignedPublicKey, SigningKey, + PublicKeyEncryptionAlgorithm, SigningKey, }; #[test] @@ -58,7 +60,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); @@ -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, @@ -92,7 +92,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)) @@ -108,7 +108,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(); @@ -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) diff --git a/crates/bitwarden-crypto/src/util.rs b/crates/bitwarden-crypto/src/util.rs index 5ab985f7c..a556f9434 100644 --- a/crates/bitwarden-crypto/src/util.rs +++ b/crates/bitwarden-crypto/src/util.rs @@ -53,9 +53,16 @@ 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() + } +} +impl Default for FromStrVisitor { + fn default() -> Self { Self(Default::default()) } } 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 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 122cf99cf..eb1aefa7e 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -253,9 +253,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,