diff --git a/crates/bitwarden-crypto/src/keys/master_key.rs b/crates/bitwarden-crypto/src/keys/master_key.rs index 920f103e4..0a435ed88 100644 --- a/crates/bitwarden-crypto/src/keys/master_key.rs +++ b/crates/bitwarden-crypto/src/keys/master_key.rs @@ -39,6 +39,10 @@ pub enum HashPurpose { pub struct MasterKey(SymmetricCryptoKey); impl MasterKey { + pub fn new(key: SymmetricCryptoKey) -> MasterKey { + Self(key) + } + /// Derives a users master key from their password, email and KDF. pub fn derive(password: &[u8], email: &[u8], kdf: &Kdf) -> Result { derive_key(password, email, kdf).map(Self) diff --git a/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs b/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs new file mode 100644 index 000000000..cf5ae7ee4 --- /dev/null +++ b/crates/bitwarden/src/auth/api/request/auth_request_token_request.rs @@ -0,0 +1,59 @@ +use log::debug; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + auth::api::response::IdentityTokenResponse, + client::{client_settings::DeviceType, ApiConfigurations}, + error::Result, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthRequestTokenRequest { + scope: String, + client_id: String, + #[serde(rename = "deviceType")] + device_type: u8, + #[serde(rename = "deviceIdentifier")] + device_identifier: String, + #[serde(rename = "deviceName")] + device_name: String, + grant_type: String, + #[serde(rename = "username")] + email: String, + #[serde(rename = "authRequest")] + auth_request_id: Uuid, + #[serde(rename = "password")] + access_code: String, +} + +impl AuthRequestTokenRequest { + pub fn new( + email: &str, + auth_request_id: &Uuid, + access_code: &str, + device_type: DeviceType, + device_identifier: &str, + ) -> Self { + let obj = Self { + scope: "api offline_access".to_string(), + client_id: "web".to_string(), + device_type: device_type as u8, + device_identifier: device_identifier.to_string(), + device_name: "chrome".to_string(), + grant_type: "password".to_string(), + email: email.to_string(), + auth_request_id: *auth_request_id, + access_code: access_code.to_string(), + }; + debug!("initializing {:?}", obj); + obj + } + + pub(crate) async fn send( + &self, + configurations: &ApiConfigurations, + ) -> Result { + super::send_identity_connect_request(configurations, Some(&self.email), &self).await + } +} diff --git a/crates/bitwarden/src/auth/api/request/mod.rs b/crates/bitwarden/src/auth/api/request/mod.rs index 67796f2f3..2b5bde225 100644 --- a/crates/bitwarden/src/auth/api/request/mod.rs +++ b/crates/bitwarden/src/auth/api/request/mod.rs @@ -15,6 +15,11 @@ pub(crate) use password_token_request::*; #[cfg(feature = "internal")] pub(crate) use renew_token_request::*; +#[cfg(feature = "mobile")] +mod auth_request_token_request; +#[cfg(feature = "mobile")] +pub(crate) use auth_request_token_request::*; + use crate::{ auth::api::response::{parse_identity_response, IdentityTokenResponse}, client::ApiConfigurations, diff --git a/crates/bitwarden/src/auth/api/request/password_token_request.rs b/crates/bitwarden/src/auth/api/request/password_token_request.rs index fd016d898..2f6414bcd 100644 --- a/crates/bitwarden/src/auth/api/request/password_token_request.rs +++ b/crates/bitwarden/src/auth/api/request/password_token_request.rs @@ -6,7 +6,7 @@ use crate::{ api::response::IdentityTokenResponse, login::{TwoFactorProvider, TwoFactorRequest}, }, - client::ApiConfigurations, + client::{client_settings::DeviceType, ApiConfigurations}, error::Result, }; @@ -35,13 +35,19 @@ pub struct PasswordTokenRequest { } impl PasswordTokenRequest { - pub fn new(email: &str, password_hash: &String, two_factor: &Option) -> Self { + pub fn new( + email: &str, + password_hash: &str, + device_type: DeviceType, + device_identifier: &str, + two_factor: &Option, + ) -> Self { let tf = two_factor.as_ref(); let obj = Self { scope: "api offline_access".to_string(), client_id: "web".to_string(), - device_type: 10, - device_identifier: "b86dd6ab-4265-4ddf-a7f1-eb28d5677f33".to_string(), + device_type: device_type as u8, + device_identifier: device_identifier.to_string(), device_name: "firefox".to_string(), grant_type: "password".to_string(), master_password_hash: password_hash.to_string(), diff --git a/crates/bitwarden/src/auth/auth_request.rs b/crates/bitwarden/src/auth/auth_request.rs index 98fe4996a..18c71afba 100644 --- a/crates/bitwarden/src/auth/auth_request.rs +++ b/crates/bitwarden/src/auth/auth_request.rs @@ -3,7 +3,7 @@ use bitwarden_crypto::{ fingerprint, AsymmetricCryptoKey, AsymmetricEncString, AsymmetricPublicCryptoKey, }; #[cfg(feature = "mobile")] -use bitwarden_crypto::{KeyDecryptable, SymmetricCryptoKey}; +use bitwarden_crypto::{EncString, KeyDecryptable, SymmetricCryptoKey}; use bitwarden_generators::{password, PasswordGeneratorRequest}; use crate::{error::Error, Client}; @@ -63,6 +63,22 @@ pub(crate) fn auth_request_decrypt_user_key( Ok(SymmetricCryptoKey::try_from(key.as_mut_slice())?) } +/// Decrypt the user key using the private key generated previously. +#[cfg(feature = "mobile")] +pub(crate) fn auth_request_decrypt_master_key( + private_key: String, + master_key: AsymmetricEncString, + user_key: EncString, +) -> Result { + use bitwarden_crypto::MasterKey; + + let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; + let mut master_key: Vec = master_key.decrypt_with_key(&key)?; + let master_key = MasterKey::new(SymmetricCryptoKey::try_from(master_key.as_mut_slice())?); + + Ok(master_key.decrypt_user_key(user_key)?) +} + /// Approve an auth request. /// /// Encrypts the user key with a public key. @@ -113,14 +129,14 @@ mod tests { use super::*; use crate::{ client::{LoginMethod, UserLoginMethod}, - mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, }; #[test] fn test_approve() { let mut client = Client::new(None); client.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "123".to_owned(), + client_id: "7b821276-e27c-400b-9853-606393c87f18".to_owned(), email: "test@bitwarden.com".to_owned(), kdf: Kdf::PBKDF2 { iterations: NonZeroU32::new(600_000).unwrap(), @@ -133,16 +149,53 @@ mod tests { .initialize_user_crypto("asdfasdfasdf", user_key, private_key) .unwrap(); - let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRtpYLp9QLaEUkdPkWZX6TrMUKFoSaFamBKDL0NlS6xwtETTqYIxRVsvnHii3Dhz+fh3aHQVyBa1rBXogeH3MLERzNADwZhpWtBT9wKCXY5o0fIWYdZV/Nf0Y+0ZoKdImrGPLPmyHGfCqrvrK7g09q8+3kXUlkdAImlQqc5TiYwiHBfUQVTBq/Ae7a0FEpajx1NUM4h3edpCYxbvnpSTuzMgbmbUUS4gdCaheA2ibYxy/zkLzsaLygoibMyGNl9Y8J5n7dDrVXpUKZTihVfXwHfEZwtKNunWsmmt8rEJWVpguUDEDVSUogoxQcNaCi7KHn9ioSip76hg1jLpypO3WwIDAQAB"; + let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB"; // Verify fingerprint let pbkey = STANDARD.decode(public_key).unwrap(); let fingerprint = fingerprint("test@bitwarden.com", &pbkey).unwrap(); - assert_eq!(fingerprint, "spill-applaud-sweep-habitable-shrunk"); + assert_eq!(fingerprint, "childless-unfair-prowler-dropbox-designate"); approve_auth_request(&mut client, public_key.to_owned()).unwrap(); } + #[tokio::test] + async fn test_decrypt_user_key() { + let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; + + let enc_user_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); + let dec = auth_request_decrypt_user_key(private_key.to_owned(), enc_user_key).unwrap(); + + assert_eq!( + dec.to_vec().as_ref(), + vec![ + 201, 37, 234, 213, 21, 75, 40, 70, 149, 213, 234, 16, 19, 251, 162, 245, 161, 74, + 34, 245, 211, 151, 211, 192, 95, 10, 117, 50, 88, 223, 23, 157 + ] + ); + } + + #[tokio::test] + async fn test_decrypt_master_key() { + let private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLtEUdxfcLxDj84yaGFsVF5hZ8Hjlb08NMQDy1RnBma06I3ZESshLYzVz4r/gegMn9OOltfV/Yxlyvida8oW6qdlfJ7AVz6Oa8pV7BiL40C7b76+oqraQpyYw2HChANB1AhXL9SqWngKmLZwjA7qiCrmcc0kZHeOb4KnKtp9iVvPVs+8veFvKgYO4ba2AAOHKFdR0W55/agXfAy+fWUAkC8mc9ikyJdQWaPV6OZvC2XFkOseBQm9Rynudh3BQpoWiL6w620efe7t5k+02/EyOFJL9f/XEEjM/+Yo0t3LAfkuhHGeKiRST59Xc9hTEmyJTeVXROtz+0fjqOp3xkaObAgMBAAECggEACs4xhnO0HaZhh1/iH7zORMIRXKeyxP2LQiTR8xwN5JJ9wRWmGAR9VasS7EZFTDidIGVME2u/h4s5EqXnhxfO+0gGksVvgNXJ/qw87E8K2216g6ZNo6vSGA7H1GH2voWwejJ4/k/cJug6dz2S402rRAKh2Wong1arYHSkVlQp3diiMa5FHAOSE+Cy09O2ZsaF9IXQYUtlW6AVXFrBEPYH2kvkaPXchh8VETMijo6tbvoKLnUHe+wTaDMls7hy8exjtVyI59r3DNzjy1lNGaGb5QSnFMXR+eHhPZc844Wv02MxC15zKABADrl58gpJyjTl6XpDdHCYGsmGpVGH3X9TQQKBgQDz/9beFjzq59ve6rGwn+EtnQfSsyYT+jr7GN8lNEXb3YOFXBgPhfFIcHRh2R00Vm9w2ApfAx2cd8xm2I6HuvQ1Os7g26LWazvuWY0Qzb+KaCLQTEGH1RnTq6CCG+BTRq/a3J8M4t38GV5TWlzv8wr9U4dl6FR4efjb65HXs1GQ4QKBgQC7/uHfrOTEHrLeIeqEuSl0vWNqEotFKdKLV6xpOvNuxDGbgW4/r/zaxDqt0YBOXmRbQYSEhmO3oy9J6XfE1SUln0gbavZeW0HESCAmUIC88bDnspUwS9RxauqT5aF8ODKN/bNCWCnBM1xyonPOs1oT1nyparJVdQoG//Y7vkB3+wKBgBqLqPq8fKAp3XfhHLfUjREDVoiLyQa/YI9U42IOz9LdxKNLo6p8rgVthpvmnRDGnpUuS+KOWjhdqDVANjF6G3t3DG7WNl8Rh5Gk2H4NhFswfSkgQrjebFLlBy9gjQVCWXt8KSmjvPbiY6q52Aaa8IUjA0YJAregvXxfopxO+/7BAoGARicvEtDp7WWnSc1OPoj6N14VIxgYcI7SyrzE0d/1x3ffKzB5e7qomNpxKzvqrVP8DzG7ydh8jaKPmv1MfF8tpYRy3AhmN3/GYwCnPqT75YYrhcrWcVdax5gmQVqHkFtIQkRSCIftzPLlpMGKha/YBV8c1fvC4LD0NPh/Ynv0gtECgYEAyOZg95/kte0jpgUEgwuMrzkhY/AaUJULFuR5MkyvReEbtSBQwV5tx60+T95PHNiFooWWVXiLMsAgyI2IbkxVR1Pzdri3gWK5CTfqb7kLuaj/B7SGvBa2Sxo478KS5K8tBBBWkITqo+wLC0mn3uZi1dyMWO1zopTA+KtEGF2dtGQ="; + + let enc_master_key = "4.dxbd5OMwi/Avy7DQxvLV+Z7kDJgHBtg/jAbgYNO7QU0Zii4rLFNco2lS5aS9z42LTZHc2p5HYwn2ZwkZNfHsQ6//d5q40MDgGYJMKBXOZP62ZHhct1XsvYBmtcUtIOm5j2HSjt2pjEuGAc1LbyGIWRJJQ3Lp1ULbL2m71I+P23GF36JyOM8SUWvpvxE/3+qqVhRFPG2VqMCYa2kLLxwVfUmpV+KKjX1TXsrq6pfJIwHNwHw4h7MSfD8xTy2bx4MiBt638Z9Vt1pGsSQkh9RgPvCbnhuCpZQloUgJ8ByLVEcrlKx3yaaxiQXvte+ZhuOI7rGdjmoVoOzisooje4JgYw==".parse().unwrap(); + let enc_user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); + let dec = + auth_request_decrypt_master_key(private_key.to_owned(), enc_master_key, enc_user_key) + .unwrap(); + + assert_eq!( + dec.to_vec().as_ref(), + vec![ + 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, + ] + ); + } + #[tokio::test] async fn test_device_login() { let kdf = Kdf::PBKDF2 { @@ -181,7 +234,9 @@ mod tests { private_key: private_key.to_owned(), method: InitUserCryptoMethod::AuthRequest { request_private_key: auth_req.private_key, - protected_user_key: approved_req, + method: AuthRequestMethod::UserKey { + protected_user_key: approved_req, + }, }, }) .await diff --git a/crates/bitwarden/src/auth/client_auth.rs b/crates/bitwarden/src/auth/client_auth.rs index aaa387741..0ae2f6f33 100644 --- a/crates/bitwarden/src/auth/client_auth.rs +++ b/crates/bitwarden/src/auth/client_auth.rs @@ -1,6 +1,8 @@ #[cfg(feature = "internal")] use bitwarden_crypto::{AsymmetricEncString, DeviceKey, TrustDeviceResponse}; +#[cfg(feature = "mobile")] +use crate::auth::login::NewAuthRequestResponse; #[cfg(feature = "secrets")] use crate::auth::login::{login_access_token, AccessTokenLoginRequest, AccessTokenLoginResponse}; use crate::{auth::renew::renew_token, error::Result, Client}; @@ -116,6 +118,25 @@ impl<'a> ClientAuth<'a> { } } +#[cfg(feature = "mobile")] +impl<'a> ClientAuth<'a> { + pub async fn login_device( + &mut self, + email: String, + device_identifier: String, + ) -> Result { + use crate::auth::login::send_new_auth_request; + + send_new_auth_request(self.client, email, device_identifier).await + } + + pub async fn login_device_complete(&mut self, auth_req: NewAuthRequestResponse) -> Result<()> { + use crate::auth::login::complete_auth_request; + + complete_auth_request(self.client, auth_req).await + } +} + #[cfg(feature = "internal")] fn trust_device(client: &Client) -> Result { let enc = client.get_encryption_settings()?; diff --git a/crates/bitwarden/src/auth/login/auth_request.rs b/crates/bitwarden/src/auth/login/auth_request.rs new file mode 100644 index 000000000..30db06124 --- /dev/null +++ b/crates/bitwarden/src/auth/login/auth_request.rs @@ -0,0 +1,131 @@ +use std::num::NonZeroU32; + +use bitwarden_api_api::{ + apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, + models::{AuthRequestCreateRequestModel, AuthRequestType}, +}; +use bitwarden_crypto::Kdf; +use uuid::Uuid; + +use crate::{ + auth::{ + api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, + auth_request::new_auth_request, + }, + client::{LoginMethod, UserLoginMethod}, + error::Result, + mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + Client, +}; + +pub struct NewAuthRequestResponse { + pub fingerprint: String, + email: String, + device_identifier: String, + auth_request_id: Uuid, + access_code: String, + private_key: String, +} + +pub(crate) async fn send_new_auth_request( + client: &mut Client, + email: String, + device_identifier: String, +) -> Result { + let config = client.get_api_configurations().await; + + let auth = new_auth_request(&email)?; + + let req = AuthRequestCreateRequestModel { + email: email.clone(), + public_key: auth.public_key, + device_identifier: device_identifier.clone(), + access_code: auth.access_code.clone(), + r#type: AuthRequestType::Variant0, // AuthenticateAndUnlock + }; + + let res = auth_requests_post(&config.api, Some(req)).await?; + + Ok(NewAuthRequestResponse { + fingerprint: auth.fingerprint, + email, + device_identifier, + auth_request_id: res.id.unwrap(), + access_code: auth.access_code, + private_key: auth.private_key, + }) +} + +pub(crate) async fn complete_auth_request( + client: &mut Client, + auth_req: NewAuthRequestResponse, +) -> Result<()> { + let config = client.get_api_configurations().await; + + let res = auth_requests_id_response_get( + &config.api, + auth_req.auth_request_id, + Some(&auth_req.access_code), + ) + .await?; + + let approved = res.request_approved.unwrap_or(false); + + if !approved { + return Err("Auth request was not approved".into()); + } + + let response = AuthRequestTokenRequest::new( + &auth_req.email, + &auth_req.auth_request_id, + &auth_req.access_code, + config.device_type, + &auth_req.device_identifier, + ) + .send(config) + .await?; + + if let IdentityTokenResponse::Authenticated(r) = response { + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + + client.set_tokens( + r.access_token.clone(), + r.refresh_token.clone(), + r.expires_in, + ); + client.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "web".to_owned(), + email: auth_req.email.to_owned(), + kdf: kdf.clone(), + })); + + let method = match res.master_password_hash { + Some(_) => AuthRequestMethod::MasterKey { + protected_master_key: res.key.unwrap().parse().unwrap(), + auth_request_key: r.key.unwrap().parse().unwrap(), + }, + None => AuthRequestMethod::UserKey { + protected_user_key: res.key.unwrap().parse().unwrap(), + }, + }; + + client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + kdf_params: kdf, + email: auth_req.email, + private_key: r.private_key.unwrap(), + method: InitUserCryptoMethod::AuthRequest { + request_private_key: auth_req.private_key, + method, + }, + }) + .await?; + + Ok(()) + } else { + Err("Failed to authenticate".into()) + } +} diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index afd7873a4..a36d27ae9 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -29,6 +29,13 @@ pub(crate) use api_key::login_api_key; #[cfg(feature = "internal")] pub use api_key::{ApiKeyLoginRequest, ApiKeyLoginResponse}; +#[cfg(feature = "mobile")] +mod auth_request; +#[cfg(feature = "mobile")] +pub use auth_request::NewAuthRequestResponse; +#[cfg(feature = "mobile")] +pub(crate) use auth_request::{complete_auth_request, send_new_auth_request}; + #[cfg(feature = "secrets")] mod access_token; #[cfg(feature = "secrets")] diff --git a/crates/bitwarden/src/auth/login/password.rs b/crates/bitwarden/src/auth/login/password.rs index f873ace97..4c978e559 100644 --- a/crates/bitwarden/src/auth/login/password.rs +++ b/crates/bitwarden/src/auth/login/password.rs @@ -63,12 +63,20 @@ pub(crate) async fn login_password( async fn request_identity_tokens( client: &mut Client, input: &PasswordLoginRequest, - password_hash: &String, + password_hash: &str, ) -> Result { + use crate::client::client_settings::DeviceType; + let config = client.get_api_configurations().await; - PasswordTokenRequest::new(&input.email, password_hash, &input.two_factor) - .send(config) - .await + PasswordTokenRequest::new( + &input.email, + password_hash, + DeviceType::ChromeBrowser, + "b86dd6ab-4265-4ddf-a7f1-eb28d5677f33", + &input.two_factor, + ) + .send(config) + .await } #[cfg(feature = "internal")] diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index 23b64eaf9..d77847574 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -14,10 +14,10 @@ use bitwarden_crypto::{HashPurpose, MasterKey}; pub use register::{RegisterKeyResponse, RegisterRequest}; #[cfg(feature = "internal")] mod auth_request; -#[cfg(feature = "mobile")] -pub(crate) use auth_request::auth_request_decrypt_user_key; #[cfg(feature = "internal")] pub use auth_request::AuthRequestResponse; +#[cfg(feature = "mobile")] +pub(crate) use auth_request::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; #[cfg(feature = "internal")] use crate::{client::Kdf, error::Result}; diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 3ccb7f9ca..b8632e21e 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -6,7 +6,7 @@ use bitwarden_crypto::SymmetricCryptoKey; #[cfg(feature = "internal")] use bitwarden_crypto::{AsymmetricEncString, EncString}; use chrono::Utc; -use reqwest::header::{self}; +use reqwest::header::{self, HeaderValue}; use uuid::Uuid; use super::AccessToken; @@ -29,6 +29,9 @@ use crate::{ pub(crate) struct ApiConfigurations { pub identity: bitwarden_api_identity::apis::configuration::Configuration, pub api: bitwarden_api_api::apis::configuration::Configuration, + /// Reqwest client useable for external integrations like email forwarders, HIBP. + #[allow(unused)] + pub external_client: reqwest::Client, pub device_type: DeviceType, } @@ -86,17 +89,28 @@ impl Client { pub fn new(settings_input: Option) -> Self { let settings = settings_input.unwrap_or_default(); - let headers = header::HeaderMap::new(); + fn new_client_builder() -> reqwest::ClientBuilder { + #[allow(unused_mut)] + let mut client_builder = reqwest::Client::builder(); - #[allow(unused_mut)] - let mut client_builder = reqwest::Client::builder().default_headers(headers); + #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))] + { + client_builder = + client_builder.use_preconfigured_tls(rustls_platform_verifier::tls_config()); + } - #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))] - { - client_builder = - client_builder.use_preconfigured_tls(rustls_platform_verifier::tls_config()); + client_builder } + let external_client = new_client_builder().build().unwrap(); + + let mut headers = header::HeaderMap::new(); + headers.append( + "Device-Type", + HeaderValue::from_str(&(settings.device_type as u8).to_string()).unwrap(), + ); + let client_builder = new_client_builder().default_headers(headers); + let client = client_builder.build().unwrap(); let identity = bitwarden_api_identity::apis::configuration::Configuration { @@ -127,6 +141,7 @@ impl Client { __api_configurations: ApiConfigurations { identity, api, + external_client, device_type: settings.device_type, }, encryption_settings: None, @@ -142,7 +157,7 @@ impl Client { #[cfg(feature = "mobile")] pub(crate) fn get_http_client(&self) -> &reqwest::Client { - &self.__api_configurations.api.client + &self.__api_configurations.external_client } #[cfg(feature = "secrets")] diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index 9b431b4b4..37bb3b905 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -54,14 +54,31 @@ pub enum InitUserCryptoMethod { AuthRequest { /// Private Key generated by the `crate::auth::new_auth_request`. request_private_key: String, + + method: AuthRequestMethod, + }, +} + +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Enum))] +pub enum AuthRequestMethod { + UserKey { /// User Key protected by the private key provided in `AuthRequestResponse`. protected_user_key: AsymmetricEncString, }, + MasterKey { + /// Master Key protected by the private key provided in `AuthRequestResponse`. + protected_master_key: AsymmetricEncString, + /// User Key protected by the MasterKey, provided by the auth response. + auth_request_key: EncString, + }, } #[cfg(feature = "internal")] pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequest) -> Result<()> { - use crate::auth::auth_request_decrypt_user_key; + use crate::auth::{auth_request_decrypt_master_key, auth_request_decrypt_user_key}; let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username { client_id: "".to_string(), @@ -89,9 +106,21 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ } InitUserCryptoMethod::AuthRequest { request_private_key, - protected_user_key, + method, } => { - let user_key = auth_request_decrypt_user_key(request_private_key, protected_user_key)?; + let user_key = match method { + AuthRequestMethod::UserKey { protected_user_key } => { + auth_request_decrypt_user_key(request_private_key, protected_user_key)? + } + AuthRequestMethod::MasterKey { + protected_master_key, + auth_request_key, + } => auth_request_decrypt_master_key( + request_private_key, + protected_master_key, + auth_request_key, + )?, + }; client.initialize_user_crypto_decrypted_key(user_key, private_key)?; } } diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index 53b9b609e..e0195f5aa 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -114,3 +114,26 @@ pub(crate) async fn login_api_key( Ok(()) } + +pub(crate) async fn login_device( + mut client: Client, + email: Option, + device_identifier: Option, +) -> Result<()> { + let email = text_prompt_when_none("Email", email)?; + let device_identifier = text_prompt_when_none("Device Identifier", device_identifier)?; + + let auth = client + .auth() + .login_device(email, device_identifier) + .await + .unwrap(); + + println!("Fingerprint: {}", auth.fingerprint); + + Text::new("Press enter once approved").prompt()?; + + client.auth().login_device_complete(auth).await.unwrap(); + + Ok(()) +} diff --git a/crates/bw/src/auth/mod.rs b/crates/bw/src/auth/mod.rs index a4c7e2ed5..1f165f5f3 100644 --- a/crates/bw/src/auth/mod.rs +++ b/crates/bw/src/auth/mod.rs @@ -1,2 +1,2 @@ mod login; -pub(crate) use login::{login_api_key, login_password}; +pub(crate) use login::{login_api_key, login_device, login_password}; diff --git a/crates/bw/src/main.rs b/crates/bw/src/main.rs index 0e7cd975e..6674bda1e 100644 --- a/crates/bw/src/main.rs +++ b/crates/bw/src/main.rs @@ -78,6 +78,11 @@ enum LoginCommands { client_id: Option, client_secret: Option, }, + Device { + #[arg(short = 'e', long, help = "Email address")] + email: Option, + device_identifier: Option, + }, } #[derive(Subcommand, Clone)] @@ -163,6 +168,12 @@ async fn process_commands() -> Result<()> { client_id, client_secret, } => auth::login_api_key(client, client_id, client_secret).await?, + LoginCommands::Device { + email, + device_identifier, + } => { + auth::login_device(client, email, device_identifier).await?; + } } return Ok(()); } diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index f85222738..d69e134c4 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -1280,9 +1280,9 @@ implementations. Private Key generated by the `crate::auth::new_auth_request`. - protected_user_key + method + - User Key protected by the private key provided in `AuthRequestResponse`.