diff --git a/crates/bitwarden-core/src/auth/auth_request.rs b/crates/bitwarden-core/src/auth/auth_request.rs index 521e60c85..3020222f8 100644 --- a/crates/bitwarden-core/src/auth/auth_request.rs +++ b/crates/bitwarden-core/src/auth/auth_request.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ fingerprint, generate_random_alphanumeric, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, - CryptoError, PublicKeyEncryptionAlgorithm, UnsignedSharedKey, + CryptoError, PublicKeyEncryptionAlgorithm, SpkiPublicKeyBytes, UnsignedSharedKey, }; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, SymmetricCryptoKey}; @@ -91,7 +91,9 @@ pub(crate) fn approve_auth_request( client: &Client, public_key: String, ) -> Result<UnsignedSharedKey, ApproveAuthRequestError> { - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( + STANDARD.decode(public_key)?, + ))?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index 570a28fd2..9b8c5db9c 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SymmetricCryptoKey, TrustDeviceResponse, - UnsignedSharedKey, UserKey, + AsymmetricPublicCryptoKey, DeviceKey, EncString, Kdf, SpkiPublicKeyBytes, SymmetricCryptoKey, + TrustDeviceResponse, UnsignedSharedKey, UserKey, }; use crate::{client::encryption_settings::EncryptionSettingsError, Client}; @@ -15,7 +15,9 @@ pub(super) fn make_register_tde_keys( org_public_key: String, remember_device: bool, ) -> Result<RegisterTdeKeyResponse, EncryptionSettingsError> { - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(org_public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( + STANDARD.decode(org_public_key)?, + ))?; let user_key = UserKey::new(SymmetricCryptoKey::make_aes256_cbc_hmac_key()); let key_pair = user_key.make_key_pair()?; diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index a76ea4294..97b6402dc 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,9 +8,10 @@ use std::collections::HashMap; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm, - SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError, + EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, + PrimitiveEncryptable, RotatedUserKeys, SignatureAlgorithm, SignedPublicKey, SigningKey, + SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; use bitwarden_error::bitwarden_error; use schemars::JsonSchema; @@ -412,7 +413,9 @@ pub(super) fn enroll_admin_password_reset( use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::AsymmetricPublicCryptoKey; - let public_key = AsymmetricPublicCryptoKey::from_der(&STANDARD.decode(public_key)?)?; + let public_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( + STANDARD.decode(public_key)?, + ))?; let key_store = client.internal.get_key_store(); let ctx = key_store.context(); // FIXME: [PM-18110] This should be removed once the key store can handle public key encryption @@ -613,6 +616,56 @@ pub fn make_user_signing_keys_for_enrollment( }) } +/// 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<RotatedUserKeys> 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<RotateUserKeysResponse, CryptoError> { + let key_store = client.internal.get_key_store(); + let ctx = key_store.context(); + + dangerous_get_v2_rotated_account_keys( + &SymmetricCryptoKey::try_from(user_key)?, + AsymmetricKeyId::UserPrivateKey, + SigningKeyId::UserSigningKey, + &ctx, + ) + .map(Into::into) +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 03390939a..5e88f3326 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -18,7 +18,10 @@ use crate::key_management::crypto::{ }; use crate::{ client::encryption_settings::EncryptionSettingsError, - key_management::crypto::CryptoClientError, Client, + key_management::crypto::{ + get_v2_rotated_account_keys, CryptoClientError, RotateUserKeysResponse, + }, + Client, }; /// A client for the crypto operations. @@ -69,6 +72,14 @@ impl CryptoClient { ) -> Result<MakeUserSigningKeysResponse, CryptoError> { make_user_signing_keys_for_enrollment(&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<RotateUserKeysResponse, CryptoError> { + get_v2_rotated_account_keys(&self.client, user_key) + } } impl CryptoClient { diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index 14a400992..d65bd1bd4 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -17,14 +17,14 @@ pub enum PublicKeyEncryptionAlgorithm { RsaOaepSha1 = 0, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub(crate) enum RawPublicKey { RsaOaepSha1(RsaPublicKey), } /// Public key of a key pair used in a public key encryption scheme. It is used for /// encrypting data. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct AsymmetricPublicCryptoKey { inner: RawPublicKey, } @@ -35,10 +35,11 @@ impl AsymmetricPublicCryptoKey { } /// Build a public key from the SubjectPublicKeyInfo DER. - pub fn from_der(der: &[u8]) -> Result<Self> { + pub fn from_der(der: &SpkiPublicKeyBytes) -> Result<Self> { Ok(AsymmetricPublicCryptoKey { inner: RawPublicKey::RsaOaepSha1( - RsaPublicKey::from_public_key_der(der).map_err(|_| CryptoError::InvalidKey)?, + RsaPublicKey::from_public_key_der(der.as_ref()) + .map_err(|_| CryptoError::InvalidKey)?, ), }) } @@ -166,8 +167,8 @@ mod tests { use crate::{ content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat}, - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, Pkcs8PrivateKeyBytes, SymmetricCryptoKey, - UnsignedSharedKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes, + SymmetricCryptoKey, UnsignedSharedKey, }; #[test] @@ -263,7 +264,8 @@ DnqOsltgPomWZ7xVfMkm9niL2OA= let private_key = Pkcs8PrivateKeyBytes::from(private_key); let private_key = AsymmetricCryptoKey::from_der(&private_key).unwrap(); - let public_key = AsymmetricPublicCryptoKey::from_der(&public_key).unwrap(); + let public_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(public_key)).unwrap(); let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap(); diff --git a/crates/bitwarden-crypto/src/keys/signed_public_key.rs b/crates/bitwarden-crypto/src/keys/signed_public_key.rs index 25407e219..5951ccf0d 100644 --- a/crates/bitwarden-crypto/src/keys/signed_public_key.rs +++ b/crates/bitwarden-crypto/src/keys/signed_public_key.rs @@ -13,7 +13,7 @@ use super::AsymmetricPublicCryptoKey; use crate::{ cose::CoseSerializable, error::EncodingError, util::FromStrVisitor, CoseSign1Bytes, CryptoError, PublicKeyEncryptionAlgorithm, RawPublicKey, SignedObject, SigningKey, - SigningNamespace, VerifyingKey, + SigningNamespace, SpkiPublicKeyBytes, VerifyingKey, }; #[cfg(feature = "wasm")] @@ -114,8 +114,10 @@ impl SignedPublicKey { public_key_message.content_format, ) { (PublicKeyEncryptionAlgorithm::RsaOaepSha1, PublicKeyFormat::Spki) => Ok( - AsymmetricPublicCryptoKey::from_der(&public_key_message.public_key.into_vec()) - .map_err(|_| EncodingError::InvalidValue("public key"))?, + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( + public_key_message.public_key.to_vec(), + )) + .map_err(|_| EncodingError::InvalidValue("public key"))?, ), } } diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 60428d8a4..d4cc15f45 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -31,7 +31,9 @@ pub use util::{generate_random_alphanumeric, generate_random_bytes, pbkdf2}; mod wordlist; pub use wordlist::EFF_LONG_WORD_LIST; mod store; -pub use store::{KeyStore, KeyStoreContext}; +pub use store::{ + dangerous_get_v2_rotated_account_keys, KeyStore, KeyStoreContext, RotatedUserKeys, +}; mod cose; pub use cose::CoseSerializable; mod signing; diff --git a/crates/bitwarden-crypto/src/store/context.rs b/crates/bitwarden-crypto/src/store/context.rs index 9675cfb5e..2df08a1f8 100644 --- a/crates/bitwarden-crypto/src/store/context.rs +++ b/crates/bitwarden-crypto/src/store/context.rs @@ -378,7 +378,10 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> { + pub(super) fn get_asymmetric_key( + &self, + key_id: Ids::Asymmetric, + ) -> Result<&AsymmetricCryptoKey> { if key_id.is_local() { self.local_asymmetric_keys.get(key_id) } else { @@ -387,7 +390,7 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> { .ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}"))) } - fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { + pub(super) fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> { if key_id.is_local() { self.local_signing_keys.get(key_id) } else { diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs new file mode 100644 index 000000000..a74edb2ec --- /dev/null +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -0,0 +1,140 @@ +use crate::{ + CoseKeyBytes, CoseSerializable, CoseSign1Bytes, CryptoError, EncString, KeyEncryptable, KeyIds, + KeyStoreContext, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey, +}; + +/// Rotated set of account keys +pub struct RotatedUserKeys { + /// 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, + /// 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. +pub fn dangerous_get_v2_rotated_account_keys<Ids: KeyIds>( + new_user_key: &SymmetricCryptoKey, + current_user_private_key_id: Ids::Asymmetric, + current_user_signing_key_id: Ids::Signing, + ctx: &KeyStoreContext<Ids>, +) -> Result<RotatedUserKeys, CryptoError> { + 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 signed_public_key = + SignedPublicKeyMessage::from_public_key(¤t_private_key.to_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)?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + traits::tests::{TestAsymmKey, TestIds, TestSigningKey, TestSymmKey}, + AsymmetricCryptoKey, KeyDecryptable, KeyStore, Pkcs8PrivateKeyBytes, + PublicKeyEncryptionAlgorithm, SignedPublicKey, SigningKey, + }; + + #[test] + fn test_account_key_rotation() { + let store: KeyStore<TestIds> = 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(); + #[allow(deprecated)] + ctx.set_asymmetric_key( + current_user_private_key_id, + AsymmetricCryptoKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1), + ) + .unwrap(); + + // 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, + ) + .unwrap(); + + // Public/Private key + assert_eq!( + rotated_keys.public_key, + ctx.get_asymmetric_key(current_user_private_key_id) + .unwrap() + .to_public_key() + .to_der() + .unwrap() + ); + let decrypted_private_key: Vec<u8> = 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<u8> = 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/store/mod.rs b/crates/bitwarden-crypto/src/store/mod.rs index 765762bac..c1425bf30 100644 --- a/crates/bitwarden-crypto/src/store/mod.rs +++ b/crates/bitwarden-crypto/src/store/mod.rs @@ -35,6 +35,9 @@ use backend::{create_store, StoreBackend}; use context::GlobalKeys; pub use context::KeyStoreContext; +mod key_rotation; +pub use key_rotation::*; + /// An in-memory key store that provides a safe and secure way to store keys and use them for /// encryption/decryption operations. The store API is designed to work only on key identifiers /// ([KeyId]). These identifiers are user-defined types that contain no key material, which means diff --git a/crates/bitwarden-wasm-internal/src/pure_crypto.rs b/crates/bitwarden-wasm-internal/src/pure_crypto.rs index 13ab4da05..20f8588a7 100644 --- a/crates/bitwarden-wasm-internal/src/pure_crypto.rs +++ b/crates/bitwarden-wasm-internal/src/pure_crypto.rs @@ -251,7 +251,9 @@ impl PureCrypto { shared_key: Vec<u8>, encapsulation_key: Vec<u8>, ) -> Result<String, CryptoError> { - let encapsulation_key = AsymmetricPublicCryptoKey::from_der(encapsulation_key.as_slice())?; + let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from( + encapsulation_key.as_slice(), + ))?; Ok(UnsignedSharedKey::encapsulate_key_unsigned( &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(shared_key))?, &encapsulation_key,