use std::str::FromStr;
use base64ct::Encoding;
use oauth2::{RedirectUrl, Scope};
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::SecretKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub struct ProofKey {
alg: String,
crv: String,
kty: String,
#[serde(rename = "use")]
u: String,
x: String,
y: String,
}
impl ProofKey {
pub fn new(key: &SecretKey) -> Self {
let point = key.public_key().to_encoded_point(false);
Self {
crv: "P-256".into(),
alg: "ES256".into(),
u: "sig".into(),
kty: "EC".into(),
x: base64ct::Base64UrlUnpadded::encode_string(point.x().unwrap().as_slice()),
y: base64ct::Base64UrlUnpadded::encode_string(point.y().unwrap().as_slice()),
}
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum SigningAlgorithm {
ES256,
ES384,
ES521,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SigningPolicy {
pub version: i32,
pub supported_algorithms: Vec<SigningAlgorithm>,
pub max_body_bytes: usize,
}
impl Default for SigningPolicy {
fn default() -> Self {
Self {
version: 1,
supported_algorithms: vec![SigningAlgorithm::ES256],
max_body_bytes: 8192,
}
}
}
pub mod request {
use super::{Deserialize, ProofKey, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct SisuQuery<'a> {
pub display: &'a str,
pub code_challenge: &'a str,
pub code_challenge_method: &'a str,
pub state: &'a str,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SisuAuthenticationRequest<'a> {
pub app_id: &'a str,
pub title_id: &'a str,
pub redirect_uri: &'a str,
pub device_token: &'a str,
pub sandbox: &'a str,
pub token_type: &'a str,
pub offers: Vec<&'a str>,
pub query: SisuQuery<'a>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SisuAuthorizationRequest<'a> {
pub access_token: &'a str,
pub app_id: &'a str,
pub device_token: &'a str,
pub sandbox: &'a str,
pub site_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub proof_key: ProofKey,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XADProperties<'a> {
pub auth_method: &'a str,
pub id: &'a str,
pub device_type: &'a str,
pub version: &'a str,
pub proof_key: ProofKey,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XASTProperties<'a> {
pub auth_method: &'a str,
pub device_token: &'a str,
pub site_name: &'a str,
pub rps_ticket: &'a str,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XASUProperties<'a> {
pub auth_method: &'a str,
pub site_name: &'a str,
pub rps_ticket: &'a str,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XSTSProperties<'a> {
pub sandbox_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_token: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title_token: Option<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub user_tokens: Vec<&'a str>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XTokenRequest<'a, T> {
pub relying_party: &'a str,
pub token_type: &'a str,
pub properties: T,
}
}
pub mod response {
use chrono::{DateTime, TimeZone, Utc};
use oauth2::basic::BasicTokenResponse;
use url::Url;
use crate::Error;
use super::{Deserialize, HashMap, Serialize, SigningPolicy};
pub type WindowsLiveTokens = BasicTokenResponse;
pub type DeviceToken = XTokenResponse<XADDisplayClaims>;
pub type UserToken = XTokenResponse<XAUDisplayClaims>;
pub type TitleToken = XTokenResponse<XATDisplayClaims>;
pub type XSTSToken = XTokenResponse<XSTSDisplayClaims>;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XADDisplayClaims {
pub xdi: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XATDisplayClaims {
pub xti: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XAUDisplayClaims {
pub xui: Vec<HashMap<String, String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct XSTSDisplayClaims {
pub xui: Vec<HashMap<String, String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct XTokenResponse<T> {
pub issue_instant: DateTime<Utc>,
pub not_after: DateTime<Utc>,
pub token: String,
pub display_claims: Option<T>,
}
impl XTokenResponse<XSTSDisplayClaims> {
#[must_use]
pub fn userhash(&self) -> String {
self.display_claims.clone().unwrap().xui[0]["uhs"].clone()
}
#[must_use]
pub fn authorization_header_value(&self) -> String {
format!("XBL3.0 x={};{}", self.userhash(), self.token)
}
}
impl<T> From<&str> for XTokenResponse<T> {
fn from(s: &str) -> Self {
Self {
issue_instant: Utc.with_ymd_and_hms(2020, 12, 15, 0, 0, 0).unwrap(),
not_after: Utc.with_ymd_and_hms(2199, 12, 15, 0, 0, 0).unwrap(),
token: s.to_owned(),
display_claims: None,
}
}
}
impl<T> XTokenResponse<T> {
pub fn check_validity(&self) -> Result<(), Error> {
if self.not_after < chrono::offset::Utc::now() {
return Err(Error::TokenExpired(self.not_after));
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct SisuAuthenticationResponse {
pub msa_oauth_redirect: Url,
pub msa_request_parameters: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct SisuAuthorizationResponse {
pub device_token: String,
pub title_token: TitleToken,
pub user_token: UserToken,
pub authorization_token: XSTSToken,
pub web_page: String,
pub sandbox: String,
pub use_modern_gamertag: Option<bool>,
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct TitleEndpointCertificate {
pub thumbprint: String,
pub is_issuer: Option<bool>,
pub root_cert_index: i32,
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct TitleEndpoint {
pub protocol: String,
pub host: String,
pub host_type: String,
pub path: Option<String>,
pub relying_party: Option<String>,
pub sub_relying_party: Option<String>,
pub token_type: Option<String>,
pub signature_policy_index: Option<i32>,
pub server_cert_index: Option<Vec<i32>>,
}
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct TitleEndpointsResponse {
pub end_points: Vec<TitleEndpoint>,
pub signature_policies: Vec<SigningPolicy>,
pub certs: Vec<TitleEndpointCertificate>,
pub root_certs: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SisuSessionId(pub String);
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum AccessTokenPrefix {
D,
T,
None,
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for AccessTokenPrefix {
fn to_string(&self) -> String {
let prefix = match self {
AccessTokenPrefix::D => "d=",
AccessTokenPrefix::T => "t=",
AccessTokenPrefix::None => "",
};
prefix.to_string()
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
pub enum DeviceType {
IOS,
ANDROID,
WIN32,
NINTENDO,
Custom(String),
}
impl FromStr for DeviceType {
type Err = Box<dyn std::error::Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let enm = match s.to_lowercase().as_ref() {
"android" => DeviceType::ANDROID,
"ios" => DeviceType::IOS,
"win32" => DeviceType::WIN32,
"nintendo" => DeviceType::NINTENDO,
val => DeviceType::Custom(val.to_owned()),
};
Ok(enm)
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for DeviceType {
fn to_string(&self) -> String {
let str = match self {
DeviceType::ANDROID => "Android",
DeviceType::IOS => "iOS",
DeviceType::WIN32 => "Win32",
DeviceType::NINTENDO => "Nintendo",
DeviceType::Custom(val) => val,
};
str.to_owned()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct XalAppParameters {
pub client_id: String,
pub title_id: Option<String>,
pub auth_scopes: Vec<Scope>,
pub redirect_uri: Option<RedirectUrl>,
pub client_secret: Option<String>,
}
#[allow(non_snake_case)]
pub mod app_params {
use oauth2::{RedirectUrl, Scope};
use super::XalAppParameters;
use crate::Constants;
pub fn APP_XBOX_BETA() -> XalAppParameters {
XalAppParameters {
client_id: "000000004415494b".into(),
title_id: Some("177887386".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn APP_XBOX() -> XalAppParameters {
XalAppParameters {
client_id: "000000004c12ae6f".into(),
title_id: Some("328178078".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn APP_GAMEPASS() -> XalAppParameters {
XalAppParameters {
client_id: "000000004c20a908".into(),
title_id: Some("1016898439".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn APP_GAMEPASS_BETA() -> XalAppParameters {
XalAppParameters {
client_id: "000000004c20a908".into(),
title_id: Some("1016898439".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn APP_FAMILY_SETTINGS() -> XalAppParameters {
XalAppParameters {
client_id: "00000000482C8F49".into(),
title_id: Some("1618633878".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn APP_OLD_XBOX_APP() -> XalAppParameters {
XalAppParameters {
client_id: "0000000048093EE3".into(),
title_id: None,
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn MC_JAVA_WIN32() -> XalAppParameters {
XalAppParameters {
client_id: "00000000402b5328".into(),
title_id: None,
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn MC_BEDROCK_SWITCH() -> XalAppParameters {
XalAppParameters {
client_id: "00000000441cc96b".into(),
title_id: Some("2047319603".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn MC_BEDROCK_ANDROID() -> XalAppParameters {
XalAppParameters {
client_id: "0000000048183522".into(),
title_id: Some("1739947436".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
pub fn MC_BEDROCK_IOS() -> XalAppParameters {
XalAppParameters {
client_id: "000000004c17c01a".into(),
title_id: Some("1810924247".into()),
auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())],
redirect_uri: Some(
RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(),
),
client_secret: None,
}
}
}
impl Default for XalAppParameters {
fn default() -> Self {
app_params::APP_GAMEPASS_BETA()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct XalClientParameters {
pub user_agent: String,
pub device_type: DeviceType,
pub client_version: String,
pub query_display: String,
}
#[allow(non_snake_case)]
pub mod client_params {
use super::{DeviceType, XalClientParameters};
pub fn CLIENT_IOS() -> XalClientParameters {
XalClientParameters {
user_agent: "XAL iOS 2021.11.20211021.000".into(),
device_type: DeviceType::IOS,
client_version: "15.6.1".into(),
query_display: "ios_phone".into(),
}
}
pub fn CLIENT_ANDROID() -> XalClientParameters {
XalClientParameters {
user_agent: "XAL Android 2020.07.20200714.000".into(),
device_type: DeviceType::ANDROID,
client_version: "8.0.0".into(),
query_display: "android_phone".into(),
}
}
pub fn CLIENT_NINTENDO() -> XalClientParameters {
XalClientParameters {
user_agent: "XAL".into(),
device_type: DeviceType::NINTENDO,
client_version: "0.0.0".into(),
query_display: "touch".into(),
}
}
pub fn CLIENT_WINDOWS() -> XalClientParameters {
XalClientParameters {
user_agent: "XAL GRTS 2024.02.20240220.000".into(),
device_type: DeviceType::WIN32,
client_version: "10.0.22621.4196".into(),
query_display: "none".into()
}
}
}
impl Default for XalClientParameters {
fn default() -> Self {
client_params::CLIENT_ANDROID()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn deserialize_xsts() {
let data = r#"
{
"IssueInstant": "2010-10-10T03:06:35.5251155Z",
"NotAfter": "2999-10-10T19:06:35.5251155Z",
"Token": "123456789",
"DisplayClaims": {
"xui": [
{
"gtg": "e",
"xid": "2669321029139235",
"uhs": "abcdefg",
"agg": "Adult",
"usr": "",
"utr": "",
"prv": ""
}
]
}
}
"#;
let xsts: response::XSTSToken =
serde_json::from_str(data).expect("BUG: Failed to deserialize XSTS response");
assert_eq!(xsts.userhash(), "abcdefg");
assert_eq!(
xsts.authorization_header_value(),
"XBL3.0 x=abcdefg;123456789"
);
assert_eq!(xsts.token, "123456789".to_owned());
assert_eq!(
xsts.display_claims.as_ref().unwrap().xui[0].get("gtg"),
Some(&"e".to_owned())
);
assert_ne!(
xsts.display_claims.as_ref().unwrap().xui[0].get("uhs"),
Some(&"invalid".to_owned())
);
}
#[test]
fn deserialize_signing_policy() {
let json_resp = r#"{
"Version": 99,
"SupportedAlgorithms": ["ES521"],
"MaxBodyBytes": 1234
}"#;
let deserialized: SigningPolicy =
serde_json::from_str(json_resp).expect("Failed to deserialize SigningPolicy");
assert_eq!(deserialized.version, 99);
assert_eq!(deserialized.max_body_bytes, 1234);
assert_eq!(
deserialized.supported_algorithms,
vec![SigningAlgorithm::ES521]
)
}
#[test]
fn devicetype_enum_into() {
assert_eq!(DeviceType::WIN32.to_string(), "Win32");
assert_eq!(DeviceType::ANDROID.to_string(), "Android");
assert_eq!(DeviceType::IOS.to_string(), "iOS");
assert_eq!(DeviceType::NINTENDO.to_string(), "Nintendo");
}
#[test]
fn str_into_devicetype_enum() {
assert_eq!(DeviceType::from_str("win32").unwrap(), DeviceType::WIN32);
assert_eq!(DeviceType::from_str("Win32").unwrap(), DeviceType::WIN32);
assert_eq!(DeviceType::from_str("WIN32").unwrap(), DeviceType::WIN32);
assert_eq!(
DeviceType::from_str("android").unwrap(),
DeviceType::ANDROID
);
assert_eq!(DeviceType::from_str("ios").unwrap(), DeviceType::IOS);
assert_eq!(
DeviceType::from_str("nintendo").unwrap(),
DeviceType::NINTENDO
);
assert_eq!(
DeviceType::from_str("androidx").unwrap(),
DeviceType::Custom("androidx".into())
);
}
}