steam_mobile/
lib.rs

1//! A port of the famous C# SteamAuth library, that allows users to add/remove a mobile
2//! authenticator, and also confirm/deny mobile confirmations.
3
4#![allow(dead_code, clippy::missing_errors_doc)]
5#![warn(missing_docs, missing_doc_code_examples)]
6#![deny(
7    missing_debug_implementations,
8    missing_copy_implementations,
9    trivial_casts,
10    trivial_numeric_casts,
11    unsafe_code,
12    unused_import_braces,
13    unused_qualifications
14)]
15
16use std::fmt;
17use std::fmt::Debug;
18use std::fmt::Formatter;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22pub use client::Authenticated;
23pub use client::SteamAuthenticator;
24pub use client::Unauthenticated;
25use const_format::concatcp;
26use parking_lot::RwLock;
27pub use reqwest::header::HeaderMap;
28pub use reqwest::Error as HttpError;
29pub use reqwest::Method;
30pub use reqwest::Url;
31use serde::Deserialize;
32use serde::Serialize;
33use steamid_parser::SteamID;
34pub use utils::format_captcha_url;
35use uuid::Uuid;
36pub use web_handler::confirmation::Confirmation;
37pub use web_handler::confirmation::ConfirmationAction;
38pub use web_handler::confirmation::Confirmations;
39pub use web_handler::confirmation::EConfirmationType;
40pub use web_handler::steam_guard_linker::AddAuthenticatorStep;
41
42use crate::errors::AuthError;
43use crate::errors::InternalError;
44use crate::errors::MobileAuthFileError;
45use crate::utils::read_from_disk;
46
47mod adapter;
48pub(crate) mod client;
49pub mod errors;
50mod page_scraper;
51pub(crate) mod retry;
52mod types;
53pub mod user;
54pub(crate) mod utils;
55mod web_handler;
56
57/// Recommended time to allow STEAM to catch up.
58const STEAM_DELAY_MS: u64 = 350;
59/// Extension of the mobile authenticator files.
60const MA_FILE_EXT: &str = ".maFile";
61
62// HOST SHOULD BE USED FOR COOKIE RETRIEVAL INSIDE COOKIE JAR!!
63
64/// Steam Community Cookie Host
65pub const STEAM_COMMUNITY_HOST: &str = "steamcommunity.com";
66/// Steam Help Cookie Host
67pub const STEAM_HELP_HOST: &str = ".help.steampowered.com";
68/// Steam Store Cookie Host
69pub const STEAM_STORE_HOST: &str = ".store.steampowered.com";
70
71/// Should not be used for cookie retrieval. Use `STEAM_COMMUNTY_HOST` instead.
72pub(crate) const STEAM_COMMUNITY_BASE: &str = "https://steamcommunity.com";
73/// Should not be used for cookie retrieval. Use `STEAM_STORE_HOST` instead.
74pub(crate) const STEAM_STORE_BASE: &str = "https://store.steampowered.com";
75/// Should not be used for cookie retrieval. Use `STEAM_API_HOST` instead.
76pub(crate) const STEAM_API_BASE: &str = "https://api.steampowered.com";
77
78pub(crate) const STEAM_LOGIN_BASE: &str = "https://login.steampowered.com";
79
80const MOBILE_REFERER: &str = concatcp!(
81    STEAM_COMMUNITY_BASE,
82    "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client"
83);
84
85#[allow(missing_docs)]
86pub type AuthResult<T> = Result<T, AuthError>;
87
88/// Information that we cache after the login operation to avoid querying Steam multiple times.
89#[derive(Debug, Clone)]
90struct SteamCache {
91    steamid: SteamID,
92    api_key: Option<String>,
93    /// Oauth token recovered at the login.
94    oauth_token: String,
95    access_token: String,
96}
97
98pub(crate) type CacheGuard = Arc<RwLock<SteamCache>>;
99
100impl SteamCache {
101    fn query_tokens(&self) -> Vec<(&'static str, String)> {
102        [("access_token", self.access_token.clone())].to_vec()
103    }
104
105    fn with_login_data(steamid: &str, access_token: String, refresh_token: String) -> Result<Self, InternalError> {
106        let parsed_steamid = SteamID::parse(steamid).ok_or_else(|| {
107            let err_str = format!("Failed to parse {steamid} as SteamID.");
108            InternalError::GeneralFailure(err_str)
109        })?;
110
111        Ok(Self {
112            steamid: parsed_steamid,
113            api_key: None,
114            oauth_token: refresh_token,
115            access_token,
116        })
117    }
118
119    fn set_api_key(&mut self, api_key: Option<String>) {
120        self.api_key = api_key;
121    }
122
123    fn api_key(&self) -> Option<&str> {
124        self.api_key.as_deref()
125    }
126
127    fn steam_id(&self) -> u64 {
128        self.steamid.to_steam64()
129    }
130
131    fn oauth_token(&self) -> &str {
132        &self.oauth_token
133    }
134}
135
136/// The `MobileAuthFile` (.maFile) is the standard file format that custom authenticators use to save auth secrets to
137/// disk.
138///
139/// It follows strictly the JSON format.
140/// Both `identity_secret` and `shared_secret` should be base64 encoded. If you don't know if they are, they probably
141/// already are.
142///
143///
144/// Example:
145/// ```json
146/// {
147///     identity_secret: "secret"
148///     shared_secret: "secret"
149///     device_id: "android:xxxxxxxxxxxxxxx"
150/// }
151/// ```
152#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
153pub struct MobileAuthFile {
154    /// Identity secret is used to generate the confirmation links for our trade requests.
155    /// If we are generating our own Authenticator, this is given by Steam.
156    identity_secret: String,
157    /// The shared secret is used to generate TOTP codes.
158    shared_secret: String,
159    /// Device ID is used to generate the confirmation links for our trade requests.
160    /// Can be retrieved from mobile device, such as a rooted android, iOS, or generated from the account's SteamID if
161    /// creating our own authenticator. Needed for confirmations to trade to work properly.
162    device_id: Option<String>,
163    /// Used if shared secret is lost. Please, don't lose it.
164    revocation_code: Option<String>,
165    /// Account name where this maFile was originated.
166    pub account_name: Option<String>,
167}
168
169impl Debug for MobileAuthFile {
170    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
171        f.debug_struct("MobileAuthFile")
172            .field("AccountName", &self.account_name)
173            .finish()
174    }
175}
176
177impl MobileAuthFile {
178    fn set_device_id(&mut self, device_id: String) {
179        self.device_id = Some(device_id);
180    }
181
182    /// Creates a new `MobileAuthFile`
183    pub fn new<T>(identity_secret: String, shared_secret: String, device_id: T) -> Self
184    where
185        T: Into<Option<String>>,
186    {
187        Self {
188            identity_secret,
189            shared_secret,
190            device_id: device_id.into(),
191            revocation_code: None,
192            account_name: None,
193        }
194    }
195
196    /// Parses a [`MobileAuthFile`] from a json string.
197    pub fn from_json(string: &str) -> Result<Self, MobileAuthFileError> {
198        serde_json::from_str::<Self>(string)
199            .map_err(|e| MobileAuthFileError::InternalError(InternalError::DeserializationError(e)))
200    }
201
202    /// Convenience function that imports the file from disk
203    ///
204    /// # Panic
205    /// Will panic if file is not found.
206    pub fn from_disk<T>(path: T) -> Result<Self, MobileAuthFileError>
207    where
208        T: Into<PathBuf>,
209    {
210        let buffer = read_from_disk(path);
211        Self::from_json(&buffer)
212    }
213}
214
215#[derive(Serialize, Deserialize, Debug)]
216/// Identifies the mobile device and needed to generate confirmation links.
217///
218/// It is on the format of a UUID V4.
219struct DeviceId(String);
220
221impl DeviceId {
222    const PREFIX: &'static str = "android:";
223
224    /// Generates a random device ID on the format of UUID v4
225    /// Example: android:780c3700-2b4f-4b9a-a196-9af6e6010d09
226    pub fn generate() -> Self {
227        Self(Self::PREFIX.to_owned() + &Uuid::new_v4().to_string())
228    }
229    pub fn validate() {}
230}