From 43a97febf83f872a7e6fd047d34cf8eca01d3685 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 23 Jan 2024 16:25:44 +0100 Subject: [PATCH] Trusted Device Encryption (#497) Implement trusted device encryption. Verified backwards compatibility with existing device keys from desktop. --- .../src/enc_string/asymmetric.rs | 9 ++ crates/bitwarden-crypto/src/error.rs | 2 + .../src/keys/asymmetric_crypto_key.rs | 9 ++ .../bitwarden-crypto/src/keys/device_key.rs | 126 ++++++++++++++++++ crates/bitwarden-crypto/src/keys/mod.rs | 6 +- crates/bitwarden-crypto/src/rsa.rs | 18 ++- crates/bitwarden/src/auth/client_auth.rs | 19 ++- 7 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 crates/bitwarden-crypto/src/keys/device_key.rs diff --git a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs index 81ac81a5b..953b3d28f 100644 --- a/crates/bitwarden-crypto/src/enc_string/asymmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/asymmetric.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use super::{from_b64_vec, split_enc_string}; use crate::{ error::{CryptoError, EncStringParseError, Result}, + rsa::encrypt_rsa2048_oaep_sha1, AsymmetricCryptoKey, KeyDecryptable, }; @@ -137,6 +138,14 @@ impl serde::Serialize for AsymmetricEncString { } impl AsymmetricEncString { + pub(crate) fn encrypt_rsa2048_oaep_sha1( + data_dec: &[u8], + key: &AsymmetricCryptoKey, + ) -> Result { + let enc = encrypt_rsa2048_oaep_sha1(&key.key, data_dec)?; + Ok(AsymmetricEncString::Rsa2048_OaepSha1_B64 { data: enc }) + } + /// The numerical representation of the encryption type of the [AsymmetricEncString]. const fn enc_type(&self) -> u8 { match self { diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 059cc88f1..cf9a9b048 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -52,6 +52,8 @@ pub enum RsaError { CreatePublicKey, #[error("Unable to create private key")] CreatePrivateKey, + #[error("Rsa error, {0}")] + Rsa(#[from] rsa::Error), } /// Alias for `Result`. diff --git a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs index a3e06800b..142013d4e 100644 --- a/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs +++ b/crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs @@ -9,6 +9,15 @@ pub struct AsymmetricCryptoKey { } impl AsymmetricCryptoKey { + /// Generate a random AsymmetricCryptoKey (RSA-2048) + pub fn generate(rng: &mut R) -> Self { + let bits = 2048; + + Self { + key: RsaPrivateKey::new(rng, bits).expect("failed to generate a key"), + } + } + pub fn from_pem(pem: &str) -> Result { use rsa::pkcs8::DecodePrivateKey; Ok(Self { diff --git a/crates/bitwarden-crypto/src/keys/device_key.rs b/crates/bitwarden-crypto/src/keys/device_key.rs new file mode 100644 index 000000000..6588944fe --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/device_key.rs @@ -0,0 +1,126 @@ +use crate::{ + error::Result, AsymmetricCryptoKey, AsymmetricEncString, EncString, KeyDecryptable, + KeyEncryptable, SymmetricCryptoKey, UserKey, +}; + +/// Device Key +/// +/// Encrypts the DevicePrivateKey +/// Allows the device to decrypt the UserKey, via the DevicePrivateKey. +#[derive(Debug)] +pub struct DeviceKey(SymmetricCryptoKey); + +#[derive(Debug)] +pub struct TrustDeviceResponse { + pub device_key: DeviceKey, + /// UserKey encrypted with DevicePublicKey + pub protected_user_key: AsymmetricEncString, + /// DevicePrivateKey encrypted with [DeviceKey] + pub protected_device_private_key: EncString, + /// DevicePublicKey encrypted with [UserKey](super::UserKey) + pub protected_device_public_key: EncString, +} + +impl DeviceKey { + /// Generate a new device key + /// + /// Note: Input has to be a SymmetricCryptoKey instead of UserKey because that's what we get + /// from EncSettings. + pub fn trust_device(user_key: &SymmetricCryptoKey) -> Result { + let mut rng = rand::thread_rng(); + let device_key = DeviceKey(SymmetricCryptoKey::generate(&mut rng)); + + let device_private_key = AsymmetricCryptoKey::generate(&mut rng); + + // Encrypt both the key and mac_key of the user key + let data = user_key.to_vec(); + + let protected_user_key = + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(&data, &device_private_key)?; + + let protected_device_public_key = device_private_key + .to_public_der()? + .encrypt_with_key(user_key)?; + + let protected_device_private_key = device_private_key + .to_der()? + .encrypt_with_key(&device_key.0)?; + + Ok(TrustDeviceResponse { + device_key, + protected_user_key, + protected_device_private_key, + protected_device_public_key, + }) + } + + /// Decrypt the user key using the device key + pub fn decrypt_user_key( + &self, + protected_device_private_key: EncString, + protected_user_key: AsymmetricEncString, + ) -> Result { + let device_private_key: Vec = protected_device_private_key.decrypt_with_key(&self.0)?; + let device_private_key = AsymmetricCryptoKey::from_der(device_private_key.as_slice())?; + + let dec: Vec = protected_user_key.decrypt_with_key(&device_private_key)?; + let user_key: SymmetricCryptoKey = dec.as_slice().try_into()?; + + Ok(UserKey(user_key)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::derive_symmetric_key; + + #[test] + fn test_trust_device() { + let key = derive_symmetric_key("test"); + + let result = DeviceKey::trust_device(&key).unwrap(); + + let decrypted = result + .device_key + .decrypt_user_key( + result.protected_device_private_key, + result.protected_user_key, + ) + .unwrap(); + + assert_eq!(key.key, decrypted.0.key); + assert_eq!(key.mac_key, decrypted.0.mac_key); + } + + #[test] + fn test_decrypt_user_key() { + // Example keys from desktop app + let user_key: &[u8] = &[ + 109, 128, 172, 147, 206, 123, 134, 95, 16, 36, 155, 113, 201, 18, 186, 230, 216, 212, + 173, 188, 74, 11, 134, 131, 137, 242, 105, 178, 105, 126, 52, 139, 248, 91, 215, 21, + 128, 91, 226, 222, 165, 67, 251, 34, 83, 81, 77, 147, 225, 76, 13, 41, 102, 45, 183, + 218, 106, 89, 254, 208, 251, 101, 130, 10, + ]; + let user_key = SymmetricCryptoKey::try_from(user_key).unwrap(); + + let key_data: &[u8] = &[ + 114, 235, 60, 115, 172, 156, 203, 145, 195, 130, 215, 250, 88, 146, 215, 230, 12, 109, + 245, 222, 54, 217, 255, 211, 221, 105, 230, 236, 65, 52, 209, 133, 76, 208, 113, 254, + 194, 216, 156, 19, 230, 62, 32, 93, 87, 7, 144, 156, 117, 142, 250, 32, 182, 118, 187, + 8, 247, 7, 203, 201, 65, 147, 206, 247, + ]; + let device_key = DeviceKey(key_data.try_into().unwrap()); + + let protected_user_key: AsymmetricEncString = "4.f+VbbacRhO2q4MOUSdt1AIjQ2FuLAvg4aDxJMXAh3VxvbmUADj8Ct/R7XEpPUqApmbRS566jS0eRVy8Sk08ogoCdj1IFN9VsIky2i2X1WHK1fUnr3UBmXE3tl2NPBbx56U+h73S2jNTSyet2W18Jg2q7/w8KIhR3J41QrG9aGoOTN93to3hb5W4z6rdrSI0e7GkizbwcIA0NH7Z1JyAhrjPm9+tjRjg060YbEbGaWTAOkZWfgbLjr8bY455DteO2xxG139cOx7EBo66N+YhjsLi0ozkeUyPQkoWBdKMcQllS7jCfB4fDyJA05ALTbk74syKkvqFxqwmQbg+aVn+dcw==".parse().unwrap(); + + let protected_device_private_key: EncString = "2.GyQfUYWW6Byy4UV5icFLxg==|EMiU7OTF79N6tfv3+YUs5zJhBAgqv6sa5YCoPl6yAETh7Tfk+JmbeizxXFPj5Q1X/tcVpDZl/3fGcxtnIxg1YtvDFn7j8uPnoApOWhCKmwcvJSIkt+qvX3lELNBwZXozSiy7PbQ0JbCMe2d4MkimR5k8+lE9FB3208yYK7nOJhlrsUCnOekCYEU9/4NCMA8tz8SpITx/MN4JJ1TQ/KjPJYLt+3JNUxK47QlgREWQvyVzCRt7ZGtcgIJ/U1qycAWMpEg9NkuV8j5QRA1S7VBsA6qliJwys5+dmTuIOmOMwdKFZDc4ZvWoRkPp2TSJBu7L8sSAgU6mmDWac8iQ+9Ka/drdfwYLrH8GAZvURk79tSpRrT7+PAFe2QdUtliUIyiqkh8iJVjZube4hRnEsRuX9V9b+UdtAr6zAj7mugO/VAu5T9J38V79V2ohG3NtXysDeKLXpAlkhjllWXeq/wret2fD4WiwqEDj0G2A/PY3F3OziIgp0UKc00AfqrPq8OVK3A+aowwVqdYadgxyoVCKWJ8unJeAXG7MrMQ9tHpzF6COoaEy7Wwoc17qko33zazwLZbfAjB4oc8Ea26jRKnJZP56sVZAjOSQQMziAsA08MRaa/DQhgRea1+Ygba0gMft8Dww8anN2gQBveTZRBWyqXYgN3U0Ity5gNauT8RnFk9faqVFt2Qxnp0JgJ+PsqEt5Hn4avBRZQQ7o8VvPnxYLDKFe3I2m6HFYFWRhOGeDYxexIuaiF2iIAYFVUmnDuWpgnUiL4XJ3KHDsjkPzcV3z4D2Knr/El2VVXve8jhDjETfovmmN28+i2e29PXvKIymTskMFpFCQPc7wBY/Id7pmgb3SujKYNpkAS2sByDoRir0my49DDGfta0dENssJhFd3x+87fZbEj3cMiikg2pBwpTLgmfIUa5cVZU2s8JZ9wu7gaioYzvX+elHa3EHLcnEUoJTtSf9kjb+Nbq4ktMgYAO2wIC96t1LvmqK4Qn2cOdw5QNlRqALhqe5V31kyIcwRMK0AyIoOPhnSqtpYdFiR3LDTvZA8dU0vSsuchCwHNMeRUtKvdzN/tk+oeznyY/mpakUESN501lEKd/QFLtJZsDZTtNlcA8fU3kDtws4ZIMR0O5+PFmgQFSU8OMobf9ClUzy/wHTvYGyDuSwbOoPeS955QKkUKXCNMj33yrPr+ioHQ1BNwLX3VmMF4bNRBY/vr+CG0/EZi0Gwl0kyHGl0yWEtpQuu+/PaROJeOraWy5D1UoZZhY4n0zJZBt1eg3FZ2rhKv4gdUc50nZpeNWE8pIqZ6RQ7qPJuqfF1Z+G73iOSnLYCHDiiFmhD5ivf9IGkTAcWcBsQ/2wcSj9bFJr4DrKfsbQ4CkSWICWVn/W+InKkO6BTsBbYmvte5SvbaN+UOtiUSkHLBCCr8273VNgcB/hgtbUires3noxYZJxoczr+i7vdlEgQnWEKrpo0CifsFxGwYS3Yy2K79iwvDMaLPDf73zLSbuoUl6602F2Mzcjnals67f+gSpaDvWt7Kg9c/ZfGjq8oNxVaXJnX3gSDsO+fhwVAtnDApL+tL8cFfxGerW4KGi9/74woH+C3MMIViBtNnrpEuvxUW97Dg5nd40oGDeyi/q+8HdcxkneyFY=|JYdol19Yi+n1r7M+06EwK5JCi2s/CWqKui2Cy6hEb3k=".parse().unwrap(); + + let decrypted = device_key + .decrypt_user_key(protected_device_private_key, protected_user_key) + .unwrap(); + + assert_eq!(decrypted.0.key, user_key.key); + assert_eq!(decrypted.0.mac_key, user_key.mac_key); + } +} diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 4f53456ef..561fd8436 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -1,18 +1,16 @@ mod key_encryptable; pub use key_encryptable::{KeyDecryptable, KeyEncryptable}; - mod master_key; pub use master_key::{HashPurpose, Kdf, MasterKey}; - mod shareable_key; pub use shareable_key::derive_shareable_key; - mod symmetric_crypto_key; #[cfg(test)] pub use symmetric_crypto_key::derive_symmetric_key; pub use symmetric_crypto_key::SymmetricCryptoKey; mod asymmetric_crypto_key; pub use asymmetric_crypto_key::AsymmetricCryptoKey; - mod user_key; pub use user_key::UserKey; +mod device_key; +pub use device_key::{DeviceKey, TrustDeviceResponse}; diff --git a/crates/bitwarden-crypto/src/rsa.rs b/crates/bitwarden-crypto/src/rsa.rs index ff665c247..52dd572aa 100644 --- a/crates/bitwarden-crypto/src/rsa.rs +++ b/crates/bitwarden-crypto/src/rsa.rs @@ -1,12 +1,13 @@ use base64::{engine::general_purpose::STANDARD, Engine}; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, - RsaPrivateKey, RsaPublicKey, + Oaep, RsaPrivateKey, RsaPublicKey, }; +use sha1::Sha1; use crate::{ error::{Result, RsaError}, - EncString, SymmetricCryptoKey, + CryptoError, EncString, SymmetricCryptoKey, }; /// RSA Key Pair @@ -42,3 +43,16 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result { private: protected, }) } + +pub(super) fn encrypt_rsa2048_oaep_sha1( + private_key: &RsaPrivateKey, + data: &[u8], +) -> Result> { + let mut rng = rand::thread_rng(); + + let padding = Oaep::new::(); + private_key + .to_public_key() + .encrypt(&mut rng, padding, data) + .map_err(|e| CryptoError::RsaError(e.into())) +} diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index d4379d1eb..c0a5e7aa3 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "internal")] +use bitwarden_crypto::{DeviceKey, TrustDeviceResponse}; + #[cfg(feature = "secrets")] use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse}; use crate::{auth::renew::renew_token, error::Result, Client}; @@ -16,6 +19,7 @@ use crate::{ RegisterKeyResponse, RegisterRequest, }, client::Kdf, + error::Error, }; pub struct ClientAuth<'a> { @@ -97,6 +101,19 @@ impl<'a> ClientAuth<'a> { pub async fn validate_password(&self, password: String, password_hash: String) -> Result { validate_password(self.client, password, password_hash).await } + + pub async fn trust_device(&self) -> Result { + trust_device(self.client) + } +} + +#[cfg(feature = "internal")] +fn trust_device(client: &Client) -> Result { + let enc = client.get_encryption_settings()?; + + let user_key = enc.get_key(&None).ok_or(Error::VaultLocked)?; + + Ok(DeviceKey::trust_device(user_key)?) } impl<'a> Client { @@ -172,7 +189,7 @@ mod tests { .login_access_token(&AccessTokenLoginRequest { access_token: "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==".into(), state_file: None, - },) + }) .await .unwrap(); assert!(res.authenticated);