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}