1use std::collections::HashMap;
4
5use prost::Message;
6use steam_protos::{
7 CAuthenticationAccessTokenGenerateForAppRequest, CAuthenticationAccessTokenGenerateForAppResponse, CAuthenticationBeginAuthSessionViaCredentialsRequest, CAuthenticationBeginAuthSessionViaCredentialsResponse, CAuthenticationBeginAuthSessionViaQRRequest, CAuthenticationBeginAuthSessionViaQRResponse, CAuthenticationDeviceDetails, CAuthenticationGetAuthSessionInfoRequest, CAuthenticationGetAuthSessionInfoResponse, CAuthenticationGetPasswordRSAPublicKeyRequest, CAuthenticationGetPasswordRSAPublicKeyResponse, CAuthenticationPollAuthSessionStatusRequest,
8 CAuthenticationPollAuthSessionStatusResponse, CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest, CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest, EAuthSessionGuardType, EAuthTokenPlatformType, ESessionPersistence, ETokenRenewalType,
9};
10
11use crate::{
12 crypto::rsa_encrypt_password,
13 error::SessionError,
14 helpers::{default_user_agent, get_spoofed_hostname},
15 transport::{ApiRequest, Transport},
16 types::{AllowedConfirmation, DeviceDetails, PlatformData, StartAuthSessionResponse},
17};
18
19#[derive(Debug, Clone)]
21pub struct RsaKeyResponse {
22 pub public_key_mod: String,
23 pub public_key_exp: String,
24 pub timestamp: u64,
25}
26
27#[derive(Debug, Clone)]
29pub struct EncryptedPassword {
30 pub encrypted_password: String,
31 pub key_timestamp: u64,
32}
33
34#[derive(Debug, Clone)]
36pub struct PollLoginStatusResponse {
37 pub new_client_id: Option<u64>,
38 pub new_challenge_url: Option<String>,
39 pub refresh_token: Option<String>,
40 pub access_token: Option<String>,
41 pub had_remote_interaction: bool,
42 pub account_name: Option<String>,
43 pub new_steam_guard_machine_auth: Option<String>,
44}
45
46pub struct AuthenticationClient {
48 transport: Transport,
49 platform_type: EAuthTokenPlatformType,
50 web_user_agent: String,
51 machine_id: Option<Vec<u8>>,
52 client_friendly_name: Option<String>,
53}
54
55impl AuthenticationClient {
56 pub fn new(transport: Transport, platform_type: EAuthTokenPlatformType, machine_id: Option<Vec<u8>>, client_friendly_name: Option<String>) -> Self {
58 Self { transport, platform_type, web_user_agent: default_user_agent(), machine_id, client_friendly_name }
59 }
60
61 pub async fn get_rsa_key(&self, account_name: &str) -> Result<RsaKeyResponse, SessionError> {
63 let request = CAuthenticationGetPasswordRSAPublicKeyRequest { account_name: Some(account_name.to_string()) };
64
65 let response: CAuthenticationGetPasswordRSAPublicKeyResponse = self.send_request("Authentication", "GetPasswordRSAPublicKey", 1, &request, None).await?;
66
67 Ok(RsaKeyResponse {
68 public_key_mod: response.publickey_mod.unwrap_or_default(),
69 public_key_exp: response.publickey_exp.unwrap_or_default(),
70 timestamp: response.timestamp.unwrap_or(0),
71 })
72 }
73
74 pub async fn encrypt_password(&self, account_name: &str, password: &str) -> Result<EncryptedPassword, SessionError> {
80 let rsa_info = self.get_rsa_key(account_name).await?;
81
82 let encrypted_password = rsa_encrypt_password(password, &rsa_info.public_key_mod, &rsa_info.public_key_exp)?;
84
85 Ok(EncryptedPassword { encrypted_password, key_timestamp: rsa_info.timestamp })
86 }
87
88 pub async fn start_session_with_credentials(&self, account_name: &str, encrypted_password: &str, key_timestamp: u64, persistence: ESessionPersistence, steam_guard_machine_token: Option<&str>) -> Result<StartAuthSessionResponse, SessionError> {
90 let platform_data = self.get_platform_data();
91
92 let device_details = CAuthenticationDeviceDetails {
93 device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
94 platform_type: Some(self.platform_type as i32),
95 os_type: platform_data.device_details.os_type,
96 gaming_device_type: platform_data.device_details.gaming_device_type,
97 client_count: None,
98 machine_id: platform_data.device_details.machine_id.clone(),
99 app_type: None,
100 };
101
102 let mut request = CAuthenticationBeginAuthSessionViaCredentialsRequest {
103 account_name: Some(account_name.to_string()),
104 encrypted_password: Some(encrypted_password.to_string()),
105 encryption_timestamp: Some(key_timestamp),
106 remember_login: Some(persistence == ESessionPersistence::KESessionPersistencePersistent),
107 persistence: Some(persistence as i32),
108 website_id: Some(platform_data.website_id.clone()),
109 device_details: Some(device_details),
110 device_friendly_name: None,
111 platform_type: Some(self.platform_type as i32),
112 guard_data: None,
113 language: None,
114 qos_level: Some(2),
115 };
116
117 if let Some(token) = steam_guard_machine_token {
119 request.guard_data = Some(token.to_string());
120 }
121
122 let response: CAuthenticationBeginAuthSessionViaCredentialsResponse = self.send_request("Authentication", "BeginAuthSessionViaCredentials", 1, &request, None).await?;
123
124 Ok(StartAuthSessionResponse {
125 client_id: response.client_id.unwrap_or(0),
126 request_id: response.request_id.unwrap_or_default(),
127 poll_interval: response.interval.unwrap_or(5.0),
128 allowed_confirmations: response
129 .allowed_confirmations
130 .into_iter()
131 .map(|c| AllowedConfirmation {
132 confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
133 message: c.associated_message,
134 })
135 .collect(),
136 steam_id: response.steamid,
137 weak_token: response.weak_token,
138 challenge_url: None,
139 version: None,
140 })
141 }
142
143 pub async fn start_session_with_qr(&self) -> Result<StartAuthSessionResponse, SessionError> {
145 let platform_data = self.get_platform_data();
146
147 let device_details = CAuthenticationDeviceDetails {
148 device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
149 platform_type: Some(self.platform_type as i32),
150 os_type: platform_data.device_details.os_type,
151 gaming_device_type: platform_data.device_details.gaming_device_type,
152 client_count: None,
153 machine_id: platform_data.device_details.machine_id.clone(),
154 app_type: None,
155 };
156
157 let request = CAuthenticationBeginAuthSessionViaQRRequest {
158 device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
159 platform_type: Some(self.platform_type as i32),
160 device_details: Some(device_details),
161 website_id: Some("Unknown".to_string()),
162 };
163
164 let response: CAuthenticationBeginAuthSessionViaQRResponse = self.send_request("Authentication", "BeginAuthSessionViaQR", 1, &request, None).await?;
165
166 Ok(StartAuthSessionResponse {
167 client_id: response.client_id.unwrap_or(0),
168 request_id: response.request_id.unwrap_or_default(),
169 poll_interval: response.interval.unwrap_or(5.0),
170 allowed_confirmations: response
171 .allowed_confirmations
172 .into_iter()
173 .map(|c| AllowedConfirmation {
174 confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
175 message: c.associated_message,
176 })
177 .collect(),
178 steam_id: None,
179 weak_token: None,
180 challenge_url: response.challenge_url,
181 version: response.version,
182 })
183 }
184
185 pub async fn submit_steam_guard_code(&self, client_id: u64, steam_id: u64, code: &str, code_type: EAuthSessionGuardType) -> Result<(), SessionError> {
187 let request = CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest {
188 client_id: Some(client_id),
189 steamid: Some(steam_id),
190 code: Some(code.to_string()),
191 code_type: Some(code_type as i32),
192 };
193
194 let _: () = self.send_request_no_response("Authentication", "UpdateAuthSessionWithSteamGuardCode", 1, &request, None).await?;
195
196 Ok(())
197 }
198
199 pub async fn poll_login_status(&self, client_id: u64, request_id: &[u8]) -> Result<PollLoginStatusResponse, SessionError> {
201 let request = CAuthenticationPollAuthSessionStatusRequest { client_id: Some(client_id), request_id: Some(request_id.to_vec()), token_to_revoke: None };
202
203 let response: CAuthenticationPollAuthSessionStatusResponse = self.send_request("Authentication", "PollAuthSessionStatus", 1, &request, None).await?;
204
205 Ok(PollLoginStatusResponse {
206 new_client_id: response.new_client_id,
207 new_challenge_url: response.new_challenge_url,
208 refresh_token: response.refresh_token,
209 access_token: response.access_token,
210 had_remote_interaction: response.had_remote_interaction.unwrap_or(false),
211 account_name: response.account_name,
212 new_steam_guard_machine_auth: response.new_guard_data,
213 })
214 }
215
216 pub async fn generate_access_token(&self, refresh_token: &str, steam_id: u64, renew: bool) -> Result<(String, Option<String>), SessionError> {
218 let request = CAuthenticationAccessTokenGenerateForAppRequest {
219 refresh_token: Some(refresh_token.to_string()),
220 steamid: Some(steam_id),
221 renewal_type: Some(if renew { ETokenRenewalType::KETokenRenewalTypeAllow as i32 } else { ETokenRenewalType::KETokenRenewalTypeNone as i32 }),
222 };
223
224 let response: CAuthenticationAccessTokenGenerateForAppResponse = self.send_request("Authentication", "GenerateAccessTokenForApp", 1, &request, None).await?;
225
226 Ok((response.access_token.unwrap_or_default(), response.refresh_token))
227 }
228
229 pub async fn get_auth_session_info(&self, access_token: &str, client_id: u64) -> Result<CAuthenticationGetAuthSessionInfoResponse, SessionError> {
231 let request = CAuthenticationGetAuthSessionInfoRequest { client_id: Some(client_id) };
232
233 self.send_request("Authentication", "GetAuthSessionInfo", 1, &request, Some(access_token)).await
234 }
235
236 #[allow(clippy::too_many_arguments)]
238 pub async fn submit_mobile_confirmation(&self, access_token: &str, version: i32, client_id: u64, steam_id: u64, signature: &[u8], confirm: bool, persistence: ESessionPersistence) -> Result<(), SessionError> {
239 let request = CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest {
240 version: Some(version),
241 client_id: Some(client_id),
242 steamid: Some(steam_id),
243 signature: Some(signature.to_vec()),
244 confirm: Some(confirm),
245 persistence: Some(persistence as i32),
246 };
247
248 self.send_request_no_response("Authentication", "UpdateAuthSessionWithMobileConfirmation", 1, &request, Some(access_token)).await
249 }
250
251 async fn send_request<Req: Message, Resp: Message + Default>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<Resp, SessionError> {
253 let platform_data = self.get_platform_data();
254 let request_data = request.encode_to_vec();
255
256 let api_request = ApiRequest {
257 api_interface: interface.to_string(),
258 api_method: method.to_string(),
259 api_version: version,
260 access_token: access_token.map(String::from),
261 request_data: Some(request_data),
262 headers: platform_data.headers,
263 };
264
265 let response = self.transport.send_request(api_request).await?;
266
267 if let Some(result) = response.result {
269 if result != 1 {
270 return Err(SessionError::from_eresult(result, response.error_message));
272 }
273 }
274
275 let response_data = response.response_data.unwrap_or_default();
277 Ok(Resp::decode(response_data.as_slice())?)
278 }
279
280 async fn send_request_no_response<Req: Message>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<(), SessionError> {
282 let platform_data = self.get_platform_data();
283 let request_data = request.encode_to_vec();
284
285 let api_request = ApiRequest {
286 api_interface: interface.to_string(),
287 api_method: method.to_string(),
288 api_version: version,
289 access_token: access_token.map(String::from),
290 request_data: Some(request_data),
291 headers: platform_data.headers,
292 };
293
294 let response = self.transport.send_request(api_request).await?;
295
296 if let Some(result) = response.result {
298 if result != 1 {
299 return Err(SessionError::from_eresult(result, response.error_message));
300 }
301 }
302
303 Ok(())
304 }
305
306 fn get_platform_data(&self) -> PlatformData {
308 match self.platform_type {
309 EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient => {
310 let machine_name = self.client_friendly_name.clone().unwrap_or_else(get_spoofed_hostname);
311
312 PlatformData {
313 website_id: "Unknown".to_string(),
314 headers: HashMap::from([("user-agent".to_string(), "Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US; Valve Steam Client/default/1665786434; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36".to_string()), ("origin".to_string(), "https://steamloopback.host".to_string())]),
315 device_details: DeviceDetails {
316 device_friendly_name: machine_name,
317 platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient,
318 os_type: Some(20), gaming_device_type: Some(1),
320 machine_id: self.machine_id.clone(),
321 },
322 }
323 }
324 EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser => PlatformData {
325 website_id: "Community".to_string(),
326 headers: HashMap::from([("user-agent".to_string(), self.web_user_agent.clone()), ("origin".to_string(), "https://steamcommunity.com".to_string()), ("referer".to_string(), "https://steamcommunity.com".to_string())]),
327 device_details: DeviceDetails {
328 device_friendly_name: self.web_user_agent.clone(),
329 platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser,
330 os_type: None,
331 gaming_device_type: None,
332 machine_id: None,
333 },
334 },
335 EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp => PlatformData {
336 website_id: "Mobile".to_string(),
337 headers: HashMap::from([("user-agent".to_string(), "okhttp/4.9.2".to_string()), ("cookie".to_string(), "mobileClient=android; mobileClientVersion=777777 3.10.3".to_string())]),
338 device_details: DeviceDetails {
339 device_friendly_name: "Galaxy S25".to_string(),
340 platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp,
341 os_type: Some(-500), gaming_device_type: Some(528),
343 machine_id: None,
344 },
345 },
346 _ => PlatformData {
347 website_id: "Community".to_string(),
348 headers: HashMap::new(),
349 device_details: DeviceDetails {
350 device_friendly_name: "Unknown".to_string(),
351 platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeUnknown,
352 os_type: None,
353 gaming_device_type: None,
354 machine_id: None,
355 },
356 },
357 }
358 }
359}