1use scraper::{Html, Selector};
4use steam_totp::{generate_auth_code, generate_device_id, Secret};
5
6use crate::{
7 client::SteamUser,
8 endpoint::steam_endpoint,
9 error::SteamUserError,
10 types::{SteamGuardStatus, TwoFactorResponse},
11};
12
13impl SteamUser {
14 #[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/AddAuthenticator/v1/", kind = Auth)]
20 pub async fn enable_two_factor(&self) -> Result<TwoFactorResponse, SteamUserError> {
21 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
22 let device_id = generate_device_id(steam_id, None);
23
24 let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?.clone();
26
27 let response: serde_json::Value = self.post_path("/ITwoFactorService/AddAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id.steam_id64().to_string()), ("authenticator_type", "1".to_string()), ("device_identifier", device_id), ("sms_phone_id", "1".to_string()), ("version", "2".to_string())]).send().await?.json().await?;
28
29 let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
30
31 let resp: TwoFactorResponse = serde_json::from_value(response.clone())?;
32
33 if let Some(ref shared_secret) = resp.shared_secret {
34 *self.session.shared_secret.lock() = Some(shared_secret.clone());
35 }
36
37 if resp.status != 1 {
38 return Err(SteamUserError::from_eresult(resp.status));
39 }
40
41 Ok(resp)
42 }
43
44 #[tracing::instrument(skip(self))]
47 pub async fn add_authenticator(&self) -> Result<TwoFactorResponse, SteamUserError> {
48 self.enable_two_factor().await
49 }
50
51 #[tracing::instrument(skip(self, shared_secret, activation_code))]
58 pub async fn finalize_two_factor(&self, shared_secret: &str, activation_code: &str) -> Result<(), SteamUserError> {
59 *self.session.shared_secret.lock() = Some(shared_secret.to_string());
60 self.finalize_authenticator(activation_code).await
61 }
62
63 #[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/FinalizeAddAuthenticator/v1/", kind = Auth)]
70 pub async fn finalize_authenticator(&self, activation_code: &str) -> Result<(), SteamUserError> {
71 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
72 let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?.clone();
73
74 let shared_secret = self.session.shared_secret.lock().as_ref().ok_or(SteamUserError::MissingCredential { field: "shared_secret" })?.clone();
75
76 let mut time_offset = self.time_offset.lock().unwrap_or(0);
77 let mut attempts_left = 30;
78
79 while attempts_left > 0 {
80 let current_time = i64::try_from(
81 std::time::SystemTime::now()
82 .duration_since(std::time::UNIX_EPOCH)?
83 .as_secs(),
84 )
85 .unwrap_or(0);
86
87 let authenticator_time = current_time + time_offset;
88 let secret = Secret::from_string(&shared_secret)?;
89 let authenticator_code = generate_auth_code(&secret, time_offset)?;
90
91 let response: serde_json::Value = self.post_path("/ITwoFactorService/FinalizeAddAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id.steam_id64().to_string()), ("authenticator_code", authenticator_code), ("authenticator_time", authenticator_time.to_string()), ("activation_code", activation_code.to_string())]).send().await?.json().await?;
92
93 let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
94
95 if let Some(server_time) = response.get("server_time").and_then(|v| v.as_i64()) {
96 time_offset = server_time - current_time;
97 *self.time_offset.lock() = Some(time_offset);
98 }
99
100 let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
101
102 if success {
103 return Ok(());
104 }
105
106 let status = i32::try_from(response.get("status").and_then(|v| v.as_i64()).unwrap_or(0)).unwrap_or(0);
107 if status == 89 {
108 return Err(SteamUserError::TwoFactorError("Invalid activation code".into()));
109 }
110
111 attempts_left -= 1;
112 time_offset += 30;
113 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
114 }
115
116 Err(SteamUserError::TwoFactorError("Failed to finalize adding authenticator after 30 attempts".into()))
117 }
118
119 #[steam_endpoint(POST, host = Api, path = "/ITwoFactorService/RemoveAuthenticator/v1/", kind = Auth)]
121 pub async fn disable_two_factor(&self, revocation_code: &str) -> Result<(), SteamUserError> {
122 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
123 let access_token = self.session.mobile_access_token.as_ref().ok_or(SteamUserError::MissingCredential { field: "mobile_access_token" })?;
124
125 let steam_id_str = steam_id.steam_id64().to_string();
126
127 let response: serde_json::Value = self.post_path("/ITwoFactorService/RemoveAuthenticator/v1/").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).form(&[("steamid", steam_id_str.as_str()), ("revocation_code", revocation_code), ("steamguard_scheme", "1")]).send().await?.json().await?;
128
129 let response = response.get("response").ok_or_else(|| SteamUserError::MalformedResponse("Missing response object".into()))?;
130
131 let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
132
133 if !success {
134 if let Some(status) = response.get("status").and_then(|v| v.as_i64()) {
135 return Err(SteamUserError::from_eresult(i32::try_from(status).unwrap_or(0)));
136 }
137 return Err(SteamUserError::TwoFactorError("Failed to remove authenticator".into()));
138 }
139
140 Ok(())
141 }
142
143 #[tracing::instrument(skip(self, revocation_code))]
148 pub async fn remove_authenticator(&self, revocation_code: &str) -> Result<(), SteamUserError> {
149 self.disable_two_factor(revocation_code).await
150 }
151
152 #[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
154 pub async fn deauthorize_devices(&self) -> Result<(), SteamUserError> {
155 let response: serde_json::Value = self.post_path("/twofactor/manage_action").header("origin", "https://store.steampowered.com").header("referer", "https://store.steampowered.com/twofactor/manage").form(&[("action", "deauthorize")]).send().await?.json().await?;
156
157 if response.is_null() {
160 return Err(SteamUserError::MalformedResponse("Failed to deauthorize devices".into()));
161 }
162
163 Ok(())
164 }
165
166 #[steam_endpoint(GET, host = Store, path = "/twofactor/manage_action", kind = Read)]
169 pub async fn get_steam_guard_status(&self) -> Result<SteamGuardStatus, SteamUserError> {
170 let response = self.get_path("/twofactor/manage_action").send().await?.text().await?;
171
172 let document = Html::parse_document(&response);
173
174 let mobile_selector = Selector::parse("#steam_authenticator_form #steam_authenticator_check[checked]").expect("valid CSS selector");
175 let email_selector = Selector::parse("#email_authenticator_form #email_authenticator_check[checked]").expect("valid CSS selector");
176 let none_selector = Selector::parse("#none_authenticator_form #none_authenticator_check[checked]").expect("valid CSS selector");
177
178 if document.select(&mobile_selector).next().is_some() {
179 return Ok(SteamGuardStatus::Mobile);
180 }
181 if document.select(&email_selector).next().is_some() {
182 return Ok(SteamGuardStatus::Email);
183 }
184 if document.select(&none_selector).next().is_some() {
185 return Ok(SteamGuardStatus::None);
186 }
187
188 Err(SteamUserError::MalformedResponse("Could not determine Steam Guard status".into()))
189 }
190
191 #[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
193 pub async fn enable_steam_guard_email(&self) -> Result<bool, SteamUserError> {
194 let response = self.post_path("/twofactor/manage_action").header("referer", "https://store.steampowered.com/twofactor/manage").form(&[("action", "email"), ("email_authenticator_check", "on")]).send().await?.text().await?;
195
196 let document = Html::parse_document(&response);
197 let title_selector = Selector::parse("title").expect("valid CSS selector");
198 let check_selector = Selector::parse("#email_authenticator_check[checked]").expect("valid CSS selector");
199
200 let title_ok = document.select(&title_selector).next().map(|t| t.text().collect::<String>() == "Steam Guard Mobile Authenticator").unwrap_or(false);
201
202 let checked = document.select(&check_selector).next().is_some();
203
204 Ok(title_ok && checked)
205 }
206
207 #[steam_endpoint(POST, host = Store, path = "/twofactor/manage_action", kind = Auth)]
209 pub async fn disable_steam_guard_email(&self) -> Result<bool, SteamUserError> {
210 let response = self.post_path("/twofactor/manage_action").header("referer", "https://store.steampowered.com/twofactor/manage_action").form(&[("action", "actuallynone")]).send().await?.text().await?;
211
212 let document = Html::parse_document(&response);
213 let title_selector = Selector::parse("title").expect("valid CSS selector");
214
215 let title_ok = document.select(&title_selector).next().map(|t| t.text().collect::<String>() == "Steam Guard Mobile Authenticator").unwrap_or(false);
216 let text_ok = response.contains("Turning Steam Guard off requires confirmation. We've sent you an email with a link to confirm disabling Steam Guard.");
217
218 Ok(title_ok && text_ok)
219 }
220}