Start work on login
This commit is contained in:
parent
f04292e9f1
commit
c3b17bafdf
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
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 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue