Skip to main content

vapour_protocol/auth/
credentials.rs

1use std::time::Duration;
2
3use base64::{Engine as _, engine::general_purpose::STANDARD};
4use rsa::{BigUint, Pkcs1v15Encrypt, RsaPublicKey};
5
6use crate::{
7    client::GuardKind,
8    connection::Connection,
9    error::{Error, Result},
10    protobuf::{
11        CAuthenticationBeginAuthSessionViaCredentialsRequest,
12        CAuthenticationBeginAuthSessionViaCredentialsResponse, CAuthenticationDeviceDetails,
13        CAuthenticationGetPasswordRsaPublicKeyRequest,
14        CAuthenticationGetPasswordRsaPublicKeyResponse,
15        CAuthenticationPollAuthSessionStatusRequest, CAuthenticationPollAuthSessionStatusResponse,
16        CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest, EAuthSessionGuardType,
17        EAuthTokenPlatformType, ESessionPersistence,
18    },
19    service_method::{ServiceMethod, call},
20};
21
22const GET_PASSWORD_RSA_KEY_METHOD: &str = "Authentication.GetPasswordRSAPublicKey#1";
23const BEGIN_CREDENTIALS_METHOD: &str = "Authentication.BeginAuthSessionViaCredentials#1";
24const POLL_METHOD: &str = "Authentication.PollAuthSessionStatus#1";
25const UPDATE_GUARD_CODE_METHOD: &str = "Authentication.UpdateAuthSessionWithSteamGuardCode#1";
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CredentialSession {
29    pub client_id: u64,
30    pub request_id: Vec<u8>,
31    pub steamid: u64,
32    pub interval: Duration,
33    pub allowed_confirmations: Vec<GuardKind>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct CompletedAuth {
38    pub refresh_token: String,
39    pub account_name: String,
40}
41
42impl CredentialSession {
43    pub fn preferred_guard_kind(&self) -> Option<GuardKind> {
44        self.allowed_confirmations
45            .iter()
46            .find_map(|kind| match kind {
47                GuardKind::EmailCode => Some(GuardKind::EmailCode),
48                GuardKind::DeviceCode => Some(GuardKind::DeviceCode),
49                GuardKind::DeviceConfirmation => None,
50            })
51            .or_else(|| {
52                self.allowed_confirmations
53                    .iter()
54                    .find(|kind| **kind == GuardKind::DeviceConfirmation)
55                    .cloned()
56            })
57    }
58}
59
60pub async fn begin(
61    connection: &Connection,
62    account_name: &str,
63    password: &str,
64    device_friendly_name: &str,
65    device_details: CAuthenticationDeviceDetails,
66    website_id: &str,
67) -> Result<CredentialSession> {
68    let rsa_key = get_password_rsa_key(connection, account_name).await?;
69    let encrypted_password = encrypt_password(password, &rsa_key)?;
70
71    let response: CAuthenticationBeginAuthSessionViaCredentialsResponse = call(
72        connection,
73        &ServiceMethod::new(BEGIN_CREDENTIALS_METHOD),
74        &CAuthenticationBeginAuthSessionViaCredentialsRequest {
75            device_friendly_name: Some(device_friendly_name.to_owned()),
76            account_name: Some(account_name.to_owned()),
77            encrypted_password: Some(encrypted_password),
78            encryption_timestamp: rsa_key.timestamp,
79            remember_login: Some(true),
80            platform_type: Some(EAuthTokenPlatformType::KEAuthTokenPlatformTypeSteamClient as i32),
81            persistence: Some(ESessionPersistence::KESessionPersistencePersistent as i32),
82            website_id: Some(website_id.to_owned()),
83            device_details: Some(device_details),
84            guard_data: None,
85            language: None,
86            qos_level: Some(2),
87        },
88    )
89    .await?;
90
91    Ok(CredentialSession {
92        client_id: response.client_id.ok_or(Error::MissingField(
93            "CAuthenticationBeginAuthSessionViaCredentialsResponse.client_id",
94        ))?,
95        request_id: response.request_id.ok_or(Error::MissingField(
96            "CAuthenticationBeginAuthSessionViaCredentialsResponse.request_id",
97        ))?,
98        steamid: response.steamid.ok_or(Error::MissingField(
99            "CAuthenticationBeginAuthSessionViaCredentialsResponse.steamid",
100        ))?,
101        interval: Duration::from_secs_f32(response.interval.unwrap_or(5.0).max(1.0)),
102        allowed_confirmations: response
103            .allowed_confirmations
104            .into_iter()
105            .filter_map(|confirmation| map_guard_kind(confirmation.confirmation_type))
106            .collect(),
107    })
108}
109
110pub async fn submit_guard_code(
111    connection: &Connection,
112    session: &CredentialSession,
113    code: &str,
114    kind: GuardKind,
115) -> Result<()> {
116    let _: crate::protobuf::CAuthenticationUpdateAuthSessionWithSteamGuardCodeResponse = call(
117        connection,
118        &ServiceMethod::new(UPDATE_GUARD_CODE_METHOD),
119        &CAuthenticationUpdateAuthSessionWithSteamGuardCodeRequest {
120            client_id: Some(session.client_id),
121            steamid: Some(session.steamid),
122            code: Some(code.to_owned()),
123            code_type: Some(guard_kind_to_proto(kind) as i32),
124        },
125    )
126    .await?;
127
128    Ok(())
129}
130
131pub async fn poll(
132    connection: &Connection,
133    session: &CredentialSession,
134) -> Result<Option<CompletedAuth>> {
135    let response: CAuthenticationPollAuthSessionStatusResponse = call(
136        connection,
137        &ServiceMethod::new(POLL_METHOD),
138        &CAuthenticationPollAuthSessionStatusRequest {
139            client_id: Some(session.client_id),
140            request_id: Some(session.request_id.clone()),
141            token_to_revoke: None,
142        },
143    )
144    .await?;
145
146    match response.refresh_token {
147        Some(refresh_token) => Ok(Some(CompletedAuth {
148            refresh_token,
149            account_name: response.account_name.ok_or(Error::MissingField(
150                "CAuthenticationPollAuthSessionStatusResponse.account_name",
151            ))?,
152        })),
153        None => Ok(None),
154    }
155}
156
157struct PasswordRsaKey {
158    public_key: RsaPublicKey,
159    timestamp: Option<u64>,
160}
161
162async fn get_password_rsa_key(
163    connection: &Connection,
164    account_name: &str,
165) -> Result<PasswordRsaKey> {
166    let response: CAuthenticationGetPasswordRsaPublicKeyResponse = call(
167        connection,
168        &ServiceMethod::new(GET_PASSWORD_RSA_KEY_METHOD),
169        &CAuthenticationGetPasswordRsaPublicKeyRequest {
170            account_name: Some(account_name.to_owned()),
171        },
172    )
173    .await?;
174
175    let modulus = parse_hex_biguint(&response.publickey_mod.ok_or(Error::MissingField(
176        "CAuthenticationGetPasswordRsaPublicKeyResponse.publickey_mod",
177    ))?)?;
178    let exponent = parse_hex_biguint(&response.publickey_exp.ok_or(Error::MissingField(
179        "CAuthenticationGetPasswordRsaPublicKeyResponse.publickey_exp",
180    ))?)?;
181
182    Ok(PasswordRsaKey {
183        public_key: RsaPublicKey::new(modulus, exponent)
184            .map_err(|error| Error::Authentication(error.to_string()))?,
185        timestamp: response.timestamp,
186    })
187}
188
189fn encrypt_password(password: &str, key: &PasswordRsaKey) -> Result<String> {
190    let mut rng = rand::thread_rng();
191    let encrypted = key
192        .public_key
193        .encrypt(&mut rng, Pkcs1v15Encrypt, password.as_bytes())
194        .map_err(|error| Error::Authentication(error.to_string()))?;
195    Ok(STANDARD.encode(encrypted))
196}
197
198fn parse_hex_biguint(value: &str) -> Result<BigUint> {
199    BigUint::parse_bytes(value.as_bytes(), 16)
200        .ok_or(Error::Authentication("invalid RSA key response".to_owned()))
201}
202
203fn map_guard_kind(code: Option<i32>) -> Option<GuardKind> {
204    match code.and_then(|value| EAuthSessionGuardType::try_from(value).ok()) {
205        Some(EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode) => Some(GuardKind::EmailCode),
206        Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode) => {
207            Some(GuardKind::DeviceCode)
208        }
209        Some(EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation) => {
210            Some(GuardKind::DeviceConfirmation)
211        }
212        _ => None,
213    }
214}
215
216fn guard_kind_to_proto(kind: GuardKind) -> EAuthSessionGuardType {
217    match kind {
218        GuardKind::EmailCode => EAuthSessionGuardType::KEAuthSessionGuardTypeEmailCode,
219        GuardKind::DeviceCode => EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceCode,
220        GuardKind::DeviceConfirmation => {
221            EAuthSessionGuardType::KEAuthSessionGuardTypeDeviceConfirmation
222        }
223    }
224}