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()?; let servers = vapore::selection::bootstrap_find_servers().await?; log::debug!("Found servers: {:?}", servers); let (session, context) = vapore::connection::CMSession::connect(&servers[0]).await?; tokio::spawn(context); 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(()) }