OneLauncher-Cracked/packages/core/src/store/credentials.rs
2025-07-25 11:54:45 +02:00

1064 lines
27 KiB
Rust

//! Handles MSA Authentication flow.
use base64::Engine;
use chrono::{DateTime, Utc};
use p256::ecdsa::SigningKey;
use p256::ecdsa::signature::Signer;
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey};
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
use std::collections::HashMap;
use std::fmt::Write;
use uuid::Uuid;
use crate::constants::MINECRAFT_SCOPES;
use crate::error::{LauncherError, LauncherResult};
use crate::store::Core;
use crate::utils::io;
use super::Dirs;
/// The core state of Microsoft authentication for the launcher
#[derive(Serialize, Deserialize, Debug)]
pub struct CredentialsStore {
/// A [`HashMap`] of all logged in users by [`Uuid`] and their Minecraft credentials.
pub users: HashMap<Uuid, MinecraftCredentials>,
/// The current stored device token to be used while logging in.
pub token: Option<SaveDeviceToken>,
/// The default Minecraft user which the launcher uses, by [`Uuid`].
pub default_user: Option<Uuid>,
}
impl CredentialsStore {
/// Initialize a new Minecraft global state.
#[tracing::instrument]
pub async fn initialize() -> LauncherResult<Self> {
let path = Dirs::get_auth_file().await?;
let store = io::read_json(&path).await.ok();
store.map_or_else(
|| {
Ok(Self {
users: HashMap::new(),
token: None,
default_user: None,
})
},
Ok,
)
}
/// Save the current Minecraft credentials.
#[tracing::instrument(skip(self))]
pub async fn save(&self) -> LauncherResult<()> {
let path = Dirs::get_auth_file().await?;
io::write_json(&path, &self).await?;
Ok(())
}
/// Refresh all tokens in the global state.
#[tracing::instrument(skip(self))]
async fn refresh(
&mut self,
current_date: DateTime<Utc>,
force: bool,
) -> LauncherResult<(DeviceKey, DeviceToken, DateTime<Utc>, bool)> {
macro_rules! device_key {
($self:ident, $device_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
let key = device_key()?;
let res = device_token(&key, current_date).await?;
self.token = Some(SaveDeviceToken {
id: key.id.clone(),
private_key: key
.key
.to_pkcs8_pem(p256::pkcs8::LineEnding::default())
.map_err(|err| MinecraftAuthError::PKCS8Error(err))?
.to_string(),
x: key.x.clone(),
y: key.y.clone(),
token: res.value.clone(),
});
self.save().await?;
(key, res.value, res.date, true)
}};
}
let (key, token, date, valid_date) = if let Some(ref token) = self.token {
if let Ok(private_key) = SigningKey::from_pkcs8_pem(&token.private_key) {
if token.token.not_after > Utc::now() && !force {
(
DeviceKey {
id: token.id.clone(),
key: private_key,
x: token.x.clone(),
y: token.y.clone(),
},
token.token.clone(),
current_date,
false,
)
} else {
let key = DeviceKey {
id: token.id.clone(),
key: private_key,
x: token.x.clone(),
y: token.y.clone(),
};
let res = device_token(&key, current_date).await?;
(key, res.value, res.date, true)
}
} else {
device_key!(self, device_key, device_token, SaveDeviceToken)
}
} else {
device_key!(self, device_key, device_token, SaveDeviceToken)
};
Ok((key, token, date, valid_date))
}
/// Begin a Microsoft authentication flow.
#[tracing::instrument(skip(self))]
pub async fn begin(&mut self) -> LauncherResult<MinecraftLogin> {
let (key, token, current_date, valid_date) = self.refresh(Utc::now(), false).await?;
let verify = generate_oauth_challenge();
let mut hash = sha2::Sha256::new();
hash.update(&verify);
let hash_result = hash.finalize();
let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash_result);
match sisu_authenticate(&token.token, &challenge, &key, current_date).await {
Ok((session_id, redirect_uri)) => Ok(MinecraftLogin {
verify,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
}),
Err(err) => {
if valid_date {
Err(err.into())
} else {
let (key, token, current_date, _) = self.refresh(Utc::now(), false).await?;
let verify = generate_oauth_challenge();
let mut hash = sha2::Sha256::new();
hash.update(&verify);
let hash_result = hash.finalize();
let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash_result);
let (session_id, redirect_uri) =
sisu_authenticate(&token.token, &challenge, &key, current_date).await?;
Ok(MinecraftLogin {
verify,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
})
}
}
}
}
/// Finish a Microsoft authentication flow.
#[tracing::instrument(skip(self))]
pub async fn finish(
&mut self,
code: &str,
flow: MinecraftLogin,
) -> LauncherResult<MinecraftCredentials> {
let (key, token, _, _) = self.refresh(Utc::now(), false).await?;
let oauth_token = oauth_token(code, &flow.verify).await?;
let sisu_authorize = sisu_authorize(
Some(&flow.session_id),
&oauth_token.value.access_token,
&token.token,
&key,
oauth_token.date,
)
.await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
minecraft_entitlements(&minecraft_token.access_token).await?;
let profile = minecraft_profile(&minecraft_token.access_token).await?;
let profile_id = profile.id.unwrap_or_default();
let credentials = MinecraftCredentials {
id: profile_id,
username: profile.name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.value.refresh_token,
#[allow(clippy::cast_possible_wrap)]
expires: oauth_token.date
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(profile_id, credentials.clone());
if self.default_user.is_none() {
self.default_user = Some(profile_id);
}
self.save().await?;
Ok(credentials)
}
/// Refresh the current stored [`MinecraftCredentials`].
async fn refresh_token(
&mut self,
creds: &MinecraftCredentials,
) -> LauncherResult<Option<MinecraftCredentials>> {
let cred_id = creds.id;
let cred_name = creds.username.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token, current_date, _) = self.refresh(oauth_token.date, false).await?;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.value.access_token,
&token.token,
&key,
current_date,
)
.await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
let val = MinecraftCredentials {
id: cred_id,
username: cred_name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.value.refresh_token,
#[allow(clippy::cast_possible_wrap)]
expires: oauth_token.date
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(val.id, val.clone());
self.save().await?;
Ok(Some(val))
}
/// Get the default user account as a set of optional [`MinecraftCredentials`].
#[tracing::instrument(skip(self))]
pub async fn get_default(&mut self) -> LauncherResult<Option<MinecraftCredentials>> {
let credentials = if let Some(default_user) = self.default_user {
if let Some(creds) = self.users.get(&default_user) {
Some(creds)
} else {
self.users.values().next()
}
} else {
self.users.values().next()
};
if let Some(creds) = credentials {
if self.default_user != Some(creds.id) {
self.default_user = Some(creds.id);
self.save().await?;
}
if creds.expires < Utc::now() {
let old_creds = creds.clone();
let res = self.refresh_token(&old_creds).await;
match res {
Ok(val) => Ok(val),
Err(err) => {
match err {
LauncherError::MinecraftAuthError(
MinecraftAuthError::RequestError { ref source, .. },
) if source.is_connect() || source.is_timeout() => {
return Ok(Some(old_creds));
}
_ => {}
}
Err(err)
}
}
} else {
Ok(Some(creds.clone()))
}
} else {
Ok(None)
}
}
/// Remove a set of [`MinecraftCredentials`] from a Uuid.
#[tracing::instrument(skip(self))]
pub async fn remove(&mut self, id: Uuid) -> LauncherResult<Option<MinecraftCredentials>> {
let val = self.users.remove(&id);
if self.default_user == Some(id) {
self.default_user = None;
}
self.save().await?;
Ok(val)
}
}
/// A [`reqwest::Request`] with a [`DateTime<Utc>`] attached to it.
struct RequestWithDate<T> {
pub date: DateTime<Utc>,
pub value: T,
}
/// A device token used for Microsoft authentication.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DeviceToken {
/// The time that the token was issued in [`DateTime<Utc>`].
pub issue_instant: DateTime<Utc>,
/// The expiration date of this token in [`DateTime<Utc>`].
pub not_after: DateTime<Utc>,
/// The token as a string.
pub token: String,
/// The JSON-encoded [`HashMap<String, Value>`] of display claims.
pub display_claims: HashMap<String, serde_json::Value>,
}
/// A transferrable EC key translated to a [`SaveDeviceToken`]
#[derive(Debug)]
pub struct DeviceKey {
/// The ID of this device key.
pub id: String,
/// The EC private key.
pub key: SigningKey,
/// The EC `x` value.
pub x: String,
/// The EC `y` value.
pub y: String,
}
/// A serializable device token that is used to finish authenticaton.
#[derive(Serialize, Deserialize, Debug)]
pub struct SaveDeviceToken {
/// The ID of the token.
pub id: String,
/// The private key associated with the token.
pub private_key: String,
/// The EC `x` value of the key.
pub x: String,
/// The EC `y` value of the key.
pub y: String,
/// The assocated [`DeviceToken`].
pub token: DeviceToken,
}
/// Core variables passed throughout the Minecraft login flow.
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, Debug)]
pub struct MinecraftLogin {
pub verify: String,
/// The OAuth challenge generated at the start of the flow.
pub challenge: String,
/// The generated session id.
pub session_id: String,
/// The xboxlive redirect URI.
pub redirect_uri: String,
}
/// A structure of all needed Minecraft credentials for logging in and account management.
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MinecraftCredentials {
/// The uuid of the credentials.
pub id: Uuid,
/// The username of the Minecraft account.
pub username: String,
/// The access token as a String.
pub access_token: String,
/// The refresh token as a string for [`MinecraftState#refresh`].
pub refresh_token: String,
/// The time that the access token expires as a [`DateTime<Utc>`].
pub expires: DateTime<Utc>,
}
#[tracing::instrument(skip(key))]
pub async fn device_token(
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
let res = send_signed_request(
None,
"https://device.auth.xboxlive.com/device/authenticate",
"/device/authenticate",
json!({
"Properties": {
"AuthMethod": "ProofOfPossession",
"Id": format!("{{{}}}", key.id),
"DeviceType": "Win32",
"Version": "10.16.0",
"ProofKey": {
"kty": "EC",
"x": key.x,
"y": key.y,
"crv": "P-256",
"alg": "ES256",
"use": "sig"
}
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}),
key,
MinecraftAuthStep::DeviceToken,
current_date,
)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RedirectUri {
pub msa_oauth_redirect: String,
}
#[tracing::instrument(skip(key))]
async fn sisu_authenticate(
token: &str,
challenge: &str,
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<(String, RequestWithDate<RedirectUri>), MinecraftAuthError> {
// what was microsoft on when writing this??????????
let res = send_signed_request::<RedirectUri>(
None,
"https://sisu.xboxlive.com/authenticate",
"/authenticate",
json!({
"AppId": Core::get().msa_client_id,
"DeviceToken": token,
"Offers": [MINECRAFT_SCOPES],
"Query": {
"code_challenge": challenge,
"code_challenge_method": "S256",
"state": generate_oauth_challenge(),
"prompt": "select_account"
},
"RedirectUri": Core::get().msa_redirect_uri,
"Sandbox": "RETAIL",
"TokenType": "code",
"TitleId": "1794566092"
}),
key,
MinecraftAuthStep::SisuAuthenicate,
current_date,
)
.await?;
let session_id = res
.headers
.get("X-SessionId")
.and_then(|x| x.to_str().ok())
.ok_or_else(|| MinecraftAuthError::SessionIdError)?
.to_string();
Ok((
session_id,
RequestWithDate {
date: res.current_date,
value: res.body,
},
))
}
#[derive(Deserialize)]
struct OAuthToken {
// pub token_type: String,
pub expires_in: u64,
// pub scope: String,
pub access_token: String,
pub refresh_token: String,
// pub user_id: String,
// pub foci: String,
}
#[tracing::instrument]
async fn oauth_token(
code: &str,
verify: &str,
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthError> {
let core = Core::get();
let mut query = HashMap::new();
query.insert("client_id", core.msa_client_id.as_str());
query.insert("code", code);
query.insert("code_verifier", verify);
query.insert("grant_type", "authorization_code");
query.insert("redirect_uri", core.msa_redirect_uri.as_str());
query.insert("scope", MINECRAFT_SCOPES);
let res = auth_retry(|| {
crate::utils::http::REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
.send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError {
source,
step: MinecraftAuthStep::OAuthToken,
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::OAuthToken,
source,
})?;
let body =
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::OAuthToken,
raw: text,
source,
status_code: status,
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
#[tracing::instrument]
async fn oauth_refresh(
refresh_token: &str,
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthError> {
let core = Core::get();
let mut query = HashMap::new();
query.insert("client_id", core.msa_client_id.as_str());
query.insert("refresh_token", refresh_token);
query.insert("grant_type", "refresh_token");
query.insert("redirect_uri", core.msa_redirect_uri.as_str());
query.insert("scope", MINECRAFT_SCOPES);
let res = auth_retry(|| {
crate::utils::http::REQWEST_CLIENT
.post("https://login.live.com/oauth20_token.srf")
.header("Accept", "application/json")
.form(&query)
.send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::RefreshOAuthToken,
source,
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::RefreshOAuthToken,
source,
})?;
let body =
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::RefreshOAuthToken,
raw: text,
source,
status_code: status,
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct SisuAuthorize {
// pub authorization_token: DeviceToken,
// pub device_token: String,
// pub sandbox: String,
pub title_token: DeviceToken,
pub user_token: DeviceToken,
// pub web_page: string,
}
#[tracing::instrument(skip(key))]
async fn sisu_authorize(
session_id: Option<&str>,
access_token: &str,
device_token: &str,
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<SisuAuthorize>, MinecraftAuthError> {
let core = Core::get();
let res = send_signed_request(
None,
"https://sisu.xboxlive.com/authorize",
"/authorize",
json!({
"AccessToken": format!("t={access_token}"),
"AppId": core.msa_client_id,
"DeviceToken": device_token,
"ProofKey": {
// kibty (kty)
"kty": "EC",
"x": key.x,
"y": key.y,
"crv": "P-256",
"alg": "ES256",
"use": "sig"
},
"Sandbox": "RETAIL",
"SessionId": session_id,
"SiteName": "user.auth.xboxlive.com",
"RelyingParty": "http://xboxlive.com",
"UseModernGamertag": true
}),
key,
MinecraftAuthStep::SisuAuthorize,
current_date,
)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[tracing::instrument(skip(key))]
async fn xsts_authorize(
authorize: SisuAuthorize,
device_token: &str,
key: &DeviceKey,
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
let res = send_signed_request(
None,
"https://xsts.auth.xboxlive.com/xsts/authorize",
"/xsts/authorize",
json!({
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT",
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [authorize.user_token.token],
"DeviceToken": device_token,
"TitleToken": authorize.title_token.token,
},
}),
key,
MinecraftAuthStep::XstsAuthorize,
current_date,
)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
struct MinecraftToken {
// pub username: String,
pub access_token: String,
// pub token_type: String,
// pub expires_in: u64,
}
#[tracing::instrument]
async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, MinecraftAuthError> {
let uhs = token
.display_claims
.get("xui")
.and_then(|x| x.get(0))
.and_then(|x| x.get("uhs"))
.and_then(|x| x.as_str().map(String::from))
.ok_or_else(|| MinecraftAuthError::HashError)?;
let token = token.token;
let res = auth_retry(|| {
crate::utils::http::REQWEST_CLIENT
.post("https://api.minecraftservices.com/launcher/login")
.header("Accept", "application/json")
.json(&json!({
"platform": "PC_LAUNCHER",
"xtoken": format!("XBL3.0 x={uhs};{token}"),
}))
.send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftToken,
source,
})?;
let status = res.status();
let text = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftToken,
source,
})?;
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftToken,
raw: text,
source,
status_code: status,
})
}
#[derive(Deserialize)]
struct MinecraftProfile {
pub id: Option<Uuid>,
pub name: String,
}
#[tracing::instrument]
async fn minecraft_profile(token: &str) -> Result<MinecraftProfile, MinecraftAuthError> {
let res = auth_retry(|| {
crate::utils::http::REQWEST_CLIENT
.get("https://api.minecraftservices.com/minecraft/profile")
.header("Accept", "application/json")
.bearer_auth(token)
.send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftProfile,
source,
})?;
let status = res.status();
let text = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftProfile,
source,
})?;
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftProfile,
raw: text,
source,
status_code: status,
})
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct MinecraftEntitlements {}
#[tracing::instrument]
async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, MinecraftAuthError> {
let res = auth_retry(|| {
crate::utils::http::REQWEST_CLIENT
.get("https://api.minecraftservices.com/entitlements/mcstore")
.header("Accept", "application/json")
.bearer_auth(token)
.send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftEntitlements,
source,
})?;
let status = res.status();
let text = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError {
step: MinecraftAuthStep::MinecraftEntitlements,
source,
})?;
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
step: MinecraftAuthStep::MinecraftEntitlements,
raw: text,
source,
status_code: status,
})
}
#[tracing::instrument(skip(request_fn))]
async fn auth_retry<F>(request_fn: impl Fn() -> F) -> Result<reqwest::Response, reqwest::Error>
where
F: Future<Output = Result<reqwest::Response, reqwest::Error>>,
{
const RETRY_COUNT: usize = 5;
const RETRY_WAIT: tokio::time::Duration = tokio::time::Duration::from_millis(250);
let mut resp = request_fn().await;
for i in 0..RETRY_COUNT {
match &resp {
Ok(_) => {
break;
}
Err(err) => {
if err.is_connect() || err.is_timeout() {
if i < RETRY_COUNT - 1 {
tracing::debug!(
"request failed due to a connection error, retrying request..."
);
tokio::time::sleep(RETRY_WAIT).await;
resp = request_fn().await;
} else {
break;
}
}
}
}
}
resp
}
#[tracing::instrument]
fn device_key() -> Result<DeviceKey, MinecraftAuthError> {
let id = Uuid::new_v4().to_string().to_uppercase();
let signing_key = SigningKey::random(&mut rand::rngs::OsRng);
let public_key = p256::ecdsa::VerifyingKey::from(&signing_key);
let encoded_point = public_key.to_encoded_point(false);
Ok(DeviceKey {
id,
key: signing_key,
x: base64::prelude::BASE64_STANDARD_NO_PAD.encode(
encoded_point
.x()
.ok_or_else(|| MinecraftAuthError::PublicKeyReading)?,
),
y: base64::prelude::BASE64_STANDARD_NO_PAD.encode(
encoded_point
.y()
.ok_or_else(|| MinecraftAuthError::PublicKeyReading)?,
),
})
}
struct SignedRequestResponse<T> {
pub headers: reqwest::header::HeaderMap,
pub current_date: DateTime<Utc>,
pub body: T,
}
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
async fn send_signed_request<T: serde::de::DeserializeOwned>(
authorization: Option<&str>,
url: &str,
url_path: &str,
raw_body: serde_json::Value,
key: &DeviceKey,
step: MinecraftAuthStep,
current_date: DateTime<Utc>,
) -> Result<SignedRequestResponse<T>, MinecraftAuthError> {
use byteorder::WriteBytesExt;
let auth = authorization.map_or(Vec::new(), |v| v.as_bytes().to_vec());
let body = serde_json::to_vec(&raw_body)
.map_err(|source| MinecraftAuthError::SerializeError { step, source })?;
let time: u128 = { ((current_date.timestamp() as u128) + 11_644_473_600) * 10_000_000 };
let mut buffer = Vec::new();
buffer
.write_u32::<byteorder::BigEndian>(1)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer
.write_u64::<byteorder::BigEndian>(time as u64)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer.extend_from_slice(b"POST");
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer.extend_from_slice(url_path.as_bytes());
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer.extend_from_slice(&auth);
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
buffer.extend_from_slice(&body);
buffer
.write_u8(0)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
let ecdsa_sig: p256::ecdsa::Signature = key.key.sign(&buffer);
let mut sig_buffer = Vec::new();
sig_buffer
.write_i32::<byteorder::BigEndian>(1)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
sig_buffer
.write_u64::<byteorder::BigEndian>(time as u64)
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes());
sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes());
let signature = base64::prelude::BASE64_STANDARD.encode(&sig_buffer);
let res = auth_retry(|| {
let mut request = crate::utils::http::REQWEST_CLIENT
.post(url)
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.header("Signature", &signature);
if url != "https://sisu.xboxlive.com/authorize" {
request = request.header("x-xbl-contract-version", "1");
}
if let Some(auth) = authorization {
request = request.header("Authorization", auth);
}
request.body(body.clone()).send()
})
.await
.map_err(|source| MinecraftAuthError::RequestError { step, source })?;
let status = res.status();
let headers = res.headers().clone();
let current_date = get_date_header(&headers);
let body = res
.text()
.await
.map_err(|source| MinecraftAuthError::RequestError { step, source })?;
let body =
serde_json::from_str(&body).map_err(|source| MinecraftAuthError::DeserializeError {
step,
raw: body,
source,
status_code: status,
})?;
Ok(SignedRequestResponse {
headers,
current_date,
body,
})
}
#[tracing::instrument]
fn get_date_header(headers: &reqwest::header::HeaderMap) -> DateTime<Utc> {
headers
.get(reqwest::header::DATE)
.and_then(|x| x.to_str().ok())
.and_then(|x| DateTime::parse_from_rfc2822(x).ok())
.map_or_else(Utc::now, |x| x.with_timezone(&Utc))
}
#[tracing::instrument]
fn generate_oauth_challenge() -> String {
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..64).map(|_| rng.r#gen::<u8>()).collect();
bytes.iter().fold(String::new(), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
/// An ordered list of all MSA authentication steps.
#[onelauncher_macro::specta]
#[derive(Debug, Serialize, Clone, Copy)]
pub enum MinecraftAuthStep {
DeviceToken,
SisuAuthenicate,
OAuthToken,
RefreshOAuthToken,
SisuAuthorize,
XstsAuthorize,
MinecraftToken,
MinecraftEntitlements,
MinecraftProfile,
}
/// Wrapper around all `Error`s that can occur during the Microsoft authentication process.
#[onelauncher_macro::error]
#[derive(Debug, thiserror::Error)]
pub enum MinecraftAuthError {
#[error("failed to read public key during key generation")]
PublicKeyReading,
#[error("failed to serialize private key using PKCS8: {0}")]
PKCS8Error(
#[from]
#[skip]
p256::pkcs8::Error,
),
#[error("failed to serialize JSON during MSA step {step:?}: {source}")]
SerializeError {
step: MinecraftAuthStep,
#[source]
#[skip]
source: serde_json::Error,
},
#[error(
"failed to deserialize JSON during MSA step {step:?}: {source}! status code {status_code} - body: {raw}"
)]
DeserializeError {
step: MinecraftAuthStep,
raw: String,
#[source]
#[skip]
source: serde_json::Error,
#[skip]
status_code: reqwest::StatusCode,
},
#[error("failed to request using HTTP during MSA step {step:?}: {source}")]
RequestError {
step: MinecraftAuthStep,
#[source]
#[skip]
source: reqwest::Error,
},
#[error("failed to create signed buffer during MSA step {step:?}: {source}")]
SigningError {
step: MinecraftAuthStep,
#[source]
#[skip]
source: std::io::Error,
},
#[error("failed to read user hash")]
HashError,
#[error("failed to read user xbox session ID")]
SessionIdError,
}