steam_client/services/
twofactor.rs1use crate::{error::SteamError, SteamClient};
7
8#[derive(Debug, Clone)]
10pub struct TwoFactorSecrets {
11 pub shared_secret: String,
13 pub identity_secret: String,
15 pub secret_1: String,
17 pub revocation_code: String,
19 pub serial_number: u64,
21 pub uri: Option<String>,
23 pub server_time: u64,
25 pub account_name: Option<String>,
27 pub phone_number_hint: Option<String>,
29 pub status: i32,
31}
32
33impl SteamClient {
34 pub async fn enable_two_factor(&mut self) -> Result<TwoFactorSecrets, SteamError> {
53 if !self.is_logged_in() {
54 return Err(SteamError::NotLoggedOn);
55 }
56
57 let steam_id = self.steam_id.as_ref().ok_or(SteamError::NotLoggedOn)?.steam_id64();
58
59 let device_id = format!("android:{}", uuid::Uuid::new_v4());
61
62 let request = steam_protos::CTwoFactorAddAuthenticatorRequest {
63 steamid: Some(steam_id),
64 authenticator_type: Some(1), device_identifier: Some(device_id),
66 sms_phone_id: Some("1".to_string()),
67 version: Some(2),
68 http_headers: vec![],
69 ..Default::default()
70 };
71
72 let response: steam_protos::CTwoFactorAddAuthenticatorResponse = self.send_unified_message("TwoFactor.AddAuthenticator#1", &request).await?;
75
76 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
77
78 Ok(TwoFactorSecrets {
79 shared_secret: BASE64_STANDARD.encode(response.shared_secret.ok_or_else(|| SteamError::Other("Missing shared_secret".into()))?),
80 identity_secret: BASE64_STANDARD.encode(response.identity_secret.ok_or_else(|| SteamError::Other("Missing identity_secret".into()))?),
81 secret_1: BASE64_STANDARD.encode(response.secret_1.ok_or_else(|| SteamError::Other("Missing secret_1".into()))?),
82 revocation_code: response.revocation_code.ok_or_else(|| SteamError::Other("Missing revocation_code".into()))?,
83 serial_number: response.serial_number.unwrap_or_default(),
84 uri: response.uri,
85 server_time: response.server_time.unwrap_or_default(),
86 account_name: response.account_name,
87 phone_number_hint: response.phone_number_hint,
88 status: response.status.unwrap_or_default(),
89 })
90 }
91
92 pub async fn finalize_two_factor(&mut self, shared_secret: &str, activation_code: &str) -> Result<(), SteamError> {
112 if !self.is_logged_in() {
113 return Err(SteamError::NotLoggedOn);
114 }
115
116 let steam_id = self.steam_id.as_ref().ok_or(SteamError::NotLoggedOn)?.steam_id64();
117
118 let mut diff: i64 = 0;
119 let mut attempts_left = 30;
120
121 loop {
122 let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(|e| SteamError::Other(e.to_string()))?.as_secs();
124
125 let time_for_code = (current_time as i64) + diff;
127 let auth_code = generate_totp_code(shared_secret, time_for_code)?;
128
129 let request = steam_protos::CTwoFactorFinalizeAddAuthenticatorRequest {
130 steamid: Some(steam_id),
131 authenticator_code: Some(auth_code),
132 authenticator_time: Some(current_time),
133 activation_code: Some(activation_code.to_string()),
134 http_headers: vec![],
135 validate_sms_code: None,
136 };
137
138 let response: steam_protos::CTwoFactorFinalizeAddAuthenticatorResponse = self.send_unified_message("TwoFactor.FinalizeAddAuthenticator#1", &request).await?;
139
140 if let Some(server_time) = response.server_time {
141 diff = (server_time as i64) - (current_time as i64);
142 }
143
144 if response.status == Some(89) {
145 return Err(SteamError::Other("Invalid activation code".into()));
146 }
147
148 if response.success == Some(true) {
149 return Ok(());
150 }
151
152 if response.want_more == Some(true) {
153 attempts_left -= 1;
154 diff += 30;
155
156 if attempts_left <= 0 {
157 return Err(SteamError::Other("Failed to finalize adding authenticator after 30 attempts".into()));
158 }
159 continue;
160 }
161
162 return Err(SteamError::Other(format!("Error {}", response.status.unwrap_or(0))));
163 }
164 }
165
166 #[allow(dead_code)]
168 async fn send_unified_message<Req: prost::Message, Res: prost::Message + Default>(&mut self, method: &str, body: &Req) -> Result<Res, SteamError> {
169 tracing::debug!("Would send unified message: {}", method);
172 let _ = body.encode_to_vec();
173 Err(SteamError::NotImplemented(format!("Unified message {} not yet implemented", method)))
174 }
175}
176
177fn generate_totp_code(shared_secret: &str, time: i64) -> Result<String, SteamError> {
181 let secret = steam_totp::Secret::from_string(shared_secret).map_err(|e| SteamError::Other(format!("Invalid shared secret: {}", e)))?;
182 steam_totp::generate_auth_code_for_time(&secret, time).map_err(|e| SteamError::Other(format!("TOTP generation failed: {}", e)))
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_totp_generation() {
191 let secret = "SGVsbG9Xb3JsZDEyMzQ1Njc4OTA="; let result = generate_totp_code(secret, 1609459200); assert!(result.is_ok());
195 let code = result.unwrap();
196 assert_eq!(code.len(), 5);
197 for c in code.chars() {
199 assert!("23456789BCDFGHJKMNPQRTVWXY".contains(c));
200 }
201 }
202}