429 lines
14 KiB
Rust
429 lines
14 KiB
Rust
use crate::api_responses::AllowedConfirmation;
|
|
use crate::protobufs::custom::CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData;
|
|
use crate::protobufs::enums::ESessionPersistence;
|
|
use crate::protobufs::steammessages_auth_steamclient::{
|
|
CAuthentication_AllowedConfirmation, CAuthentication_DeviceDetails,
|
|
CAuthentication_PollAuthSessionStatus_Request, CAuthentication_PollAuthSessionStatus_Response,
|
|
EAuthSessionGuardType,
|
|
};
|
|
use crate::protobufs::steammessages_auth_steamclient::{
|
|
CAuthentication_BeginAuthSessionViaCredentials_Response,
|
|
CAuthentication_BeginAuthSessionViaQR_Request, CAuthentication_BeginAuthSessionViaQR_Response,
|
|
CAuthentication_GetPasswordRSAPublicKey_Response,
|
|
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
|
|
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response, EAuthTokenPlatformType,
|
|
};
|
|
use crate::steamapi::authentication::AuthenticationClient;
|
|
use crate::steamapi::EResult;
|
|
use crate::token::Tokens;
|
|
use crate::transport::Transport;
|
|
use log::*;
|
|
use rsa::{PublicKey, RsaPublicKey};
|
|
use std::time::Duration;
|
|
|
|
#[derive(Debug)]
|
|
pub enum LoginError {
|
|
BadCredentials,
|
|
TooManyAttempts,
|
|
UnknownEResult(EResult),
|
|
AuthAlreadyStarted,
|
|
NetworkFailure(reqwest::Error),
|
|
OtherFailure(anyhow::Error),
|
|
}
|
|
|
|
impl std::fmt::Display for LoginError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
write!(f, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for LoginError {}
|
|
|
|
impl From<reqwest::Error> for LoginError {
|
|
fn from(err: reqwest::Error) -> Self {
|
|
LoginError::NetworkFailure(err)
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for LoginError {
|
|
fn from(err: anyhow::Error) -> Self {
|
|
LoginError::OtherFailure(err)
|
|
}
|
|
}
|
|
|
|
impl From<EResult> for LoginError {
|
|
fn from(err: EResult) -> Self {
|
|
match err {
|
|
EResult::InvalidPassword => LoginError::BadCredentials,
|
|
EResult::RateLimitExceeded => LoginError::TooManyAttempts,
|
|
err => LoginError::UnknownEResult(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct BeginQrLoginResponse {
|
|
challenge_url: String,
|
|
confirmation_methonds: Vec<AllowedConfirmation>,
|
|
}
|
|
|
|
impl BeginQrLoginResponse {
|
|
pub fn challenge_url(&self) -> &String {
|
|
&self.challenge_url
|
|
}
|
|
|
|
pub fn confirmation_methods(&self) -> &Vec<AllowedConfirmation> {
|
|
&self.confirmation_methonds
|
|
}
|
|
}
|
|
|
|
/// Handles the user login flow.
|
|
#[derive(Debug)]
|
|
pub struct UserLogin<T>
|
|
where
|
|
T: Transport,
|
|
{
|
|
client: AuthenticationClient<T>,
|
|
device_details: DeviceDetails,
|
|
|
|
started_auth: Option<StartAuth>,
|
|
}
|
|
|
|
impl<T> UserLogin<T>
|
|
where
|
|
T: Transport,
|
|
{
|
|
pub fn new(transport: T, device_details: DeviceDetails) -> Self {
|
|
Self {
|
|
client: AuthenticationClient::new(transport),
|
|
device_details,
|
|
started_auth: None,
|
|
}
|
|
}
|
|
|
|
pub fn begin_auth_via_credentials(
|
|
&mut self,
|
|
account_name: &str,
|
|
password: &str,
|
|
) -> anyhow::Result<Vec<AllowedConfirmation>, LoginError> {
|
|
if self.started_auth.is_some() {
|
|
return Err(LoginError::AuthAlreadyStarted);
|
|
}
|
|
trace!("UserLogin::begin_auth_via_credentials");
|
|
|
|
let rsa = self.client.fetch_rsa_key(account_name.to_owned())?;
|
|
|
|
let mut req = CAuthentication_BeginAuthSessionViaCredentials_Request_BinaryGuardData::new();
|
|
req.set_account_name(account_name.to_owned());
|
|
let rsa_resp = rsa.into_response_data();
|
|
req.set_encryption_timestamp(rsa_resp.timestamp());
|
|
let encrypted_password = encrypt_password(rsa_resp, password);
|
|
req.set_encrypted_password(encrypted_password);
|
|
req.set_persistence(ESessionPersistence::k_ESessionPersistence_Persistent);
|
|
req.device_details = self.device_details.clone().into_message_field();
|
|
req.set_language(0); // english, probably
|
|
req.set_qos_level(2); // value from observed traffic
|
|
|
|
let resp = self.client.begin_auth_session_via_credentials(req)?;
|
|
|
|
if resp.result != EResult::OK {
|
|
return Err(resp.result.into());
|
|
}
|
|
|
|
debug!("auth session started");
|
|
self.started_auth = Some(resp.into_response_data().into());
|
|
|
|
Ok(self
|
|
.started_auth
|
|
.as_ref()
|
|
.unwrap()
|
|
.allowed_confirmations()
|
|
.iter()
|
|
.map(|c| c.clone().into())
|
|
.collect())
|
|
}
|
|
|
|
pub fn begin_auth_via_qr(&mut self) -> anyhow::Result<BeginQrLoginResponse, LoginError> {
|
|
if self.started_auth.is_some() {
|
|
return Err(LoginError::AuthAlreadyStarted);
|
|
}
|
|
|
|
let mut req = CAuthentication_BeginAuthSessionViaQR_Request::new();
|
|
req.set_platform_type(self.device_details.platform_type);
|
|
req.set_device_friendly_name(self.device_details.friendly_name.clone());
|
|
let resp = self.client.begin_auth_session_via_qr(req)?;
|
|
|
|
if resp.result != EResult::OK {
|
|
return Err(resp.result.into());
|
|
}
|
|
|
|
let data = resp.response_data();
|
|
let return_resp = BeginQrLoginResponse {
|
|
challenge_url: data.challenge_url().into(),
|
|
confirmation_methonds: data
|
|
.allowed_confirmations
|
|
.iter()
|
|
.map(|c| c.clone().into())
|
|
.collect(),
|
|
};
|
|
|
|
debug!("auth session started");
|
|
self.started_auth = Some(resp.into_response_data().into());
|
|
|
|
Ok(return_resp)
|
|
}
|
|
|
|
fn poll_until_info(
|
|
&mut self,
|
|
) -> anyhow::Result<CAuthentication_PollAuthSessionStatus_Response> {
|
|
let Some(started_auth) = self.started_auth.as_ref() else {
|
|
return Err(anyhow::anyhow!("no auth session started"));
|
|
};
|
|
|
|
loop {
|
|
let mut req = CAuthentication_PollAuthSessionStatus_Request::new();
|
|
req.set_client_id(started_auth.client_id());
|
|
req.set_request_id(started_auth.request_id().to_vec());
|
|
|
|
let resp = self.client.poll_auth_session(req)?;
|
|
if resp.result != EResult::OK {
|
|
// EResult::FileNotFound is returned when the server couldn't find the auth session
|
|
return Err(anyhow::anyhow!("poll failed: {:?}", resp.result));
|
|
}
|
|
|
|
let data = resp.response_data();
|
|
let has_data = data.has_access_token()
|
|
|| data.has_account_name()
|
|
|| data.has_agreement_session_url()
|
|
|| data.has_had_remote_interaction()
|
|
|| data.has_new_challenge_url()
|
|
|| data.has_new_client_id()
|
|
|| data.has_new_guard_data()
|
|
|| data.has_refresh_token();
|
|
|
|
if has_data {
|
|
return Ok(resp.into_response_data());
|
|
}
|
|
|
|
std::thread::sleep(Duration::from_secs_f32(started_auth.interval()));
|
|
}
|
|
}
|
|
|
|
pub fn poll_until_tokens(&mut self) -> anyhow::Result<Tokens> {
|
|
loop {
|
|
let mut next_poll = self.poll_until_info()?;
|
|
|
|
if next_poll.has_access_token() {
|
|
return Ok(Tokens::new(
|
|
next_poll.take_access_token(),
|
|
next_poll.take_refresh_token(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Submit a 2fa code generated from a device, or received in an email.
|
|
pub fn submit_steam_guard_code(
|
|
&mut self,
|
|
guard_type: EAuthSessionGuardType,
|
|
code: String,
|
|
) -> anyhow::Result<
|
|
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response,
|
|
UpdateAuthSessionError,
|
|
> {
|
|
let Some(started_auth) = self.started_auth.as_ref() else {
|
|
return Err(UpdateAuthSessionError::SessionNotStarted);
|
|
};
|
|
|
|
if guard_type != EAuthSessionGuardType::k_EAuthSessionGuardType_DeviceCode
|
|
&& guard_type != EAuthSessionGuardType::k_EAuthSessionGuardType_EmailCode
|
|
{
|
|
return Err(UpdateAuthSessionError::InvalidGuardType);
|
|
}
|
|
|
|
let mut req = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request::new();
|
|
req.set_client_id(started_auth.client_id());
|
|
req.set_code_type(guard_type);
|
|
req.set_code(code);
|
|
match started_auth {
|
|
StartAuth::BeginAuthSessionViaCredentials(ref resp) => {
|
|
req.set_steamid(resp.steamid());
|
|
}
|
|
StartAuth::BeginAuthSessionViaQR(_) => {
|
|
return Err(anyhow::anyhow!("qr auth not supported").into());
|
|
}
|
|
}
|
|
|
|
let resp = self.client.update_session_with_steam_guard_code(req)?;
|
|
|
|
if resp.result != EResult::OK {
|
|
return Err(resp.result.into());
|
|
}
|
|
|
|
Ok(resp.into_response_data())
|
|
}
|
|
}
|
|
|
|
fn encrypt_password(
|
|
rsa_resp: CAuthentication_GetPasswordRSAPublicKey_Response,
|
|
password: impl AsRef<[u8]>,
|
|
) -> String {
|
|
let rsa_exponent = rsa::BigUint::parse_bytes(rsa_resp.publickey_exp().as_bytes(), 16).unwrap();
|
|
let rsa_modulus = rsa::BigUint::parse_bytes(rsa_resp.publickey_mod().as_bytes(), 16).unwrap();
|
|
let public_key = RsaPublicKey::new(rsa_modulus, rsa_exponent).unwrap();
|
|
#[cfg(test)]
|
|
let mut rng = rand::rngs::mock::StepRng::new(2, 1);
|
|
#[cfg(not(test))]
|
|
let mut rng = rand::rngs::OsRng;
|
|
let padding = rsa::PaddingScheme::new_pkcs1v15_encrypt();
|
|
base64::encode(
|
|
public_key
|
|
.encrypt(&mut rng, padding, password.as_ref())
|
|
.unwrap(),
|
|
)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum StartAuth {
|
|
BeginAuthSessionViaCredentials(CAuthentication_BeginAuthSessionViaCredentials_Response),
|
|
BeginAuthSessionViaQR(CAuthentication_BeginAuthSessionViaQR_Response),
|
|
}
|
|
|
|
impl StartAuth {
|
|
pub(crate) fn client_id(&self) -> u64 {
|
|
match self {
|
|
StartAuth::BeginAuthSessionViaCredentials(resp) => resp.client_id(),
|
|
StartAuth::BeginAuthSessionViaQR(resp) => resp.client_id(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn request_id(&self) -> &[u8] {
|
|
match self {
|
|
StartAuth::BeginAuthSessionViaCredentials(resp) => resp.request_id(),
|
|
StartAuth::BeginAuthSessionViaQR(resp) => resp.request_id(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn interval(&self) -> f32 {
|
|
match self {
|
|
StartAuth::BeginAuthSessionViaCredentials(resp) => resp.interval(),
|
|
StartAuth::BeginAuthSessionViaQR(resp) => resp.interval(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn allowed_confirmations(&self) -> &Vec<CAuthentication_AllowedConfirmation> {
|
|
match self {
|
|
StartAuth::BeginAuthSessionViaCredentials(resp) => &resp.allowed_confirmations,
|
|
StartAuth::BeginAuthSessionViaQR(resp) => &resp.allowed_confirmations,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CAuthentication_BeginAuthSessionViaCredentials_Response> for StartAuth {
|
|
fn from(resp: CAuthentication_BeginAuthSessionViaCredentials_Response) -> Self {
|
|
Self::BeginAuthSessionViaCredentials(resp)
|
|
}
|
|
}
|
|
|
|
impl From<CAuthentication_BeginAuthSessionViaQR_Response> for StartAuth {
|
|
fn from(resp: CAuthentication_BeginAuthSessionViaQR_Response) -> Self {
|
|
Self::BeginAuthSessionViaQR(resp)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DeviceDetails {
|
|
/// The name to display for this device. You should make this unique, identifiable, and human readable. Used when managing account sessions.
|
|
pub friendly_name: String,
|
|
pub platform_type: EAuthTokenPlatformType,
|
|
/// Corresponds to the EOSType enum.
|
|
pub os_type: i32,
|
|
/// Corresponds to the EGamingDeviceType enum.
|
|
pub gaming_device_type: u32,
|
|
}
|
|
|
|
impl DeviceDetails {
|
|
fn into_message_field(self) -> protobuf::MessageField<CAuthentication_DeviceDetails> {
|
|
Some(self.into()).into()
|
|
}
|
|
}
|
|
|
|
impl From<DeviceDetails> for CAuthentication_DeviceDetails {
|
|
fn from(details: DeviceDetails) -> Self {
|
|
let mut inner = CAuthentication_DeviceDetails::new();
|
|
inner.set_device_friendly_name(details.friendly_name);
|
|
inner.set_platform_type(details.platform_type);
|
|
inner.set_os_type(details.os_type);
|
|
inner.set_gaming_device_type(details.gaming_device_type);
|
|
inner
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum UpdateAuthSessionError {
|
|
SessionNotStarted,
|
|
InvalidGuardType,
|
|
TooManyAttempts,
|
|
SessionExpired,
|
|
IncorrectSteamGuardCode,
|
|
UnknownEResult(EResult),
|
|
NetworkFailure(reqwest::Error),
|
|
OtherFailure(anyhow::Error),
|
|
}
|
|
|
|
impl std::fmt::Display for UpdateAuthSessionError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
write!(f, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for UpdateAuthSessionError {}
|
|
|
|
impl From<EResult> for UpdateAuthSessionError {
|
|
fn from(err: EResult) -> Self {
|
|
match err {
|
|
EResult::RateLimitExceeded => UpdateAuthSessionError::TooManyAttempts,
|
|
EResult::Expired => UpdateAuthSessionError::SessionExpired,
|
|
EResult::TwoFactorCodeMismatch => UpdateAuthSessionError::IncorrectSteamGuardCode,
|
|
_ => UpdateAuthSessionError::UnknownEResult(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<reqwest::Error> for UpdateAuthSessionError {
|
|
fn from(err: reqwest::Error) -> Self {
|
|
UpdateAuthSessionError::NetworkFailure(err)
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for UpdateAuthSessionError {
|
|
fn from(err: anyhow::Error) -> Self {
|
|
UpdateAuthSessionError::OtherFailure(err)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_encrypt_password() {
|
|
let mut rsa_resp = CAuthentication_GetPasswordRSAPublicKey_Response::new();
|
|
rsa_resp.set_publickey_exp(String::from("010001"));
|
|
rsa_resp.set_publickey_mod(String::from("98f9088c1250b17fe19d2b2422d54a1eef0036875301731f11bd17900e215318eb6de1546727c0b7b61b86cefccdcb2f8108c813154d9a7d55631965eece810d4ab9d8a59c486bda778651b876176070598a93c2325c275cb9c17bdbcacf8edc9c18c0c5d59bc35703505ef8a09ed4c62b9f92a3fac5740ce25e490ab0e26d872140e4103d912d1e3958f844264211277ee08d2b4dd3ac58b030b25342bd5c949ae7794e46a8eab26d5a8deca683bfd381da6c305b19868b8c7cd321ce72c693310a6ebf2ecd43642518f825894602f6c239cf193cb4346ce64beac31e20ef88f934f2f776597734bb9eae1ebdf4a453973b6df9d5e90777bffe5db83dd1757b"));
|
|
rsa_resp.set_timestamp(1);
|
|
let result = encrypt_password(rsa_resp, "kelwleofpsm3n4ofc");
|
|
assert_eq!(result.len(), 344);
|
|
assert_eq!(result, "RUo/3IfbkVcJi1q1S5QlpKn1mEn3gNJoc/Z4VwxRV9DImV6veq/YISEuSrHB3885U5MYFLn1g94Y+cWRL6HGXoV+gOaVZe43m7O92RwiVz6OZQXMfAv3UC/jcqn/xkitnj+tNtmx55gCxmGbO2KbqQ0TQqAyqCOOw565B+Cwr2OOorpMZAViv9sKA/G3Q6yzscU6rhua179c8QjC1Hk3idUoSzpWfT4sHNBW/EREXZ3Dkjwu17xzpfwIUpnBVIlR8Vj3coHgUCpTsKVRA3T814v9BYPlvLYwmw5DW3ddx+2SyTY0P5uuog36TN2PqYS7ioF5eDe16gyfRR4Nzn/7wA==");
|
|
}
|
|
|
|
#[test]
|
|
fn test_encrypt_password_2() {
|
|
let mut rsa_resp = CAuthentication_GetPasswordRSAPublicKey_Response::new();
|
|
rsa_resp.set_publickey_exp(String::from("010001"));
|
|
rsa_resp.set_publickey_mod(String::from("ca6a8dc290279b25c38a282b9a7b01306c5978bd7a2f60dcfd52134ac58faf121568ebd85ca6a2128413b76ec70fb3150b3181bbe2a1a8349b68da9c303960bdf4e34296b27bd4ea29b4d1a695168ddfc974bb6ba427206fdcdb088bf27261a52f343a51e19759fe4072b7a2047a6bc31361950d9e87d7977b31b71696572babe45ea6a7d132547984462fd5787607e0d9ff1c637e04d593e7538c880c3cdd252b75bcb703a7b8bb01cd8898b04980f40b76235d50fc1544c39ccbe763892322fc6d0a5acaf8be09efbc20fcfebcd3b02a1eb95d9d0c338e96674c17edbb0257cd43d04974423f1f995a28b9e159322d9db2708826804c0eccafffc94dd2a3d5"));
|
|
rsa_resp.set_timestamp(104444850000);
|
|
let result = encrypt_password(rsa_resp, "foo");
|
|
assert_eq!(result, "jmlMXmhbweWn+wJnnf96W3Lsh0dRmzrBfMxREUuEW11rRYcfXWupBIT3eK1fmQHMZmyJeMhZiRpgIaZ7DafojQT6djJr+RKeREJs0ys9hKwxD5FGlqsTLXXEeuyopyd2smHBbmmF47voe59KEoiZZapP+eYnpJy3O2k7e1P9BH9LsKIN/nWF1ogM2jjJ328AejUpM64tPl/kInFJ1CHrLiAAKDPk42fLAAKs97xIi0JkosG6yp+8HhFqQxxZ8/bNI1IVkQC1Hdc2AN0QlNKxbDXquAn6ARgw/4b5DwUpnOb9de+Q6iX3v1/M07Se7JV8/4tuz8Thy2Chbxsf9E1TuQ==");
|
|
}
|
|
}
|