steam-auth-rs 0.1.2

Steam authentication and session management
Documentation
//! Authentication client for Steam's auth API.

use std::collections::HashMap;

use prost::Message;
use steam_protos::{
    CAuthenticationAccessTokenGenerateForAppRequest, CAuthenticationAccessTokenGenerateForAppResponse, CAuthenticationBeginAuthSessionViaCredentialsRequest, CAuthenticationBeginAuthSessionViaCredentialsResponse, CAuthenticationBeginAuthSessionViaQRRequest, CAuthenticationBeginAuthSessionViaQRResponse, CAuthenticationDeviceDetails, CAuthenticationGetAuthSessionInfoRequest, CAuthenticationGetAuthSessionInfoResponse, CAuthenticationGetPasswordRSAPublicKeyRequest, CAuthenticationGetPasswordRSAPublicKeyResponse, CAuthenticationPollAuthSessionStatusRequest,
    CAuthenticationPollAuthSessionStatusResponse, CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest, CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest, EAuthSessionGuardType, EAuthTokenPlatformType, ESessionPersistence, ETokenRenewalType,
};

use crate::{
    crypto::rsa_encrypt_password,
    error::SessionError,
    helpers::{default_user_agent, get_spoofed_hostname},
    transport::{ApiRequest, Transport},
    types::{AllowedConfirmation, DeviceDetails, PlatformData, StartAuthSessionResponse},
};

/// Result of RSA key fetch.
#[derive(Debug, Clone)]
pub struct RsaKeyResponse {
    pub public_key_mod: String,
    pub public_key_exp: String,
    pub timestamp: u64,
}

/// Result of password encryption.
#[derive(Debug, Clone)]
pub struct EncryptedPassword {
    pub encrypted_password: String,
    pub key_timestamp: u64,
}

/// Poll response from auth status.
#[derive(Debug, Clone)]
pub struct PollLoginStatusResponse {
    pub new_client_id: Option<u64>,
    pub new_challenge_url: Option<String>,
    pub refresh_token: Option<String>,
    pub access_token: Option<String>,
    pub had_remote_interaction: bool,
    pub account_name: Option<String>,
    pub new_steam_guard_machine_auth: Option<String>,
}

/// Authentication client for communicating with Steam's auth service.
pub struct AuthenticationClient {
    transport: Transport,
    platform_type: EAuthTokenPlatformType,
    web_user_agent: String,
    machine_id: Option<Vec<u8>>,
    client_friendly_name: Option<String>,
}

impl AuthenticationClient {
    /// Create a new authentication client.
    pub fn new(transport: Transport, platform_type: EAuthTokenPlatformType, machine_id: Option<Vec<u8>>, client_friendly_name: Option<String>) -> Self {
        Self { transport, platform_type, web_user_agent: default_user_agent(), machine_id, client_friendly_name }
    }

    /// Get RSA public key for password encryption.
    pub async fn get_rsa_key(&self, account_name: &str) -> Result<RsaKeyResponse, SessionError> {
        let request = CAuthenticationGetPasswordRSAPublicKeyRequest { account_name: Some(account_name.to_string()) };

        let response: CAuthenticationGetPasswordRSAPublicKeyResponse = self.send_request("Authentication", "GetPasswordRSAPublicKey", 1, &request, None).await?;

        Ok(RsaKeyResponse {
            public_key_mod: response.publickey_mod.unwrap_or_default(),
            public_key_exp: response.publickey_exp.unwrap_or_default(),
            timestamp: response.timestamp.unwrap_or(0),
        })
    }

    /// Encrypt a password using RSA.
    ///
    /// This method fetches Steam's RSA public key and uses it to encrypt the
    /// password. The actual encryption is performed by the pure function
    /// [`rsa_encrypt_password`].
    pub async fn encrypt_password(&self, account_name: &str, password: &str) -> Result<EncryptedPassword, SessionError> {
        let rsa_info = self.get_rsa_key(account_name).await?;

        // Use the pure encryption function from crypto module
        let encrypted_password = rsa_encrypt_password(password, &rsa_info.public_key_mod, &rsa_info.public_key_exp)?;

        Ok(EncryptedPassword { encrypted_password, key_timestamp: rsa_info.timestamp })
    }

    /// Start an auth session with credentials.
    pub async fn start_session_with_credentials(&self, account_name: &str, encrypted_password: &str, key_timestamp: u64, persistence: ESessionPersistence, steam_guard_machine_token: Option<&str>) -> Result<StartAuthSessionResponse, SessionError> {
        let platform_data = self.get_platform_data();

        let device_details = CAuthenticationDeviceDetails {
            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
            platform_type: Some(self.platform_type as i32),
            os_type: platform_data.device_details.os_type,
            gaming_device_type: platform_data.device_details.gaming_device_type,
            client_count: None,
            machine_id: platform_data.device_details.machine_id.clone(),
            app_type: None,
        };

        let mut request = CAuthenticationBeginAuthSessionViaCredentialsRequest {
            account_name: Some(account_name.to_string()),
            encrypted_password: Some(encrypted_password.to_string()),
            encryption_timestamp: Some(key_timestamp),
            remember_login: Some(persistence == ESessionPersistence::KESessionPersistencePersistent),
            persistence: Some(persistence as i32),
            website_id: Some(platform_data.website_id.clone()),
            device_details: Some(device_details),
            device_friendly_name: None,
            platform_type: Some(self.platform_type as i32),
            guard_data: None,
            language: None,
            qos_level: Some(2),
        };

        // Add machine token if provided
        if let Some(token) = steam_guard_machine_token {
            request.guard_data = Some(token.to_string());
        }

        let response: CAuthenticationBeginAuthSessionViaCredentialsResponse = self.send_request("Authentication", "BeginAuthSessionViaCredentials", 1, &request, None).await?;

        Ok(StartAuthSessionResponse {
            client_id: response.client_id.unwrap_or(0),
            request_id: response.request_id.unwrap_or_default(),
            poll_interval: response.interval.unwrap_or(5.0),
            allowed_confirmations: response
                .allowed_confirmations
                .into_iter()
                .map(|c| AllowedConfirmation {
                    confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
                    message: c.associated_message,
                })
                .collect(),
            steam_id: response.steamid,
            weak_token: response.weak_token,
            challenge_url: None,
            version: None,
        })
    }

    /// Start a QR code auth session.
    pub async fn start_session_with_qr(&self) -> Result<StartAuthSessionResponse, SessionError> {
        let platform_data = self.get_platform_data();

        let device_details = CAuthenticationDeviceDetails {
            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
            platform_type: Some(self.platform_type as i32),
            os_type: platform_data.device_details.os_type,
            gaming_device_type: platform_data.device_details.gaming_device_type,
            client_count: None,
            machine_id: platform_data.device_details.machine_id.clone(),
            app_type: None,
        };

        let request = CAuthenticationBeginAuthSessionViaQRRequest {
            device_friendly_name: Some(platform_data.device_details.device_friendly_name.clone()),
            platform_type: Some(self.platform_type as i32),
            device_details: Some(device_details),
            website_id: Some("Unknown".to_string()),
        };

        let response: CAuthenticationBeginAuthSessionViaQRResponse = self.send_request("Authentication", "BeginAuthSessionViaQR", 1, &request, None).await?;

        Ok(StartAuthSessionResponse {
            client_id: response.client_id.unwrap_or(0),
            request_id: response.request_id.unwrap_or_default(),
            poll_interval: response.interval.unwrap_or(5.0),
            allowed_confirmations: response
                .allowed_confirmations
                .into_iter()
                .map(|c| AllowedConfirmation {
                    confirmation_type: EAuthSessionGuardType::try_from(c.confirmation_type.unwrap_or(0)).unwrap_or(EAuthSessionGuardType::KEAuthSessionGuardTypeUnknown),
                    message: c.associated_message,
                })
                .collect(),
            steam_id: None,
            weak_token: None,
            challenge_url: response.challenge_url,
            version: response.version,
        })
    }

    /// Submit a Steam Guard code.
    pub async fn submit_steam_guard_code(&self, client_id: u64, steam_id: u64, code: &str, code_type: EAuthSessionGuardType) -> Result<(), SessionError> {
        let request = CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest {
            client_id: Some(client_id),
            steamid: Some(steam_id),
            code: Some(code.to_string()),
            code_type: Some(code_type as i32),
        };

        let _: () = self.send_request_no_response("Authentication", "UpdateAuthSessionWithSteamGuardCode", 1, &request, None).await?;

        Ok(())
    }

    /// Poll the auth session status.
    pub async fn poll_login_status(&self, client_id: u64, request_id: &[u8]) -> Result<PollLoginStatusResponse, SessionError> {
        let request = CAuthenticationPollAuthSessionStatusRequest { client_id: Some(client_id), request_id: Some(request_id.to_vec()), token_to_revoke: None };

        let response: CAuthenticationPollAuthSessionStatusResponse = self.send_request("Authentication", "PollAuthSessionStatus", 1, &request, None).await?;

        Ok(PollLoginStatusResponse {
            new_client_id: response.new_client_id,
            new_challenge_url: response.new_challenge_url,
            refresh_token: response.refresh_token,
            access_token: response.access_token,
            had_remote_interaction: response.had_remote_interaction.unwrap_or(false),
            account_name: response.account_name,
            new_steam_guard_machine_auth: response.new_guard_data,
        })
    }

    /// Generate an access token from a refresh token.
    pub async fn generate_access_token(&self, refresh_token: &str, steam_id: u64, renew: bool) -> Result<(String, Option<String>), SessionError> {
        let request = CAuthenticationAccessTokenGenerateForAppRequest {
            refresh_token: Some(refresh_token.to_string()),
            steamid: Some(steam_id),
            renewal_type: Some(if renew { ETokenRenewalType::KETokenRenewalTypeAllow as i32 } else { ETokenRenewalType::KETokenRenewalTypeNone as i32 }),
        };

        let response: CAuthenticationAccessTokenGenerateForAppResponse = self.send_request("Authentication", "GenerateAccessTokenForApp", 1, &request, None).await?;

        Ok((response.access_token.unwrap_or_default(), response.refresh_token))
    }

    /// Get information about an auth session (for QR approval).
    pub async fn get_auth_session_info(&self, access_token: &str, client_id: u64) -> Result<CAuthenticationGetAuthSessionInfoResponse, SessionError> {
        let request = CAuthenticationGetAuthSessionInfoRequest { client_id: Some(client_id) };

        self.send_request("Authentication", "GetAuthSessionInfo", 1, &request, Some(access_token)).await
    }

    /// Submit mobile confirmation to approve/deny a login.
    #[allow(clippy::too_many_arguments)]
    pub async fn submit_mobile_confirmation(&self, access_token: &str, version: i32, client_id: u64, steam_id: u64, signature: &[u8], confirm: bool, persistence: ESessionPersistence) -> Result<(), SessionError> {
        let request = CAuthenticationUpdateAuthSessionWithMobileConfirmationRequest {
            version: Some(version),
            client_id: Some(client_id),
            steamid: Some(steam_id),
            signature: Some(signature.to_vec()),
            confirm: Some(confirm),
            persistence: Some(persistence as i32),
        };

        self.send_request_no_response("Authentication", "UpdateAuthSessionWithMobileConfirmation", 1, &request, Some(access_token)).await
    }

    /// Send a protobuf request and decode the response.
    async fn send_request<Req: Message, Resp: Message + Default>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<Resp, SessionError> {
        let platform_data = self.get_platform_data();
        let request_data = request.encode_to_vec();

        let api_request = ApiRequest {
            api_interface: interface.to_string(),
            api_method: method.to_string(),
            api_version: version,
            access_token: access_token.map(String::from),
            request_data: Some(request_data),
            headers: platform_data.headers,
        };

        let response = self.transport.send_request(api_request).await?;

        // Check for errors
        if let Some(result) = response.result {
            if result != 1 {
                // EResult::OK = 1
                return Err(SessionError::from_eresult(result, response.error_message));
            }
        }

        // Decode response
        let response_data = response.response_data.unwrap_or_default();
        Ok(Resp::decode(response_data.as_slice())?)
    }

    /// Send a protobuf request with no response body.
    async fn send_request_no_response<Req: Message>(&self, interface: &str, method: &str, version: u32, request: &Req, access_token: Option<&str>) -> Result<(), SessionError> {
        let platform_data = self.get_platform_data();
        let request_data = request.encode_to_vec();

        let api_request = ApiRequest {
            api_interface: interface.to_string(),
            api_method: method.to_string(),
            api_version: version,
            access_token: access_token.map(String::from),
            request_data: Some(request_data),
            headers: platform_data.headers,
        };

        let response = self.transport.send_request(api_request).await?;

        // Check for errors
        if let Some(result) = response.result {
            if result != 1 {
                return Err(SessionError::from_eresult(result, response.error_message));
            }
        }

        Ok(())
    }

    /// Get platform-specific data for requests.
    fn get_platform_data(&self) -> PlatformData {
        match self.platform_type {
            EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient => {
                let machine_name = self.client_friendly_name.clone().unwrap_or_else(get_spoofed_hostname);

                PlatformData {
                    website_id: "Unknown".to_string(),
                    headers: HashMap::from([("user-agent".to_string(), "Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US; Valve Steam Client/default/1665786434; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36".to_string()), ("origin".to_string(), "https://steamloopback.host".to_string())]),
                    device_details: DeviceDetails {
                        device_friendly_name: machine_name,
                        platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient,
                        os_type: Some(20), // EOSType::Win11
                        gaming_device_type: Some(1),
                        machine_id: self.machine_id.clone(),
                    },
                }
            }
            EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser => PlatformData {
                website_id: "Community".to_string(),
                headers: HashMap::from([("user-agent".to_string(), self.web_user_agent.clone()), ("origin".to_string(), "https://steamcommunity.com".to_string()), ("referer".to_string(), "https://steamcommunity.com".to_string())]),
                device_details: DeviceDetails {
                    device_friendly_name: self.web_user_agent.clone(),
                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeWebBrowser,
                    os_type: None,
                    gaming_device_type: None,
                    machine_id: None,
                },
            },
            EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp => PlatformData {
                website_id: "Mobile".to_string(),
                headers: HashMap::from([("user-agent".to_string(), "okhttp/4.9.2".to_string()), ("cookie".to_string(), "mobileClient=android; mobileClientVersion=777777 3.10.3".to_string())]),
                device_details: DeviceDetails {
                    device_friendly_name: "Galaxy S25".to_string(),
                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeMobileApp,
                    os_type: Some(-500), // EOSType::AndroidUnknown
                    gaming_device_type: Some(528),
                    machine_id: None,
                },
            },
            _ => PlatformData {
                website_id: "Community".to_string(),
                headers: HashMap::new(),
                device_details: DeviceDetails {
                    device_friendly_name: "Unknown".to_string(),
                    platform_type: EAuthTokenPlatformType::KEAuthTokenPlatformTypeUnknown,
                    os_type: None,
                    gaming_device_type: None,
                    machine_id: None,
                },
            },
        }
    }
}