Start work on login
This commit is contained in:
parent
f04292e9f1
commit
c3b17bafdf
|
@ -5,10 +5,12 @@ version.workspace = true
|
|||
|
||||
[dependencies]
|
||||
async-tungstenite = { version = "0.27.0", features = ["tokio-rustls-native-certs"] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.6.0"
|
||||
color-eyre.workspace = true
|
||||
flate2 = "1.0.33"
|
||||
futures = "0.3.30"
|
||||
hex = "0.4.3"
|
||||
keyvalues-serde.workspace = true
|
||||
log.workspace = true
|
||||
num_enum = "0.7.3"
|
||||
|
@ -22,8 +24,6 @@ tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros", "time
|
|||
vapore-proto.path = "../proto"
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.22.1"
|
||||
dialoguer = "0.11.0"
|
||||
env_logger.workspace = true
|
||||
hex = "0.4.3"
|
||||
qrcode = "0.14.1"
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
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::{
|
||||
connection::{CMContext, CMSession},
|
||||
state::apps::License,
|
||||
error::{RSAEncryptSnafu, RSAParameterParseSnafu, RSAParseSnafu},
|
||||
message::CMProtoBufMessage,
|
||||
platform::generate_machine_id,
|
||||
state::apps::AppsHandler,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
|
@ -13,10 +28,7 @@ struct SteamClientInner {
|
|||
/// TODO: Support recreation when one fails
|
||||
pub session: CMSession,
|
||||
|
||||
/// 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>>>,
|
||||
apps: AppsHandler,
|
||||
}
|
||||
|
||||
/// Higher-level Steam client. Unlike [crate::connection::CMSession],
|
||||
|
@ -29,19 +41,99 @@ pub struct 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 inner = Arc::new(SteamClientInner {
|
||||
apps: AppsHandler::listen(&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>> {
|
||||
self.inner.licenses.borrow().clone()
|
||||
pub async fn auth_password(
|
||||
&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 {
|
||||
|
|
|
@ -75,6 +75,15 @@ pub enum ClientError {
|
|||
TryFromELicenseType {
|
||||
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 {
|
||||
|
|
|
@ -2,6 +2,7 @@ pub mod client;
|
|||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod message;
|
||||
pub mod platform;
|
||||
pub mod selection;
|
||||
pub mod state;
|
||||
|
||||
|
|
28
lib/src/platform.rs
Normal file
28
lib/src/platform.rs
Normal 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
|
||||
}
|
|
@ -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 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::{
|
||||
connection::CMSession,
|
||||
error::{TryFromELicenseTypeSnafu, TryFromEPaymentMethodSnafu},
|
||||
message::{CMProtoBufMessage, CMRawProtoBufMessage},
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue