steam_vent/auth/
mod.rs

1mod confirmation;
2mod guard_data;
3
4use crate::connection::raw::RawConnection;
5use crate::connection::unauthenticated::service_method_un_authenticated;
6use crate::message::NetMessage;
7use crate::message::{MalformedBody, ServiceMethodMessage};
8use crate::net::NetworkError;
9use crate::proto::enums::ESessionPersistence;
10use crate::proto::steammessages_auth_steamclient::CAuthentication_GetPasswordRSAPublicKey_Request;
11use crate::proto::steammessages_auth_steamclient::{
12    CAuthentication_AllowedConfirmation, CAuthentication_BeginAuthSessionViaCredentials_Request,
13    CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails,
14    CAuthentication_PollAuthSessionStatus_Request, CAuthentication_PollAuthSessionStatus_Response,
15    CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request, EAuthSessionGuardType,
16    EAuthTokenPlatformType,
17};
18use crate::session::{ConnectionError, LoginError};
19use base64::prelude::BASE64_STANDARD;
20use base64::Engine;
21pub use confirmation::*;
22pub use guard_data::*;
23use num_bigint_dig::BigUint;
24use num_traits::Num;
25use protobuf::{EnumOrUnknown, MessageField};
26use rsa::RsaPublicKey;
27use std::time::Duration;
28use steam_vent_crypto::encrypt_with_key_pkcs1;
29use thiserror::Error;
30use tokio::time::sleep;
31use tracing::{debug, info, instrument};
32
33pub(crate) async fn begin_password_auth(
34    connection: &mut RawConnection,
35    account: &str,
36    password: &str,
37    guard_data: Option<&str>,
38) -> Result<StartedAuth, ConnectionError> {
39    let (pub_key, timestamp) = get_password_rsa(connection, account.into()).await?;
40    let encrypted_password =
41        encrypt_with_key_pkcs1(&pub_key, password.as_bytes()).map_err(LoginError::InvalidPubKey)?;
42    let encoded_password = BASE64_STANDARD.encode(encrypted_password);
43    info!(account, "starting credentials login");
44    let req = CAuthentication_BeginAuthSessionViaCredentials_Request {
45        account_name: Some(account.into()),
46        encrypted_password: Some(encoded_password),
47        encryption_timestamp: Some(timestamp),
48        persistence: Some(EnumOrUnknown::new(
49            ESessionPersistence::k_ESessionPersistence_Persistent,
50        )),
51
52        // todo: platform types
53        website_id: Some("Client".into()),
54        device_details: MessageField::some(CAuthentication_DeviceDetails {
55            device_friendly_name: Some("DESKTOP-VENT".into()),
56            platform_type: Some(EnumOrUnknown::new(
57                EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient,
58            )),
59            os_type: Some(1),
60            ..CAuthentication_DeviceDetails::default()
61        }),
62        guard_data: guard_data.map(String::from),
63        ..CAuthentication_BeginAuthSessionViaCredentials_Request::default()
64    };
65    let res = service_method_un_authenticated(connection, req).await?;
66    Ok(StartedAuth::Credentials(res))
67}
68
69pub(crate) enum StartedAuth {
70    Credentials(CAuthentication_BeginAuthSessionViaCredentials_Response),
71}
72
73#[derive(Debug, Error)]
74#[non_exhaustive]
75pub enum ConfirmationError {
76    #[error(transparent)]
77    Network(#[from] NetworkError),
78    #[error("Aborted")]
79    Aborted,
80}
81
82impl StartedAuth {
83    fn raw_confirmations(&self) -> &[CAuthentication_AllowedConfirmation] {
84        match self {
85            StartedAuth::Credentials(res) => res.allowed_confirmations.as_slice(),
86        }
87    }
88
89    pub fn allowed_confirmations(&self) -> Vec<ConfirmationMethod> {
90        self.raw_confirmations()
91            .iter()
92            .cloned()
93            .map(ConfirmationMethod::from)
94            .collect()
95    }
96
97    #[allow(dead_code)]
98    pub fn action_required(&self) -> bool {
99        self.raw_confirmations().iter().any(|method| {
100            method.confirmation_type() != EAuthSessionGuardType::k_EAuthSessionGuardType_None
101        })
102    }
103
104    fn client_id(&self) -> u64 {
105        match self {
106            StartedAuth::Credentials(res) => res.client_id(),
107        }
108    }
109
110    pub fn steam_id(&self) -> u64 {
111        match self {
112            StartedAuth::Credentials(res) => res.steamid(),
113        }
114    }
115
116    fn request_id(&self) -> Vec<u8> {
117        match self {
118            StartedAuth::Credentials(res) => res.request_id().into(),
119        }
120    }
121
122    fn interval(&self) -> f32 {
123        match self {
124            StartedAuth::Credentials(res) => res.interval(),
125        }
126    }
127
128    pub fn poll(&self) -> PendingAuth {
129        PendingAuth {
130            interval: self.interval(),
131            client_id: self.client_id(),
132            request_id: self.request_id(),
133        }
134    }
135
136    pub async fn submit_confirmation(
137        &self,
138        connection: &RawConnection,
139        confirmation: ConfirmationAction,
140    ) -> Result<(), ConfirmationError> {
141        match confirmation {
142            ConfirmationAction::GuardToken(token, ty) => {
143                let req = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request {
144                    client_id: Some(self.client_id()),
145                    steamid: Some(self.steam_id()),
146                    code: Some(token.0),
147                    code_type: Some(EnumOrUnknown::new(ty.into())),
148                    ..CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request::default()
149                };
150                let _ = service_method_un_authenticated(connection, req).await?;
151            }
152            ConfirmationAction::None => {}
153            ConfirmationAction::Abort => return Err(ConfirmationError::Aborted),
154        };
155        Ok(())
156    }
157}
158
159/// The token to send to steam to confirm the login
160#[derive(Debug)]
161pub struct SteamGuardToken(String);
162
163pub(crate) struct PendingAuth {
164    client_id: u64,
165    request_id: Vec<u8>,
166    interval: f32,
167}
168
169impl PendingAuth {
170    pub(crate) async fn wait_for_tokens(
171        self,
172        connection: &RawConnection,
173    ) -> Result<Tokens, NetworkError> {
174        loop {
175            let mut response = poll_until_info(
176                connection,
177                self.client_id,
178                &self.request_id,
179                Duration::from_secs_f32(self.interval),
180            )
181            .await?;
182            if response.has_access_token() {
183                return Ok(Tokens {
184                    access_token: Token(response.take_access_token()),
185                    refresh_token: Token(response.take_refresh_token()),
186                    new_guard_data: response.new_guard_data,
187                });
188            }
189        }
190    }
191}
192
193#[derive(Debug, Clone)]
194pub(crate) struct Token(String);
195
196impl AsRef<str> for Token {
197    fn as_ref(&self) -> &str {
198        self.0.as_ref()
199    }
200}
201
202#[derive(Debug, Clone)]
203pub(crate) struct Tokens {
204    #[allow(dead_code)]
205    pub access_token: Token,
206    pub refresh_token: Token,
207    pub new_guard_data: Option<String>,
208}
209
210async fn poll_until_info(
211    connection: &RawConnection,
212    client_id: u64,
213    request_id: &[u8],
214    interval: Duration,
215) -> Result<CAuthentication_PollAuthSessionStatus_Response, NetworkError> {
216    loop {
217        let req = CAuthentication_PollAuthSessionStatus_Request {
218            client_id: Some(client_id),
219            request_id: Some(request_id.into()),
220            ..CAuthentication_PollAuthSessionStatus_Request::default()
221        };
222
223        let resp = service_method_un_authenticated(connection, req).await?;
224        let has_data = resp.has_access_token()
225            || resp.has_account_name()
226            || resp.has_agreement_session_url()
227            || resp.has_had_remote_interaction()
228            || resp.has_new_challenge_url()
229            || resp.has_new_client_id()
230            || resp.has_new_guard_data()
231            || resp.has_refresh_token();
232
233        if has_data {
234            return Ok(resp);
235        }
236
237        sleep(interval).await;
238    }
239}
240
241#[instrument(skip(connection))]
242async fn get_password_rsa(
243    connection: &mut RawConnection,
244    account: String,
245) -> Result<(RsaPublicKey, u64), NetworkError> {
246    debug!("getting password rsa");
247    let req = CAuthentication_GetPasswordRSAPublicKey_Request {
248        account_name: Some(account),
249        ..CAuthentication_GetPasswordRSAPublicKey_Request::default()
250    };
251    let response = service_method_un_authenticated(connection, req).await?;
252
253    let key_mod =
254        BigUint::from_str_radix(response.publickey_mod.as_deref().unwrap_or_default(), 16)
255            .map_err(|e| {
256                MalformedBody::new(
257                    ServiceMethodMessage::<CAuthentication_GetPasswordRSAPublicKey_Request>::KIND,
258                    e,
259                )
260            })?;
261    let key_exp =
262        BigUint::from_str_radix(response.publickey_exp.as_deref().unwrap_or_default(), 16)
263            .map_err(|e| {
264                MalformedBody::new(
265                    ServiceMethodMessage::<CAuthentication_GetPasswordRSAPublicKey_Request>::KIND,
266                    e,
267                )
268            })?;
269    let key = RsaPublicKey::new(key_mod, key_exp).map_err(|e| {
270        MalformedBody::new(
271            ServiceMethodMessage::<CAuthentication_GetPasswordRSAPublicKey_Request>::KIND,
272            e,
273        )
274    })?;
275    Ok((key, response.timestamp.unwrap_or_default()))
276}