diff --git a/lib/examples/login_steamguard_new.rs b/lib/examples/login_steamguard_new.rs new file mode 100644 index 0000000..fd5fb2e --- /dev/null +++ b/lib/examples/login_steamguard_new.rs @@ -0,0 +1,29 @@ +use color_eyre::eyre::{self}; +use vapore::client::SteamClient; + +#[tokio::main] +pub async fn main() -> eyre::Result<()> { + env_logger::init(); + color_eyre::install()?; + + let servers = vapore::selection::bootstrap_find_servers().await?; + log::debug!("Found servers: {:?}", servers); + + let client = SteamClient::connect(&servers).await?; + + let username = dialoguer::Input::::new() + .with_prompt("Username") + .interact_text()?; + let password = dialoguer::Password::new() + .with_prompt("Password") + .interact()?; + let guard_code = dialoguer::Input::::new() + .with_prompt("Steam Guard Code") + .interact_text()?; + + client + .auth_password(username, password, Some(guard_code)) + .await?; + + Ok(()) +} diff --git a/lib/src/client.rs b/lib/src/client.rs index 6a541d0..5b28d0f 100644 --- a/lib/src/client.rs +++ b/lib/src/client.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use base64::Engine as _; +use base64::{prelude::BASE64_STANDARD, Engine as _}; use snafu::prelude::*; use vapore_proto::{ enums::ESessionPersistence, @@ -9,14 +9,22 @@ use vapore_proto::{ CAuthentication_BeginAuthSessionViaCredentials_Request, CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails, CAuthentication_GetPasswordRSAPublicKey_Request, - CAuthentication_GetPasswordRSAPublicKey_Response, EAuthTokenPlatformType, + CAuthentication_GetPasswordRSAPublicKey_Response, + CAuthentication_PollAuthSessionStatus_Request, + CAuthentication_PollAuthSessionStatus_Response, + CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, + CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthSessionGuardType, + EAuthTokenPlatformType, }, - steammessages_clientserver_login::CMsgClientHello, + steammessages_base::{cmsg_ipaddress, CMsgIPAddress}, + steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse}, }; use crate::{ connection::CMSession, - error::{RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu}, + error::{ + ListenRecvSnafu, MissingFieldSnafu, RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu, + }, message::CMProtoBufMessage, platform::generate_machine_id, state::apps::AppsHandler, @@ -41,8 +49,8 @@ pub struct SteamClient { } impl SteamClient { - pub async fn connect(servers: &[&str]) -> Result { - let (session, context) = CMSession::connect(servers[0]).await?; + pub async fn connect(servers: &[String]) -> Result { + let (session, context) = CMSession::connect(&servers[0]).await?; let inner = Arc::new(SteamClientInner { apps: AppsHandler::listen(&session), @@ -84,6 +92,8 @@ impl SteamClient { ) .await?; + password_key_response.ok()?; + let password_key = rsa::RsaPublicKey::new( rsa::BigUint::parse_bytes(password_key_response.body.publickey_mod().as_bytes(), 16) .context(RSAParameterParseSnafu {})?, @@ -107,13 +117,9 @@ impl SteamClient { "Authentication.BeginAuthSessionViaCredentials#1".to_string(), CAuthentication_BeginAuthSessionViaCredentials_Request { account_name: Some(username.clone()), - encrypted_password: Some( - base64::engine::general_purpose::STANDARD.encode(&encrypted_password), - ), + encrypted_password: Some(BASE64_STANDARD.encode(&encrypted_password)), encryption_timestamp: password_key_response.body.timestamp, - persistence: Some(protobuf::EnumOrUnknown::new( - ESessionPersistence::k_ESessionPersistence_Ephemeral, - )), + persistence: Some(ESessionPersistence::k_ESessionPersistence_Ephemeral.into()), device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails { device_friendly_name: Some("vapore".to_string()), platform_type: Some(protobuf::EnumOrUnknown::new( @@ -128,6 +134,122 @@ impl SteamClient { }, ) .await?; + log::debug!("Got auth response {:#x?}", auth_session_response); + auth_session_response.ok()?; + + let confirmation_type = auth_session_response + .body + .allowed_confirmations + .first() + .unwrap_or_default() + .confirmation_type(); + + if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_Unknown { + return Err(ClientError::NoAllowedConfirmations); + } else if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_None { + // No required confirmation, we can go directly to login + } else if confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode + || confirmation_type == EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode + { + let update_response: CMProtoBufMessage< + CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, + > = self + .inner + .session + .call_service_method( + "Authentication.UpdateAuthSessionWithSteamGuardCode#1".to_string(), + CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request { + client_id: auth_session_response.body.client_id, + steamid: auth_session_response.body.steamid, + code, + code_type: Some(confirmation_type.into()), + ..Default::default() + }, + ) + .await?; + + update_response.ok()?; + } else { + return Err(ClientError::UnsupportedConfirmation { + typ: confirmation_type, + }); + } + + let poll_response: CMProtoBufMessage = self + .inner + .session + .call_service_method( + "Authentication.PollAuthSessionStatus#1".to_string(), + CAuthentication_PollAuthSessionStatus_Request { + client_id: auth_session_response.body.client_id, + request_id: auth_session_response.body.request_id.clone(), + ..Default::default() + }, + ) + .await?; + poll_response.ok()?; + log::debug!("Got poll response: {:#?}", poll_response); + + let mut logon_receiver = self + .inner + .session + .subscribe_message_type(EMsg::k_EMsgClientLogOnResponse); + + let account_name = poll_response.body.account_name.context(MissingFieldSnafu { + field: "account_name", + })?; + let refresh_token = poll_response + .body + .refresh_token + .context(MissingFieldSnafu { + field: "refresh_token", + })?; + + log::debug!( + "Got account name {}, access token {}", + account_name, + refresh_token + ); + + // normal user, desktop instance, public universe + self.inner.session.set_steam_id(0x0110_0001_0000_0000); + + self.inner.session.send_notification( + EMsg::k_EMsgClientLogon, + CMsgClientLogon { + account_name: Some(account_name), + access_token: Some(refresh_token), + machine_name: Some("vapore".to_string()), + machine_id: Some(machine_id), + client_language: Some("english".to_string()), + protocol_version: Some(0x1002c), + client_os_type: Some(20), + client_package_version: Some(1771), + supports_rate_limit_response: Some(true), + should_remember_password: Some(true), + obfuscated_private_ip: protobuf::MessageField::some(CMsgIPAddress { + ip: Some(cmsg_ipaddress::Ip::V4(0xc0a8_0102 ^ 0xbaad_f00d)), + ..Default::default() + }), + deprecated_obfustucated_private_ip: Some(0xc0a8_0102 ^ 0xbaad_f00d), + ..Default::default() + }, + )?; + + let raw_logon = logon_receiver.recv().await.context(ListenRecvSnafu {})?; + let logon: CMProtoBufMessage = + CMProtoBufMessage::deserialize(raw_logon)?; + + self.inner.session.set_steam_id(logon.header.steamid()); + self.inner + .session + .set_client_session_id(logon.header.client_sessionid()); + + if let Some(heartbeat_seconds) = logon.body.heartbeat_seconds { + if heartbeat_seconds >= 5 { + self.inner.session.begin_heartbeat(heartbeat_seconds as u32); + } + } Ok(()) } diff --git a/lib/src/error.rs b/lib/src/error.rs index 9a0a310..ea4728f 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,4 +1,5 @@ use snafu::prelude::*; +use vapore_proto::steammessages_auth_steamclient::EAuthSessionGuardType; use vapore_struct::eresult::EResult; #[derive(Debug, Snafu)] @@ -38,6 +39,9 @@ pub enum ClientError { #[snafu(display("Protobuf Serialization error"))] ProtobufSer { source: protobuf::Error }, + #[snafu(display("Struct Missing Field `{field}`"))] + MissingField { field: &'static str }, + #[snafu(display("Invalid WebSocket message type from server"))] BadWSMessageType, @@ -77,6 +81,17 @@ pub enum ClientError { #[snafu(display("Unable to encrupt with RSA"))] RSAEncrypt { source: rsa::Error }, + + #[snafu(display("Unsupported Confirmation type {typ:?}"))] + UnsupportedConfirmation { typ: EAuthSessionGuardType }, + + #[snafu(display("No allowed confirmations"))] + NoAllowedConfirmations, + + #[snafu(display("Unable to fetch messages from listen thread"))] + ListenRecv { + source: tokio::sync::broadcast::error::RecvError, + }, } impl From> for ClientError {