Skip to main content

steam_client/services/
appauth.rs

1//! App authentication for Steam client.
2//!
3//! This module provides functionality for creating app tickets and
4//! auth session tickets used for game authentication/DRM.
5
6use std::io::Write;
7
8use byteorder::{LittleEndian, WriteBytesExt};
9use steamid::SteamID;
10
11use crate::{error::SteamError, SteamClient};
12
13/// An auth session ticket for game authentication.
14#[derive(Debug, Clone)]
15pub struct AuthSessionTicket {
16    /// The raw ticket data.
17    pub ticket: Vec<u8>,
18    /// Handle for this ticket (used to cancel).
19    pub handle: u32,
20    /// App ID this ticket is for.
21    pub appid: u32,
22    /// SteamID of the ticket owner (0 for self).
23    pub steam_id: u64,
24    /// CRC32 of the ticket.
25    pub ticket_crc: u32,
26    /// State of the ticket.
27    pub estate: u32,
28}
29
30/// Result of activating an auth session ticket.
31#[derive(Debug, Clone)]
32pub struct AuthSessionResult {
33    /// The SteamID of the ticket owner.
34    pub steamid: SteamID,
35    /// Auth session response code.
36    pub auth_session_response: u32,
37}
38
39impl SteamClient {
40    /// Request an encrypted app ticket for a particular app.
41    ///
42    /// The app must be set up on the Steam backend for encrypted app tickets.
43    ///
44    /// # Arguments
45    /// * `appid` - The Steam AppID of the app you want a ticket for
46    /// * `user_data` - Optional user data if the app expects it
47    ///
48    /// # Returns
49    /// The encrypted app ticket as raw bytes.
50    pub async fn create_encrypted_app_ticket(&mut self, appid: u32, user_data: Option<&[u8]>) -> Result<Vec<u8>, SteamError> {
51        if !self.is_logged_in() {
52            return Err(SteamError::NotLoggedOn);
53        }
54
55        let msg = steam_protos::CMsgClientRequestEncryptedAppTicket { app_id: Some(appid), userdata: user_data.map(|d| d.to_vec()) };
56
57        // Send request and wait for response
58        let response: steam_protos::CMsgClientEncryptedAppTicketResponse = self.send_request_and_wait(steam_enums::EMsg::ClientRequestEncryptedAppTicket, &msg).await?;
59
60        if response.eresult.unwrap_or(1) != 1 {
61            return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(response.eresult.unwrap_or(2)).unwrap_or(steam_enums::EResult::Fail)));
62        }
63
64        Ok(response.encrypted_ticket.and_then(|t| t.encrypted_ticket).unwrap_or_default())
65    }
66
67    /// Request an app ownership ticket for a particular app.
68    ///
69    /// # Arguments
70    /// * `appid` - The Steam AppID of the app you want a ticket for
71    ///
72    /// # Returns
73    /// The ownership ticket as raw bytes.
74    pub async fn get_app_ownership_ticket(&mut self, appid: u32) -> Result<Vec<u8>, SteamError> {
75        if !self.is_logged_in() {
76            return Err(SteamError::NotLoggedOn);
77        }
78
79        let msg = steam_protos::CMsgClientGetAppOwnershipTicket { app_id: Some(appid) };
80
81        // Send request and wait for response
82        let response: steam_protos::CMsgClientGetAppOwnershipTicketResponse = self.send_request_and_wait(steam_enums::EMsg::ClientGetAppOwnershipTicket, &msg).await?;
83
84        if response.eresult.unwrap_or(1) != 1 {
85            return Err(SteamError::SteamResult(steam_enums::EResult::from_i32(response.eresult.unwrap_or(2) as i32).unwrap_or(steam_enums::EResult::Fail)));
86        }
87
88        Ok(response.ticket.unwrap_or_default())
89    }
90
91    /// Create an auth session ticket for game server authentication.
92    ///
93    /// This ticket can be sent to a game server which will validate it
94    /// with Steam to verify your identity and ownership.
95    ///
96    /// # Arguments
97    /// * `appid` - The Steam AppID of the game
98    ///
99    /// # Returns
100    /// An AuthSessionTicket that can be used for authentication.
101    pub async fn create_auth_session_ticket(&mut self, appid: u32) -> Result<AuthSessionTicket, SteamError> {
102        if !self.is_logged_in() {
103            return Err(SteamError::NotLoggedOn);
104        }
105
106        if self.gc_tokens.is_empty() {
107            return Err(SteamError::Other("No GC tokens available. Wait for connection to establish fully.".to_string()));
108        }
109
110        // Get an ownership ticket first
111        // Note: In a real implementation this would wait for the ticket response
112        // For now we'll assume we can get one or fail
113        // Since we can't properly wait for the ticket in this structure without a large
114        // refactor, we'll proceed but acknowledge this is incomplete.
115        // In node-steam-user, this gets a ticket from cache or requests one.
116        let ownership_ticket = self.get_app_ownership_ticket(appid).await?;
117
118        // Consume a GC token
119        let gc_token = self.gc_tokens.remove(0);
120
121        // Construct the session ticket buffer
122        let mut buffer = Vec::new();
123
124        // 1. Length-prefixed GC Token
125        buffer.write_u32::<LittleEndian>(gc_token.len() as u32)?;
126        buffer.write_all(&gc_token)?;
127
128        // 2. Length-prefixed Session Header (24 bytes)
129        buffer.write_u32::<LittleEndian>(24)?;
130        buffer.write_u32::<LittleEndian>(1)?; // unknown 1
131        buffer.write_u32::<LittleEndian>(2)?; // unknown 2
132
133        // Convert IP string to int
134        let ip_int = if let Some(ip_str) = self.account.read().public_ip.clone() {
135            match ip_str.parse::<std::net::Ipv4Addr>() {
136                Ok(ip) => u32::from(ip).swap_bytes(), // Network byte order
137                Err(_) => 0,
138            }
139        } else {
140            0
141        };
142        buffer.write_u32::<LittleEndian>(ip_int)?;
143
144        buffer.write_u32::<LittleEndian>(0)?; // filler
145
146        let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
147        let connect_time_ms = self.connect_time; // Assuming this is ms
148        let session_time = (timestamp.saturating_sub(connect_time_ms)) as u32;
149        buffer.write_u32::<LittleEndian>(session_time)?;
150
151        self.connection_count += 1;
152        buffer.write_u32::<LittleEndian>(self.connection_count)?;
153
154        // 3. Length-prefixed Ownership Ticket
155        buffer.write_u32::<LittleEndian>(ownership_ticket.len() as u32)?;
156        buffer.write_all(&ownership_ticket)?;
157
158        // Calculate CRC32
159        let mut crc = flate2::Crc::new();
160        crc.update(&buffer);
161        let crc32 = crc.sum();
162
163        // Create the ticket object
164        let ticket = AuthSessionTicket {
165            ticket: buffer,
166            handle: 0,
167            appid,
168            steam_id: 0, // 0 indicates "self" / our own ticket
169            ticket_crc: crc32,
170            estate: 0,
171        };
172
173        // Activate the ticket
174        self.activate_auth_session_tickets(appid, vec![ticket.clone()]).await?;
175
176        Ok(ticket)
177    }
178
179    /// Cancel an auth session ticket.
180    ///
181    /// Call this when you're done using a ticket to free resources.
182    ///
183    /// # Arguments
184    /// * `ticket` - The ticket to cancel
185    pub async fn cancel_auth_session_ticket(&mut self, ticket: AuthSessionTicket) -> Result<(), SteamError> {
186        if !self.is_logged_in() {
187            return Err(SteamError::NotLoggedOn);
188        }
189
190        // Remove from active tickets
191        if let Some(pos) = self.active_tickets.iter().position(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid && t.ticket_crc == ticket.ticket_crc) {
192            self.active_tickets.remove(pos);
193        }
194
195        // Update the auth list to remove this ticket
196        self.send_auth_list(Some(ticket.appid)).await
197    }
198
199    /// Activate auth session tickets to validate players.
200    ///
201    /// This is typically used by game servers to validate connecting players.
202    ///
203    /// # Arguments
204    /// * `appid` - The app ID
205    /// * `tickets` - Tickets to activate
206    pub async fn activate_auth_session_tickets(&mut self, appid: u32, tickets: Vec<AuthSessionTicket>) -> Result<Vec<AuthSessionResult>, SteamError> {
207        if !self.is_logged_in() {
208            return Err(SteamError::NotLoggedOn);
209        }
210
211        for mut ticket in tickets {
212            // Check if already active
213            if self.active_tickets.iter().any(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid && t.ticket_crc == ticket.ticket_crc) {
214                continue;
215            }
216
217            // If we have an active ticket for this user/app, remove it (unless it's our
218            // own)
219            if ticket.steam_id != 0 {
220                if let Some(pos) = self.active_tickets.iter().position(|t| t.steam_id == ticket.steam_id && t.appid == ticket.appid) {
221                    self.active_tickets.remove(pos);
222                }
223            }
224
225            // Add to active list
226            ticket.estate = if ticket.steam_id == 0 { 0 } else { 1 };
227            self.active_tickets.push(ticket);
228        }
229
230        self.send_auth_list(Some(appid)).await?;
231
232        // Return empty for now - proper implementation would wait for response
233        Ok(Vec::new())
234    }
235
236    /// End auth sessions for specific users.
237    ///
238    /// # Arguments
239    /// * `appid` - The app ID
240    /// * `steamids` - SteamIDs of users to end sessions for
241    pub async fn end_auth_sessions(&mut self, appid: u32, steamids: Vec<SteamID>) -> Result<(), SteamError> {
242        if !self.is_logged_in() {
243            return Err(SteamError::NotLoggedOn);
244        }
245
246        // Remove from active tickets
247        let steamids_u64: Vec<u64> = steamids.iter().map(|s| s.steam_id64()).collect();
248        self.active_tickets.retain(|t| !(t.appid == appid && steamids_u64.contains(&t.steam_id)));
249
250        self.send_auth_list(Some(appid)).await
251    }
252
253    /// Send the ClientAuthList message with current active tickets.
254    async fn send_auth_list(&mut self, force_appid: Option<u32>) -> Result<(), SteamError> {
255        let mut app_ids: Vec<u32> = self.active_tickets.iter().map(|t| t.appid).collect();
256        app_ids.sort();
257        app_ids.dedup();
258
259        if let Some(aid) = force_appid {
260            if !app_ids.contains(&aid) {
261                app_ids.push(aid);
262            }
263        }
264
265        let mut msg = steam_protos::CMsgClientAuthList {
266            tokens_left: Some(self.gc_tokens.len() as u32),
267            last_request_seq: Some(self.auth.read().auth_seq_me),
268            last_request_seq_from_server: Some(self.auth.read().auth_seq_them),
269            app_ids: app_ids.clone(),
270            message_sequence: Some(self.auth.read().auth_seq_me + 1),
271            ..Default::default()
272        };
273
274        for ticket in &self.active_tickets {
275            let ticket_msg = steam_protos::CMsgAuthTicket {
276                gameid: Some(ticket.appid as u64),
277                ticket: Some(ticket.ticket.clone()),
278                h_steam_pipe: Some(self.h_steam_pipe),
279                ticket_crc: Some(ticket.ticket_crc),
280                steamid: Some(ticket.steam_id),
281                ..Default::default()
282            };
283            msg.tickets.push(ticket_msg);
284        }
285
286        self.auth.write().auth_seq_me += 1;
287
288        self.send_message(steam_enums::EMsg::ClientAuthList, &msg).await
289    }
290}