1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Two-factor authentication (2FA) management.
//!
//! This module provides functionality to enable and finalize TOTP
//! two-factor authentication on Steam accounts.
use crate::{error::SteamError, SteamClient};
/// Two-factor authentication secrets returned when enabling 2FA.
#[derive(Debug, Clone)]
pub struct TwoFactorSecrets {
/// The shared secret used to generate TOTP codes (base64 encoded).
pub shared_secret: String,
/// The identity secret used for trade confirmations (base64 encoded).
pub identity_secret: String,
/// Secret 1 (base64 encoded).
pub secret_1: String,
/// The revocation code to disable 2FA.
pub revocation_code: String,
/// Serial number of the authenticator.
pub serial_number: u64,
/// URI for adding to authenticator apps.
pub uri: Option<String>,
/// Steam server time when 2FA was enabled.
pub server_time: u64,
/// Account name.
pub account_name: Option<String>,
/// Phone number hint (last digits).
pub phone_number_hint: Option<String>,
/// Status code from Steam.
pub status: i32,
}
impl SteamClient {
/// Enable TOTP two-factor authentication on this account.
///
/// This begins the 2FA setup process. Steam will send an SMS with
/// an activation code that must be passed to [`finalize_two_factor`].
///
/// # Returns
///
/// Returns [`TwoFactorSecrets`] containing the shared secret and other
/// data needed to generate TOTP codes. **Save these securely!**
///
/// # Example
///
/// ```rust,ignore
/// let secrets = client.enable_two_factor().await?;
/// tracing::info!("Shared secret: {}", secrets.shared_secret);
/// tracing::info!("Revocation code: {}", secrets.revocation_code);
/// // Wait for SMS, then call finalize_two_factor
/// ```
pub async fn enable_two_factor(&mut self) -> Result<TwoFactorSecrets, SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let steam_id = self.steam_id.as_ref().ok_or(SteamError::NotLoggedOn)?.steam_id64();
// Generate device identifier (similar to steam-totp getDeviceID)
let device_id = format!("android:{}", uuid::Uuid::new_v4());
let request = steam_protos::CTwoFactorAddAuthenticatorRequest {
steamid: Some(steam_id),
authenticator_type: Some(1), // TOTP
device_identifier: Some(device_id),
sms_phone_id: Some("1".to_string()),
version: Some(2),
http_headers: vec![],
..Default::default()
};
// Send unified message - this would need the unified message infrastructure
// For now, we'll use the service method pattern
let response: steam_protos::CTwoFactorAddAuthenticatorResponse = self.send_unified_message("TwoFactor.AddAuthenticator#1", &request).await?;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
Ok(TwoFactorSecrets {
shared_secret: BASE64_STANDARD.encode(response.shared_secret.ok_or_else(|| SteamError::Other("Missing shared_secret".into()))?),
identity_secret: BASE64_STANDARD.encode(response.identity_secret.ok_or_else(|| SteamError::Other("Missing identity_secret".into()))?),
secret_1: BASE64_STANDARD.encode(response.secret_1.ok_or_else(|| SteamError::Other("Missing secret_1".into()))?),
revocation_code: response.revocation_code.ok_or_else(|| SteamError::Other("Missing revocation_code".into()))?,
serial_number: response.serial_number.unwrap_or_default(),
uri: response.uri,
server_time: response.server_time.unwrap_or_default(),
account_name: response.account_name,
phone_number_hint: response.phone_number_hint,
status: response.status.unwrap_or_default(),
})
}
/// Finalize the two-factor authentication setup.
///
/// After calling [`enable_two_factor`], Steam sends an SMS with an
/// activation code. Call this method with that code to complete the 2FA
/// setup.
///
/// # Arguments
///
/// * `shared_secret` - The shared secret returned from
/// [`enable_two_factor`]
/// * `activation_code` - The SMS activation code from Steam
///
/// # Example
///
/// ```rust,ignore
/// // After receiving SMS code
/// client.finalize_two_factor(&secrets.shared_secret, "ABC123").await?;
/// tracing::info!("2FA enabled successfully!");
/// ```
pub async fn finalize_two_factor(&mut self, shared_secret: &str, activation_code: &str) -> Result<(), SteamError> {
if !self.is_logged_in() {
return Err(SteamError::NotLoggedOn);
}
let steam_id = self.steam_id.as_ref().ok_or(SteamError::NotLoggedOn)?.steam_id64();
let mut diff: i64 = 0;
let mut attempts_left = 30;
loop {
// Get current time
let current_time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(|e| SteamError::Other(e.to_string()))?.as_secs();
// Generate TOTP code from shared secret with time offset
let time_for_code = (current_time as i64) + diff;
let auth_code = generate_totp_code(shared_secret, time_for_code)?;
let request = steam_protos::CTwoFactorFinalizeAddAuthenticatorRequest {
steamid: Some(steam_id),
authenticator_code: Some(auth_code),
authenticator_time: Some(current_time),
activation_code: Some(activation_code.to_string()),
http_headers: vec![],
validate_sms_code: None,
};
let response: steam_protos::CTwoFactorFinalizeAddAuthenticatorResponse = self.send_unified_message("TwoFactor.FinalizeAddAuthenticator#1", &request).await?;
if let Some(server_time) = response.server_time {
diff = (server_time as i64) - (current_time as i64);
}
if response.status == Some(89) {
return Err(SteamError::Other("Invalid activation code".into()));
}
if response.success == Some(true) {
return Ok(());
}
if response.want_more == Some(true) {
attempts_left -= 1;
diff += 30;
if attempts_left <= 0 {
return Err(SteamError::Other("Failed to finalize adding authenticator after 30 attempts".into()));
}
continue;
}
return Err(SteamError::Other(format!("Error {}", response.status.unwrap_or(0))));
}
}
/// Send a unified service message (internal helper).
#[allow(dead_code)]
async fn send_unified_message<Req: prost::Message, Res: prost::Message + Default>(&mut self, method: &str, body: &Req) -> Result<Res, SteamError> {
// Unified messages use a special job-based format
// For now, return not implemented
tracing::debug!("Would send unified message: {}", method);
let _ = body.encode_to_vec();
Err(SteamError::NotImplemented(format!("Unified message {} not yet implemented", method)))
}
}
/// Generate a TOTP code from a shared secret.
///
/// This uses the `steam-totp` crate for proper Steam TOTP generation.
fn generate_totp_code(shared_secret: &str, time: i64) -> Result<String, SteamError> {
let secret = steam_totp::Secret::from_string(shared_secret).map_err(|e| SteamError::Other(format!("Invalid shared secret: {}", e)))?;
steam_totp::generate_auth_code_for_time(&secret, time).map_err(|e| SteamError::Other(format!("TOTP generation failed: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_totp_generation() {
// Test with a known secret (this is just a test, not a real secret)
let secret = "SGVsbG9Xb3JsZDEyMzQ1Njc4OTA="; // Base64 encoded test data
let result = generate_totp_code(secret, 1609459200); // 2021-01-01 00:00:00 UTC
assert!(result.is_ok());
let code = result.unwrap();
assert_eq!(code.len(), 5);
// All characters should be from Steam's alphabet
for c in code.chars() {
assert!("23456789BCDFGHJKMNPQRTVWXY".contains(c));
}
}
}