From c3b17bafdfb3940bd0ee81e16624ddaf999f10b3 Mon Sep 17 00:00:00 2001 From: Artemis Tosini Date: Fri, 6 Sep 2024 00:35:24 +0000 Subject: [PATCH] Start work on login --- lib/Cargo.toml | 4 +- lib/src/client.rs | 114 ++++++++++++++++++++++++++++++++++++++---- lib/src/error.rs | 9 ++++ lib/src/lib.rs | 1 + lib/src/platform.rs | 28 +++++++++++ lib/src/state/apps.rs | 58 ++++++++++++++++++++- 6 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 lib/src/platform.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 21beb7f..ab64acd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -5,10 +5,12 @@ version.workspace = true [dependencies] async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] } +base64 = "0.22.1" bitflags = "2.6.0" color-eyre.workspace = true flate2 = "1.0.33" futures = "0.3.30" +hex = "0.4.3" keyvalues-serde.workspace = true log.workspace = true num_enum = "0.7.3" @@ -22,8 +24,6 @@ tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros", "time vapore-proto.path = "../proto" [dev-dependencies] -base64 = "0.22.1" dialoguer = "0.11.0" env_logger.workspace = true -hex = "0.4.3" qrcode = "0.14.1" diff --git a/lib/src/client.rs b/lib/src/client.rs index 8e003a0..c3999d0 100644 --- a/lib/src/client.rs +++ b/lib/src/client.rs @@ -1,10 +1,25 @@ use std::sync::Arc; -use tokio::sync::watch; +use base64::Engine as _; +use snafu::prelude::*; +use vapore_proto::{ + enums::ESessionPersistence, + enums_clientserver::EMsg, + steammessages_auth_steamclient::{ + CAuthentication_BeginAuthSessionViaCredentials_Request, + CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails, + CAuthentication_GetPasswordRSAPublicKey_Request, + CAuthentication_GetPasswordRSAPublicKey_Response, EAuthTokenPlatformType, + }, + steammessages_clientserver_login::CMsgClientHello, +}; use crate::{ connection::{CMContext, CMSession}, - state::apps::License, + error::{RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu}, + message::CMProtoBufMessage, + platform::generate_machine_id, + state::apps::AppsHandler, ClientError, }; @@ -13,10 +28,7 @@ struct SteamClientInner { /// TODO: Support recreation when one fails pub session: CMSession, - /// Licenses owned by current user, as told periodically by Valve. - /// Use an Arc in the receiver so that the sender doesn't block if - /// someone keeps a long-lived reference to the licenses - pub licenses: watch::Receiver>>, + apps: AppsHandler, } /// Higher-level Steam client. Unlike [crate::connection::CMSession], @@ -29,19 +41,99 @@ pub struct SteamClient { } impl SteamClient { - pub async fn connect(servers: &[&str]) -> Result<(Self, CMContext), ClientError> { + pub async fn connect(servers: &[&str]) -> Result { let (session, context) = CMSession::connect(servers[0]).await?; let inner = Arc::new(SteamClientInner { + apps: AppsHandler::listen(&session), session, - licenses: todo!(), }); - Ok((Self { inner }, context)) + tokio::spawn(context); + + inner.session.send_notification( + EMsg::k_EMsgClientHello, + CMsgClientHello { + protocol_version: Some(0x1002c), + ..Default::default() + }, + )?; + + Ok(Self { inner }) } - pub fn licenses(&self) -> Arc> { - self.inner.licenses.borrow().clone() + pub async fn auth_password( + &self, + username: String, + password: String, + code: Option, + ) -> Result<(), ClientError> { + let machine_id = generate_machine_id(); + + let password_key_response: CMProtoBufMessage< + CAuthentication_GetPasswordRSAPublicKey_Response, + > = self + .inner + .session + .call_service_method( + "Authentication.GetPasswordRSAPublicKey#1".to_string(), + CAuthentication_GetPasswordRSAPublicKey_Request { + account_name: Some(username.clone()), + ..Default::default() + }, + ) + .await?; + + let password_key = rsa::RsaPublicKey::new( + rsa::BigUint::parse_bytes(password_key_response.body.publickey_mod().as_bytes(), 16) + .context(RSAParameterParseSnafu {})?, + rsa::BigUint::parse_bytes(password_key_response.body.publickey_exp().as_bytes(), 16) + .context(RSAParameterParseSnafu {})?, + ) + .context(RSAParseSnafu {})?; + + let encrypted_password = password_key + .encrypt( + &mut rand::thread_rng(), + rsa::Pkcs1v15Encrypt, + password.as_bytes(), + ) + .context(RSAEncryptSnafu {})?; + + let auth_session_response = self + .inner + .session + .call_service_method::<_, CAuthentication_BeginAuthSessionViaCredentials_Response>( + "Authentication.BeginAuthSessionViaCredentials#1".to_string(), + CAuthentication_BeginAuthSessionViaCredentials_Request { + account_name: Some(username.clone()), + encrypted_password: Some( + base64::engine::general_purpose::STANDARD.encode(&encrypted_password), + ), + encryption_timestamp: password_key_response.body.timestamp, + persistence: Some(protobuf::EnumOrUnknown::new( + ESessionPersistence::k_ESessionPersistence_Ephemeral, + )), + device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails { + device_friendly_name: Some("vapore".to_string()), + platform_type: Some(protobuf::EnumOrUnknown::new( + EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient, + )), + // Windows 11 + os_type: Some(20), + machine_id: Some(machine_id.clone()), + ..Default::default() + }), + ..Default::default() + }, + ) + .await?; + + Ok(()) + } + + pub fn apps(&self) -> &AppsHandler { + &self.inner.apps } pub fn session(&self) -> CMSession { diff --git a/lib/src/error.rs b/lib/src/error.rs index e59bd4f..45ee98d 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -75,6 +75,15 @@ pub enum ClientError { TryFromELicenseType { source: TryFromPrimitiveError, }, + + #[snafu(display("Unable to parse RSA key parameters"))] + RSAParameterParse, + + #[snafu(display("Unable to parse RSA key"))] + RSAParse { source: rsa::Error }, + + #[snafu(display("Unable to encrupt with RSA"))] + RSAEncrypt { source: rsa::Error }, } impl From> for ClientError { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 2895746..b0e9df7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,6 +2,7 @@ pub mod client; pub mod connection; pub mod error; pub mod message; +pub mod platform; pub mod selection; pub mod state; diff --git a/lib/src/platform.rs b/lib/src/platform.rs new file mode 100644 index 0000000..61d49c5 --- /dev/null +++ b/lib/src/platform.rs @@ -0,0 +1,28 @@ +use rand::RngCore; + +pub fn generate_machine_id() -> Vec { + // machine_id is supposed to be a binary key/value with the SHA1 of the machine's + // BB3: Machine GUID + // FF2: MAC address + // 3B3: Disk ID + // We should probably make these consistent so Valve doesn't get suspicious, + // but for now let's make them random + // TODO: Find a more generic way to make this + let mut machine_id = Vec::with_capacity(155); + machine_id.extend_from_slice(b"\x00MessageObject\x00"); + for key in [b"BB3", b"FF2", b"3B3"] { + let mut data = [0u8; 20]; + rand::thread_rng().fill_bytes(&mut data); + let hex_bytes = hex::encode(data).into_bytes(); + + // Type is string + machine_id.push(b'\x01'); + machine_id.extend_from_slice(key); + machine_id.push(b'\x00'); + machine_id.extend_from_slice(&hex_bytes); + machine_id.push(b'\x00'); + } + // suitable end bytes + machine_id.extend_from_slice(b"\x08\x08"); + machine_id +} diff --git a/lib/src/state/apps.rs b/lib/src/state/apps.rs index 73234cd..7d419ce 100644 --- a/lib/src/state/apps.rs +++ b/lib/src/state/apps.rs @@ -1,11 +1,20 @@ -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{ + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; use num_enum::TryFromPrimitive; use snafu::prelude::*; -use vapore_proto::steammessages_clientserver::cmsg_client_license_list; +use tokio::sync::watch; +use vapore_proto::{ + enums_clientserver::EMsg, + steammessages_clientserver::{cmsg_client_license_list, CMsgClientLicenseList}, +}; use crate::{ + connection::CMSession, error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu}, + message::{CMProtoBufMessage, CMRawProtoBufMessage}, ClientError, }; @@ -271,3 +280,48 @@ impl TryFrom for License { }) } } + +pub struct AppsHandler { + /// Licenses owned by current user, as told periodically by Valve. + /// Use an Arc in the receiver so that the sender doesn't block if + /// someone keeps a long-lived reference to the licenses + pub licenses: watch::Receiver>>, +} + +impl AppsHandler { + pub(crate) fn listen(session: &CMSession) -> Self { + let (licenses_sender, licenses) = watch::channel(Arc::new(Vec::new())); + + let session_cloned = session.clone(); + tokio::spawn(async move { + let mut listener = session_cloned.subscribe_message_type(EMsg::k_EMsgClientLicenseList); + while let Ok(raw_message) = listener.recv().await { + match CMProtoBufMessage::::deserialize(raw_message) { + Ok(message) => { + let licenses = message + .body + .licenses + .into_iter() + .map(License::try_from) + .filter_map(|item| match item { + Ok(license) => Some(license), + Err(err) => { + log::debug!("Invalid License object received: {}", err); + None + } + }) + .collect(); + + if licenses_sender.send(Arc::new(licenses)).is_err() { + // If there's no receiver then there's no more SteamClient, just die + return; + } + } + Err(err) => log::debug!("Invalid CMsgClientList: {}", err), + } + } + }); + + Self { licenses } + } +}