Skip to content

Commit

Permalink
Trusted Device Encryption (#497)
Browse files Browse the repository at this point in the history
Implement trusted device encryption. Verified backwards compatibility
with existing device keys from desktop.
  • Loading branch information
Hinton authored Jan 23, 2024
1 parent ae32c8d commit 43a97fe
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 7 deletions.
9 changes: 9 additions & 0 deletions crates/bitwarden-crypto/src/enc_string/asymmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -137,6 +138,14 @@ impl serde::Serialize for AsymmetricEncString {
}

impl AsymmetricEncString {
pub(crate) fn encrypt_rsa2048_oaep_sha1(
data_dec: &[u8],
key: &AsymmetricCryptoKey,
) -> Result<AsymmetricEncString> {
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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-crypto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, CryptoError>`.
Expand Down
9 changes: 9 additions & 0 deletions crates/bitwarden-crypto/src/keys/asymmetric_crypto_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ pub struct AsymmetricCryptoKey {
}

impl AsymmetricCryptoKey {
/// Generate a random AsymmetricCryptoKey (RSA-2048)
pub fn generate<R: rand::CryptoRng + rand::RngCore>(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<Self> {
use rsa::pkcs8::DecodePrivateKey;
Ok(Self {
Expand Down
126 changes: 126 additions & 0 deletions crates/bitwarden-crypto/src/keys/device_key.rs
Original file line number Diff line number Diff line change
@@ -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<TrustDeviceResponse> {
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<UserKey> {
let device_private_key: Vec<u8> = protected_device_private_key.decrypt_with_key(&self.0)?;
let device_private_key = AsymmetricCryptoKey::from_der(device_private_key.as_slice())?;

let dec: Vec<u8> = 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);
}
}
6 changes: 2 additions & 4 deletions crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
18 changes: 16 additions & 2 deletions crates/bitwarden-crypto/src/rsa.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -42,3 +43,16 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result<RsaKeyPair> {
private: protected,
})
}

pub(super) fn encrypt_rsa2048_oaep_sha1(
private_key: &RsaPrivateKey,
data: &[u8],
) -> Result<Vec<u8>> {
let mut rng = rand::thread_rng();

let padding = Oaep::new::<Sha1>();
private_key
.to_public_key()
.encrypt(&mut rng, padding, data)
.map_err(|e| CryptoError::RsaError(e.into()))
}
19 changes: 18 additions & 1 deletion crates/bitwarden/src/auth/client_auth.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -16,6 +19,7 @@ use crate::{
RegisterKeyResponse, RegisterRequest,
},
client::Kdf,
error::Error,
};

pub struct ClientAuth<'a> {
Expand Down Expand Up @@ -97,6 +101,19 @@ impl<'a> ClientAuth<'a> {
pub async fn validate_password(&self, password: String, password_hash: String) -> Result<bool> {
validate_password(self.client, password, password_hash).await
}

pub async fn trust_device(&self) -> Result<TrustDeviceResponse> {
trust_device(self.client)
}
}

#[cfg(feature = "internal")]
fn trust_device(client: &Client) -> Result<TrustDeviceResponse> {
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 {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 43a97fe

Please sign in to comment.