diff --git a/Cargo.lock b/Cargo.lock index f469af1..7949ff0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1854,24 +1854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "vapore-client" -version = "0.0.0" -dependencies = [ - "ctor", - "env_logger", - "log", -] - -[[package]] -name = "vapore-proto" -version = "0.1.0" -dependencies = [ - "protobuf", - "protobuf-codegen", -] - -[[package]] -name = "vapored" +name = "vapore" version = "0.1.0" dependencies = [ "async-tungstenite", @@ -1894,6 +1877,23 @@ dependencies = [ "vapore-proto", ] +[[package]] +name = "vapore-client" +version = "0.0.0" +dependencies = [ + "ctor", + "env_logger", + "log", +] + +[[package]] +name = "vapore-proto" +version = "0.1.0" +dependencies = [ + "protobuf", + "protobuf-codegen", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index dd57af9..4670ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "client", - "daemon", + "lib", "proto", ] diff --git a/flake.nix b/flake.nix index acd54bf..60cfff6 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,7 @@ protobuf ]; RUST_SRC_PATH = "${rustPackages.rustPlatform.rustLibSrc}"; - RUST_LOG = "debug,vapored=trace,vapore-client=trace"; + RUST_LOG = "debug,vapore=trace,vapore-client=trace"; RUST_BACKTRACE = "1"; }; diff --git a/daemon/Cargo.toml b/lib/Cargo.toml similarity index 95% rename from daemon/Cargo.toml rename to lib/Cargo.toml index 8277e4c..787e6cc 100644 --- a/daemon/Cargo.toml +++ b/lib/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "vapored" +name = "vapore" edition = "2021" version.workspace = true @@ -8,7 +8,6 @@ async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs base64 = "0.22.1" color-eyre.workspace = true dialoguer = "0.11.0" -env_logger.workspace = true flate2 = "1.0.33" futures = "0.3.30" hex = "0.4.3" @@ -22,3 +21,6 @@ rsa = "0.9.6" serde = { version = "1.0.209", features = ["derive"] } tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros", "time"]} vapore-proto.path = "../proto" + +[dev-dependencies] +env_logger.workspace = true diff --git a/lib/examples/login_qr.rs b/lib/examples/login_qr.rs new file mode 100644 index 0000000..4684ba0 --- /dev/null +++ b/lib/examples/login_qr.rs @@ -0,0 +1,191 @@ +use color_eyre::eyre; +use rand::RngCore; +use vapore::message::CMProtoBufMessage; +use vapore_proto::{ + enums_clientserver::EMsg, + steammessages_auth_steamclient::{ + CAuthentication_BeginAuthSessionViaQR_Request, + CAuthentication_BeginAuthSessionViaQR_Response, CAuthentication_DeviceDetails, + CAuthentication_PollAuthSessionStatus_Request, + CAuthentication_PollAuthSessionStatus_Response, EAuthTokenPlatformType, + }, + steammessages_base::{cmsg_ipaddress, CMsgIPAddress}, + steammessages_clientserver::CMsgClientLicenseList, + steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse}, +}; + +#[tokio::main] +pub async fn main() -> eyre::Result<()> { + env_logger::init(); + color_eyre::install()?; + + log::info!("Starting vapored"); + + let servers = vapore::connection::bootstrap_find_servers().await?; + log::debug!("Found servers: {:?}", servers); + + let session = vapore::connection::CMSession::connect(&servers[0]).await?; + + session.send_notification( + EMsg::k_EMsgClientHello, + CMsgClientHello { + protocol_version: Some(0x1002c), + ..Default::default() + }, + )?; + + log::debug!("Sent hello"); + + // 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"); + + let response = session + .call_service_method::<_, CAuthentication_BeginAuthSessionViaQR_Response>( + "Authentication.BeginAuthSessionViaQR#1".to_string(), + CAuthentication_BeginAuthSessionViaQR_Request { + 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?; + + log::debug!("Got response {:#x?}", response); + + let code = qrcode::QrCode::new(response.body.challenge_url()).unwrap(); + log::info!( + "Got new QR code:\n{}", + code.render::().build() + ); + + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs_f32( + response.body.interval(), + )); + + let (refresh_token, account_name) = loop { + interval.tick().await; + let request = CAuthentication_PollAuthSessionStatus_Request { + client_id: response.body.client_id, + request_id: response.body.request_id.clone(), + ..Default::default() + }; + let response: CMProtoBufMessage = session + .call_service_method( + "Authentication.PollAuthSessionStatus#1".to_string(), + request, + ) + .await?; + + log::debug!("Got auth poll status {:#?}", response.body); + + if let Some(access_token) = response.body.refresh_token { + let account_name = response.body.account_name.unwrap_or_default(); + break (access_token, account_name); + } + + if let Some(new_url) = response.body.new_challenge_url { + let code = qrcode::QrCode::new(new_url)?; + log::info!( + "Got new QR code:\n{}", + code.render::().build() + ) + }; + }; + + log::debug!( + "Got account name {}, access token {}", + account_name, + refresh_token + ); + + // normal user, desktop instance, public universe + session.set_steam_id(0x0110_0001_0000_0000); + + 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 session_cloned = session.clone(); + tokio::spawn(async move { + let license_list_raw = session_cloned + .subscribe_message_type(EMsg::k_EMsgClientLicenseList) + .recv() + .await + .unwrap(); + let license_list: CMProtoBufMessage = + CMProtoBufMessage::deserialize(license_list_raw).unwrap(); + for license in &license_list.body.licenses { + log::info!("Own package ID: {}", license.package_id()); + } + }); + + let mut finish_receiver = session.subscribe_message_type(EMsg::k_EMsgClientLogOnResponse); + + let raw_response = finish_receiver.recv().await?; + let response: CMProtoBufMessage = + CMProtoBufMessage::deserialize(raw_response)?; + + log::debug!("Got logon response: {:#x?}", response); + + if response.body.eresult != Some(0x01) { + eyre::bail!("Login failed"); + }; + + session.set_steam_id(response.header.steamid()); + session.set_client_session_id(response.header.client_sessionid()); + + if let Some(heartbeat_seconds) = response.body.heartbeat_seconds { + if heartbeat_seconds >= 5 { + session.begin_heartbeat(heartbeat_seconds as u32); + } + } + + Ok(()) +} diff --git a/daemon/src/main.rs b/lib/examples/login_steamguard.rs similarity index 97% rename from daemon/src/main.rs rename to lib/examples/login_steamguard.rs index ab36ca9..07e5f63 100644 --- a/daemon/src/main.rs +++ b/lib/examples/login_steamguard.rs @@ -1,8 +1,8 @@ use base64::Engine; use color_eyre::eyre::{self, Context, OptionExt}; -use connection::CMSession; -use message::CMProtoBufMessage; use rand::RngCore; +use vapore::connection::CMSession; +use vapore::message::CMProtoBufMessage; use vapore_proto::{ enums::ESessionPersistence, enums_clientserver::EMsg, @@ -22,9 +22,6 @@ use vapore_proto::{ steammessages_clientserver_login::{CMsgClientHello, CMsgClientLogon, CMsgClientLogonResponse}, }; -mod connection; -mod message; - #[tokio::main] pub async fn main() -> eyre::Result<()> { env_logger::init(); @@ -32,10 +29,10 @@ pub async fn main() -> eyre::Result<()> { log::info!("Starting vapored"); - let servers = connection::bootstrap_find_servers().await?; + let servers = vapore::connection::bootstrap_find_servers().await?; log::debug!("Found servers: {:?}", servers); - let session = connection::CMSession::connect(&servers[0]).await?; + let session = CMSession::connect(&servers[0]).await?; session.send_notification( EMsg::k_EMsgClientHello, diff --git a/daemon/src/connection.rs b/lib/src/connection.rs similarity index 100% rename from daemon/src/connection.rs rename to lib/src/connection.rs diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..ea305ac --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,2 @@ +pub mod connection; +pub mod message; diff --git a/daemon/src/message.rs b/lib/src/message.rs similarity index 100% rename from daemon/src/message.rs rename to lib/src/message.rs