Start work on login

This commit is contained in:
Artemis Tosini 2024-09-06 00:35:24 +00:00
parent f04292e9f1
commit c3b17bafdf
Signed by: artemist
GPG key ID: ADFFE553DCBB831E
6 changed files with 199 additions and 15 deletions

View file

@ -5,10 +5,12 @@ version.workspace = true
[dependencies] [dependencies]
async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] } async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] }
base64 = "0.22.1"
bitflags = "2.6.0" bitflags = "2.6.0"
color-eyre.workspace = true color-eyre.workspace = true
flate2 = "1.0.33" flate2 = "1.0.33"
futures = "0.3.30" futures = "0.3.30"
hex = "0.4.3"
keyvalues-serde.workspace = true keyvalues-serde.workspace = true
log.workspace = true log.workspace = true
num_enum = "0.7.3" num_enum = "0.7.3"
@ -22,8 +24,6 @@ tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros", "time
vapore-proto.path = "../proto" vapore-proto.path = "../proto"
[dev-dependencies] [dev-dependencies]
base64 = "0.22.1"
dialoguer = "0.11.0" dialoguer = "0.11.0"
env_logger.workspace = true env_logger.workspace = true
hex = "0.4.3"
qrcode = "0.14.1" qrcode = "0.14.1"

View file

@ -1,10 +1,25 @@
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::watch; use base64::Engine as _;
use snafu::prelude::*;
use vapore_proto::{
enums::ESessionPersistence,
enums_clientserver::EMsg,
steammessages_auth_steamclient::{
CAuthentication_BeginAuthSessionViaCredentials_Request,
CAuthentication_BeginAuthSessionViaCredentials_Response, CAuthentication_DeviceDetails,
CAuthentication_GetPasswordRSAPublicKey_Request,
CAuthentication_GetPasswordRSAPublicKey_Response, EAuthTokenPlatformType,
},
steammessages_clientserver_login::CMsgClientHello,
};
use crate::{ use crate::{
connection::{CMContext, CMSession}, connection::{CMContext, CMSession},
state::apps::License, error::{RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu},
message::CMProtoBufMessage,
platform::generate_machine_id,
state::apps::AppsHandler,
ClientError, ClientError,
}; };
@ -13,10 +28,7 @@ struct SteamClientInner {
/// TODO: Support recreation when one fails /// TODO: Support recreation when one fails
pub session: CMSession, pub session: CMSession,
/// Licenses owned by current user, as told periodically by Valve. apps: AppsHandler,
/// Use an Arc in the receiver so that the sender doesn't block if
/// someone keeps a long-lived reference to the licenses
pub licenses: watch::Receiver<Arc<Vec<License>>>,
} }
/// Higher-level Steam client. Unlike [crate::connection::CMSession], /// Higher-level Steam client. Unlike [crate::connection::CMSession],
@ -29,19 +41,99 @@ pub struct SteamClient {
} }
impl SteamClient { impl SteamClient {
pub async fn connect(servers: &[&str]) -> Result<(Self, CMContext), ClientError> { pub async fn connect(servers: &[&str]) -> Result<Self, ClientError> {
let (session, context) = CMSession::connect(servers[0]).await?; let (session, context) = CMSession::connect(servers[0]).await?;
let inner = Arc::new(SteamClientInner { let inner = Arc::new(SteamClientInner {
apps: AppsHandler::listen(&session),
session, session,
licenses: todo!(),
}); });
Ok((Self { inner }, context)) tokio::spawn(context);
inner.session.send_notification(
EMsg::k_EMsgClientHello,
CMsgClientHello {
protocol_version: Some(0x1002c),
..Default::default()
},
)?;
Ok(Self { inner })
} }
pub fn licenses(&self) -> Arc<Vec<License>> { pub async fn auth_password(
self.inner.licenses.borrow().clone() &self,
username: String,
password: String,
code: Option<String>,
) -> Result<(), ClientError> {
let machine_id = generate_machine_id();
let password_key_response: CMProtoBufMessage<
CAuthentication_GetPasswordRSAPublicKey_Response,
> = self
.inner
.session
.call_service_method(
"Authentication.GetPasswordRSAPublicKey#1".to_string(),
CAuthentication_GetPasswordRSAPublicKey_Request {
account_name: Some(username.clone()),
..Default::default()
},
)
.await?;
let password_key = rsa::RsaPublicKey::new(
rsa::BigUint::parse_bytes(password_key_response.body.publickey_mod().as_bytes(), 16)
.context(RSAParameterParseSnafu {})?,
rsa::BigUint::parse_bytes(password_key_response.body.publickey_exp().as_bytes(), 16)
.context(RSAParameterParseSnafu {})?,
)
.context(RSAParseSnafu {})?;
let encrypted_password = password_key
.encrypt(
&mut rand::thread_rng(),
rsa::Pkcs1v15Encrypt,
password.as_bytes(),
)
.context(RSAEncryptSnafu {})?;
let auth_session_response = self
.inner
.session
.call_service_method::<_, CAuthentication_BeginAuthSessionViaCredentials_Response>(
"Authentication.BeginAuthSessionViaCredentials#1".to_string(),
CAuthentication_BeginAuthSessionViaCredentials_Request {
account_name: Some(username.clone()),
encrypted_password: Some(
base64::engine::general_purpose::STANDARD.encode(&encrypted_password),
),
encryption_timestamp: password_key_response.body.timestamp,
persistence: Some(protobuf::EnumOrUnknown::new(
ESessionPersistence::k_ESessionPersistence_Ephemeral,
)),
device_details: protobuf::MessageField::some(CAuthentication_DeviceDetails {
device_friendly_name: Some("vapore".to_string()),
platform_type: Some(protobuf::EnumOrUnknown::new(
EAuthTokenPlatformType::k_EAuthTokenPlatformType_SteamClient,
)),
// Windows 11
os_type: Some(20),
machine_id: Some(machine_id.clone()),
..Default::default()
}),
..Default::default()
},
)
.await?;
Ok(())
}
pub fn apps(&self) -> &AppsHandler {
&self.inner.apps
} }
pub fn session(&self) -> CMSession { pub fn session(&self) -> CMSession {

View file

@ -75,6 +75,15 @@ pub enum ClientError {
TryFromELicenseType { TryFromELicenseType {
source: TryFromPrimitiveError<ELicenseType>, source: TryFromPrimitiveError<ELicenseType>,
}, },
#[snafu(display("Unable to parse RSA key parameters"))]
RSAParameterParse,
#[snafu(display("Unable to parse RSA key"))]
RSAParse { source: rsa::Error },
#[snafu(display("Unable to encrupt with RSA"))]
RSAEncrypt { source: rsa::Error },
} }
impl<T> From<std::sync::PoisonError<T>> for ClientError { impl<T> From<std::sync::PoisonError<T>> for ClientError {

View file

@ -2,6 +2,7 @@ pub mod client;
pub mod connection; pub mod connection;
pub mod error; pub mod error;
pub mod message; pub mod message;
pub mod platform;
pub mod selection; pub mod selection;
pub mod state; pub mod state;

28
lib/src/platform.rs Normal file
View file

@ -0,0 +1,28 @@
use rand::RngCore;
pub fn generate_machine_id() -> Vec<u8> {
// machine_id is supposed to be a binary key/value with the SHA1 of the machine's
// BB3: Machine GUID
// FF2: MAC address
// 3B3: Disk ID
// We should probably make these consistent so Valve doesn't get suspicious,
// but for now let's make them random
// TODO: Find a more generic way to make this
let mut machine_id = Vec::with_capacity(155);
machine_id.extend_from_slice(b"\x00MessageObject\x00");
for key in [b"BB3", b"FF2", b"3B3"] {
let mut data = [0u8; 20];
rand::thread_rng().fill_bytes(&mut data);
let hex_bytes = hex::encode(data).into_bytes();
// Type is string
machine_id.push(b'\x01');
machine_id.extend_from_slice(key);
machine_id.push(b'\x00');
machine_id.extend_from_slice(&hex_bytes);
machine_id.push(b'\x00');
}
// suitable end bytes
machine_id.extend_from_slice(b"\x08\x08");
machine_id
}

View file

@ -1,11 +1,20 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use snafu::prelude::*; use snafu::prelude::*;
use vapore_proto::steammessages_clientserver::cmsg_client_license_list; use tokio::sync::watch;
use vapore_proto::{
enums_clientserver::EMsg,
steammessages_clientserver::{cmsg_client_license_list, CMsgClientLicenseList},
};
use crate::{ use crate::{
connection::CMSession,
error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu}, error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu},
message::{CMProtoBufMessage, CMRawProtoBufMessage},
ClientError, ClientError,
}; };
@ -271,3 +280,48 @@ impl TryFrom<cmsg_client_license_list::License> for License {
}) })
} }
} }
pub struct AppsHandler {
/// Licenses owned by current user, as told periodically by Valve.
/// Use an Arc in the receiver so that the sender doesn't block if
/// someone keeps a long-lived reference to the licenses
pub licenses: watch::Receiver<Arc<Vec<License>>>,
}
impl AppsHandler {
pub(crate) fn listen(session: &CMSession) -> Self {
let (licenses_sender, licenses) = watch::channel(Arc::new(Vec::new()));
let session_cloned = session.clone();
tokio::spawn(async move {
let mut listener = session_cloned.subscribe_message_type(EMsg::k_EMsgClientLicenseList);
while let Ok(raw_message) = listener.recv().await {
match CMProtoBufMessage::<CMsgClientLicenseList>::deserialize(raw_message) {
Ok(message) => {
let licenses = message
.body
.licenses
.into_iter()
.map(License::try_from)
.filter_map(|item| match item {
Ok(license) => Some(license),
Err(err) => {
log::debug!("Invalid License object received: {}", err);
None
}
})
.collect();
if licenses_sender.send(Arc::new(licenses)).is_err() {
// If there's no receiver then there's no more SteamClient, just die
return;
}
}
Err(err) => log::debug!("Invalid CMsgClientList: {}", err),
}
}
});
Self { licenses }
}
}