steam_mobile/web_handler/steam_guard_linker/
mod.rs1use std::time::Duration;
2
3use const_format::concatcp;
4use futures::TryFutureExt;
5use futures_timer::Delay;
6use parking_lot::lock_api::RwLockReadGuard;
7use parking_lot::RawRwLock;
8use reqwest::Method;
9use tracing::debug;
10
11use crate::client::MobileClient;
12use crate::errors::AuthError;
13use crate::errors::LinkerError;
14use crate::utils::dump_cookies_by_domain_and_name;
15use crate::utils::generate_canonical_device_id;
16use crate::web_handler::steam_guard_linker::types::AddAuthenticatorErrorResponseBase;
17use crate::web_handler::steam_guard_linker::types::AddAuthenticatorRequest;
18use crate::web_handler::steam_guard_linker::types::AddAuthenticatorResponseBase;
19use crate::web_handler::steam_guard_linker::types::FinalizeAddAuthenticatorBase;
20use crate::web_handler::steam_guard_linker::types::FinalizeAddAuthenticatorErrorBase;
21use crate::web_handler::steam_guard_linker::types::FinalizeAddAuthenticatorRequest;
22use crate::web_handler::steam_guard_linker::types::GenericSuccessResponse;
23use crate::web_handler::steam_guard_linker::types::HasPhoneResponse;
24use crate::web_handler::steam_guard_linker::types::PhoneAjaxRequest;
25use crate::web_handler::steam_guard_linker::types::QueryStatusResponseBase;
26use crate::web_handler::steam_guard_linker::types::RemoveAuthenticatorRequest;
27use crate::web_handler::steam_guard_linker::types::RemoveAuthenticatorResponseBase;
28use crate::CacheGuard;
29use crate::MobileAuthFile;
30use crate::SteamCache;
31use crate::STEAM_API_BASE;
32use crate::STEAM_COMMUNITY_BASE;
33use crate::STEAM_COMMUNITY_HOST;
34
35mod types;
36
37pub use types::QueryStatusResponse;
38
39const PHONEAJAX_URL: &str = concatcp!(STEAM_COMMUNITY_BASE, "/steamguard/phoneajax");
40pub const STEAM_ADD_PHONE_CATCHUP_SECS: u64 = 5;
41
42type LinkerResult<T> = Result<T, LinkerError>;
43
44pub struct Authenticator {
46 phone_number: String,
47}
48
49struct AuthenticatorOptions {
50 save_path: String,
51 print_output: bool,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum AddAuthenticatorStep {
57 InitialStep,
59 EmailConfirmation,
61 MobileAuth(MobileAuthFile),
63}
64
65const QUERY_STATUS_ENDPOINT: &str = concatcp!(STEAM_API_BASE, "/ITwoFactorService/QueryStatus/v1/");
66
67pub async fn twofactor_status(client: &MobileClient, cache: CacheGuard) -> LinkerResult<QueryStatusResponse> {
69 let cache = cache.read();
70 let steamid = &[("steamid", cache.steamid.to_steam64())];
71 let query = cache.query_tokens();
72
73 let response = client
74 .request_with_session_guard_and_decode::<_, _, QueryStatusResponseBase>(
75 QUERY_STATUS_ENDPOINT.to_owned(),
76 Method::POST,
77 None,
78 Some(steamid),
79 Some(query),
80 )
81 .await?
82 .inner;
83
84 Ok(response)
85}
86
87pub async fn account_has_phone(client: &MobileClient) -> LinkerResult<bool> {
90 let session_id =
91 dump_cookies_by_domain_and_name(&client.cookie_store.read(), STEAM_COMMUNITY_HOST, "sessionid").unwrap();
92 let payload = PhoneAjaxRequest::has_phone(&session_id);
93
94 let response: HasPhoneResponse = client
95 .request_with_session_guard(
96 PHONEAJAX_URL.to_owned(),
97 Method::POST,
98 None,
99 Some(payload),
100 None::<&str>,
101 )
102 .and_then(|x| x.json::<HasPhoneResponse>().err_into())
103 .await?;
104
105 Ok(response.user_has_phone)
106}
107
108pub async fn check_sms(client: &MobileClient, sms_code: &str) -> LinkerResult<bool> {
109 let session_id =
110 dump_cookies_by_domain_and_name(&client.cookie_store.read(), STEAM_COMMUNITY_HOST, "sessionid").unwrap();
111 let payload = PhoneAjaxRequest::check_sms(&session_id, sms_code);
112
113 let response = client
114 .request_with_session_guard_and_decode::<_, _, GenericSuccessResponse>(
115 PHONEAJAX_URL.to_owned(),
116 Method::POST,
117 None,
118 Some(payload),
119 None::<&str>,
120 )
121 .await?;
122
123 Ok(response.success)
124}
125
126pub async fn check_email_confirmation(client: &MobileClient) -> LinkerResult<bool> {
129 let session_id =
130 dump_cookies_by_domain_and_name(&client.cookie_store.read(), STEAM_COMMUNITY_HOST, "sessionid").unwrap();
131 let payload = PhoneAjaxRequest::check_email_confirmation(&*session_id);
132
133 let response: GenericSuccessResponse = client
134 .request_with_session_guard_and_decode::<_, _, GenericSuccessResponse>(
135 PHONEAJAX_URL.to_owned(),
136 Method::POST,
137 None,
138 Some(payload),
139 None::<&str>,
140 )
141 .await?;
142
143 Ok(response.success)
144}
145
146pub async fn add_phone_to_account(client: &MobileClient, phone_number: &str) -> LinkerResult<bool> {
147 let session_id =
148 dump_cookies_by_domain_and_name(&client.cookie_store.read(), STEAM_COMMUNITY_HOST, "sessionid").unwrap();
149
150 let payload = PhoneAjaxRequest::add_phone(&*session_id, phone_number);
151
152 let response = client
153 .request_with_session_guard_and_decode::<_, _, GenericSuccessResponse>(
154 PHONEAJAX_URL.to_owned(),
155 Method::POST,
156 None,
157 Some(payload),
158 None::<&str>,
159 )
160 .await?;
161
162 Ok(response.success)
163}
164
165pub fn validate_phone_number(phone_number: &str) -> bool {
166 phone_number.starts_with('+')
167}
168
169pub(crate) async fn finalize(
171 client: &MobileClient,
172 cached_data: RwLockReadGuard<'_, RawRwLock, SteamCache>,
173 mafile: &MobileAuthFile,
174 sms_code: &str,
175) -> LinkerResult<()> {
176 let steamid = cached_data.steam_id().to_string();
177 let oauth_token = cached_data.oauth_token();
178
179 let finalize_url = format!(
180 "{}{}",
181 STEAM_API_BASE, "/ITwoFactorService/FinalizeAddAuthenticator/v0001"
182 );
183
184 let mut initial_payload = FinalizeAddAuthenticatorRequest {
185 steamid: &*steamid,
186 oauth_token,
187 sms_activation_code: sms_code,
188 ..Default::default()
189 };
190
191 let account_secret = steam_totp::Secret::from_b64(&mafile.shared_secret).unwrap();
192
193 let mut tries: usize = 0;
194 while tries <= 30 {
195 let (code, mut time) = steam_totp::generate_auth_code_with_time_async(account_secret.clone()).await?;
196 time.0 += 1;
197 initial_payload.swap_codes(code, time.0);
198
199 let response_text = client
200 .request_with_session_guard(
201 finalize_url.clone(),
202 Method::POST,
203 None,
204 Some(&initial_payload),
205 None::<&str>,
206 )
207 .and_then(|resp| resp.text().err_into())
208 .await?;
209
210 debug!("FinalizeAuthenticator raw response: {:#}", response_text);
211
212 let response = match serde_json::from_str::<FinalizeAddAuthenticatorBase>(&*response_text) {
213 Ok(resp) => resp.response,
214 Err(_err) => {
215 let error_resp = serde_json::from_str::<FinalizeAddAuthenticatorErrorBase>(&*response_text).unwrap();
216 return match error_resp.response.status {
217 89 => Err(LinkerError::BadSMSCode),
218 88 => {
219 if tries == 30 {
220 return Err(LinkerError::UnableToGenerateCorrectCodes);
221 }
222 continue;
223 }
224 _ => Err(LinkerError::GeneralFailure("Something went wrong".to_string())),
225 };
226 }
227 };
228
229 if response.want_more {
231 Delay::new(Duration::from_secs(1)).await;
232 tries += 1;
233 continue;
234 }
235
236 return Ok(());
237 }
238
239 Err(LinkerError::GeneralFailure(
240 "Maximum tries achieved. Something went wrong.".to_string(),
241 ))
242}
243
244pub(crate) async fn add_authenticator_to_account(
251 client: &MobileClient,
252 cached_data: RwLockReadGuard<'_, RawRwLock, SteamCache>,
253) -> Result<MobileAuthFile, LinkerError> {
254 let add_auth_url = format!("{}{}", STEAM_API_BASE, "/ITwoFactorService/AddAuthenticator/v0001");
255 let oauth_token = cached_data.oauth_token();
256 let steamid = cached_data.steam_id().to_string();
257 let time = steam_totp::time::Time::with_offset().await?.to_string();
258
259 let payload = AddAuthenticatorRequest::new(oauth_token, &steamid, time.parse().unwrap());
260
261 let response_text = client
262 .request_with_session_guard(add_auth_url, Method::POST, None, Some(payload), None::<&str>)
263 .and_then(|resp| resp.text().err_into())
264 .await?;
265
266 debug!("Steam addauth raw response: {:?}", response_text);
267
268 let mut mafile = match serde_json::from_str::<AddAuthenticatorResponseBase>(&response_text) {
269 Ok(resp) => resp.steam_guard_success_details.mobile_auth,
270 Err(err) => {
271 eprintln!("Error found deserializing add auth response: {:#?}", err);
272 let error_resp = serde_json::from_str::<AddAuthenticatorErrorResponseBase>(&response_text).unwrap();
273 return match error_resp.response.status {
274 29 => Err(LinkerError::AuthenticatorPresent),
275 2 => Err(LinkerError::GeneralFailure(
276 "After too many failed attempts, this may be a lock on the phone number or account. Going to test \
277 this tomorrow."
278 .to_string(),
279 )),
280 _ => Err(LinkerError::GeneralFailure("Something went wrong".to_string())),
281 };
282 }
283 };
284 mafile.set_device_id(generate_canonical_device_id(&steamid));
285 Ok(mafile)
286}
287
288#[derive(Debug, PartialEq, Copy, Clone)]
289pub enum RemoveAuthenticatorScheme {
290 ReturnToEmailCodes,
291 RemoveSteamGuard,
292}
293
294impl RemoveAuthenticatorScheme {
295 fn as_str(self) -> &'static str {
296 match self {
297 RemoveAuthenticatorScheme::ReturnToEmailCodes => "1",
298 RemoveAuthenticatorScheme::RemoveSteamGuard => "2",
299 }
300 }
301}
302
303pub(crate) async fn remove_authenticator(
305 client: &MobileClient,
306 cached_data: RwLockReadGuard<'_, RawRwLock, SteamCache>,
307 revocation_token: &str,
308 remove_authenticator_scheme: RemoveAuthenticatorScheme,
309) -> Result<(), AuthError> {
310 let _url = format!(
311 "{}{}",
312 STEAM_API_BASE, "/ITwoFactorService/RemoveAuthenticator/v1?access_token="
313 );
314
315 let steamid = cached_data.steam_id().to_string();
316 let oauth_token = cached_data.oauth_token();
317 let payload = RemoveAuthenticatorRequest::new(oauth_token, steamid, revocation_token, remove_authenticator_scheme);
318
319 client
321 .request_with_session_guard_and_decode::<_, _, RemoveAuthenticatorResponseBase>(
322 _url,
323 Method::POST,
324 None,
325 Some(payload),
326 None::<&str>,
327 )
328 .await
329 .map(|_| ())
330 .map_err(Into::into)
331}