steam_mobile/web_handler/steam_guard_linker/
mod.rs

1use 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
44/// By default, your `MobileAuth` file will always be printed to the terminal.
45pub struct Authenticator {
46    phone_number: String,
47}
48
49struct AuthenticatorOptions {
50    save_path: String,
51    print_output: bool,
52}
53
54/// Steps to add an authenticator to a Steam Account.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum AddAuthenticatorStep {
57    /// The user is signing up for the first time.
58    InitialStep,
59    /// The authenticator is awaiting the user's email confirmation to enable the addition of a phone number to Steam.
60    EmailConfirmation,
61    /// Authenticator succeeded and retrieved `MobileAuthFile`.
62    MobileAuth(MobileAuthFile),
63}
64
65const QUERY_STATUS_ENDPOINT: &str = concatcp!(STEAM_API_BASE, "/ITwoFactorService/QueryStatus/v1/");
66
67/// Queries Steam API to check SteamGuard Status.
68pub 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
87/// Queries the `/steamguard/phoneajax` to check if the user has a phone number.
88/// Returns true if user has already a phone registered.
89pub 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
126/// Signals Steam that the user confirmed the phone add request email, and is ready for the next step.
127/// Confirming the email allows `SteamAuthenticator` to register a new phone number to account.
128pub 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
169/// Last step to add a new authenticator.
170pub(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        // Steam want more codes, delay a bit and send all again.
230        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
244/// Add a new authenticator to this steam account.
245///
246///
247/// User will receive a SMS message, with the code required to finalize registering the new authenticator.
248/// Returns the VERY important `SteamGuardAccount`, that must be saved before the next step(finalize auth) is completed,
249/// otherwise the user is on risk of losing the account, since the `revocation_code` will also be lost.
250pub(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
303/// Remove authenticator from account.
304pub(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    // FIXME: add error handling and error variants
320    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}