Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trusted Device Encryption #497

Merged
merged 22 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]

Check warning on line 10 in crates/bitwarden-crypto/src/keys/device_key.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-crypto/src/keys/device_key.rs#L10

Added line #L10 was not covered by tests
pub struct DeviceKey(SymmetricCryptoKey);

#[derive(Debug)]

Check warning on line 13 in crates/bitwarden-crypto/src/keys/device_key.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-crypto/src/keys/device_key.rs#L13

Added line #L13 was not covered by tests
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 @@
RegisterKeyResponse, RegisterRequest,
},
client::Kdf,
error::Error,
};

pub struct ClientAuth<'a> {
Expand Down Expand Up @@ -97,6 +101,19 @@
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)
}

Check warning on line 107 in crates/bitwarden/src/auth/client_auth.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden/src/auth/client_auth.rs#L105-L107

Added lines #L105 - L107 were not covered by tests
}

#[cfg(feature = "internal")]
fn trust_device(client: &Client) -> Result<TrustDeviceResponse> {
let enc = client.get_encryption_settings()?;

Check warning on line 112 in crates/bitwarden/src/auth/client_auth.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden/src/auth/client_auth.rs#L111-L112

Added lines #L111 - L112 were not covered by tests

let user_key = enc.get_key(&None).ok_or(Error::VaultLocked)?;

Check warning on line 114 in crates/bitwarden/src/auth/client_auth.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden/src/auth/client_auth.rs#L114

Added line #L114 was not covered by tests

Ok(DeviceKey::trust_device(user_key)?)

Check warning on line 116 in crates/bitwarden/src/auth/client_auth.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden/src/auth/client_auth.rs#L116

Added line #L116 was not covered by tests
}

impl<'a> Client {
Expand Down Expand Up @@ -172,7 +189,7 @@
.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
Loading