1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

auth works!

This commit is contained in:
mat 2022-10-16 22:13:56 -05:00
parent 8e86450947
commit 74288b55c3
11 changed files with 161 additions and 50 deletions

1
Cargo.lock generated
View file

@ -116,7 +116,6 @@ dependencies = [
name = "azalea-auth"
version = "0.1.0"
dependencies = [
"anyhow",
"azalea-buf",
"azalea-crypto",
"chrono",

View file

@ -8,7 +8,6 @@ version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.65"
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"}
chrono = {version = "0.4.22", default-features = false}

View file

@ -1,7 +1,6 @@
//! Handle Minecraft (Xbox) authentication.
use crate::cache::{self, CachedAccount, ExpiringValue};
use anyhow::{anyhow, bail};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
@ -10,6 +9,7 @@ use std::{
path::PathBuf,
time::{Instant, SystemTime, UNIX_EPOCH},
};
use thiserror::Error;
#[derive(Default)]
pub struct AuthOpts {
@ -25,13 +25,36 @@ pub struct AuthOpts {
pub cache_file: Option<PathBuf>,
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error(
"The Minecraft API is indicating that you don't own the game. \
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
)]
DoesNotOwnGame,
#[error("Error getting Microsoft auth token: {0}")]
GetMicrosoftAuthToken(#[from] GetMicrosoftAuthTokenError),
#[error("Error refreshing Microsoft auth token: {0}")]
RefreshMicrosoftAuthToken(#[from] RefreshMicrosoftAuthTokenError),
#[error("Error getting Xbox Live auth token: {0}")]
GetXboxLiveAuthToken(#[from] MinecraftXstsAuthError),
#[error("Error getting Minecraft profile: {0}")]
GetMinecraftProfile(#[from] GetProfileError),
#[error("Error checking ownership: {0}")]
CheckOwnership(#[from] CheckOwnershipError),
#[error("Error getting Minecraft auth token: {0}")]
GetMinecraftAuthToken(#[from] MinecraftAuthError),
#[error("Error authenticating with Xbox Live: {0}")]
GetXboxLiveAuth(#[from] XboxLiveAuthError),
}
/// Authenticate with authenticate with Microsoft. If the data isn't cached,
/// they'll be asked to go to log into Microsoft in a web page.
///
/// The email is technically only used as a cache key, so it *could* be
/// anything. You should just have it be the actual email so it's not confusing
/// though, and in case the Microsoft API does start providing the real email.
pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
pub async fn auth(email: &str, opts: AuthOpts) -> Result<AuthResult, AuthError> {
let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(&cache_file, email).await {
Some(account)
} else { None };
@ -82,17 +105,14 @@ pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
if opts.check_ownership {
let has_game = check_ownership(&client, &minecraft_access_token).await?;
if !has_game {
bail!(
"The Minecraft API is indicating that you don't own the game. \
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
);
return Err(AuthError::DoesNotOwnGame);
}
}
profile = get_profile(&client, &minecraft_access_token).await?;
if let Some(cache_file) = opts.cache_file {
cache::set_account_in_cache(
if let Err(e) = cache::set_account_in_cache(
&cache_file,
email,
CachedAccount {
@ -103,7 +123,9 @@ pub async fn auth(email: &str, opts: AuthOpts) -> anyhow::Result<AuthResult> {
profile: profile.clone(),
},
)
.await?;
.await {
log::warn!("Error while caching auth data: {}", e);
}
}
}
@ -192,10 +214,18 @@ pub struct ProfileResponse {
// nintendo switch (so it works for accounts that are under 18 years old)
const CLIENT_ID: &str = "00000000441cc96b";
#[derive(Debug, Error)]
pub enum GetMicrosoftAuthTokenError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
#[error("Authentication timed out")]
Timeout,
}
/// Asks the user to go to a webpage and log in with Microsoft.
async fn interactive_get_ms_auth_token(
client: &reqwest::Client,
) -> anyhow::Result<ExpiringValue<AccessTokenResponse>> {
) -> Result<ExpiringValue<AccessTokenResponse>, GetMicrosoftAuthTokenError> {
let res = client
.post("https://login.live.com/oauth20_connect.srf")
.form(&vec![
@ -247,13 +277,19 @@ async fn interactive_get_ms_auth_token(
}
}
Err(anyhow!("Authentication timed out"))
Err(GetMicrosoftAuthTokenError::Timeout)
}
#[derive(Debug, Error)]
pub enum RefreshMicrosoftAuthTokenError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
async fn refresh_ms_auth_token(
client: &reqwest::Client,
refresh_token: &str,
) -> anyhow::Result<ExpiringValue<AccessTokenResponse>> {
) -> Result<ExpiringValue<AccessTokenResponse>, RefreshMicrosoftAuthTokenError> {
let access_token_response = client
.post("https://login.live.com/oauth20_token.srf")
.form(&vec![
@ -278,10 +314,18 @@ async fn refresh_ms_auth_token(
})
}
#[derive(Debug, Error)]
pub enum XboxLiveAuthError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
#[error("Invalid expiry date: {0}")]
InvalidExpiryDate(String),
}
async fn auth_with_xbox_live(
client: &reqwest::Client,
access_token: &str,
) -> anyhow::Result<ExpiringValue<XboxLiveAuth>> {
) -> Result<ExpiringValue<XboxLiveAuth>, XboxLiveAuthError> {
let auth_json = json!({
"Properties": {
"AuthMethod": "RPS",
@ -311,12 +355,7 @@ async fn auth_with_xbox_live(
// not_after looks like 2020-12-21T19:52:08.4463796Z
let expires_at = DateTime::parse_from_rfc3339(&res.not_after)
.map_err(|_| {
anyhow!(
"Failed to parse not_after date from Xbox Live: {}",
res.not_after
)
})?
.map_err(|e| XboxLiveAuthError::InvalidExpiryDate(format!("{}: {}", res.not_after, e)))?
.with_timezone(&Utc)
.timestamp() as u64;
Ok(ExpiringValue {
@ -328,10 +367,16 @@ async fn auth_with_xbox_live(
})
}
#[derive(Debug, Error)]
pub enum MinecraftXstsAuthError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
async fn obtain_xsts_for_minecraft(
client: &reqwest::Client,
xbl_auth_token: &str,
) -> anyhow::Result<String> {
) -> Result<String, MinecraftXstsAuthError> {
let res = client
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.header("Accept", "application/json")
@ -352,11 +397,17 @@ async fn obtain_xsts_for_minecraft(
Ok(res.token)
}
#[derive(Debug, Error)]
pub enum MinecraftAuthError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
async fn auth_with_minecraft(
client: &reqwest::Client,
user_hash: &str,
xsts_token: &str,
) -> anyhow::Result<ExpiringValue<MinecraftAuthResponse>> {
) -> Result<ExpiringValue<MinecraftAuthResponse>, MinecraftAuthError> {
let res = client
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.header("Accept", "application/json")
@ -377,10 +428,16 @@ async fn auth_with_minecraft(
})
}
#[derive(Debug, Error)]
pub enum CheckOwnershipError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
async fn check_ownership(
client: &reqwest::Client,
minecraft_access_token: &str,
) -> anyhow::Result<bool> {
) -> Result<bool, CheckOwnershipError> {
let res = client
.get("https://api.minecraftservices.com/entitlements/mcstore")
.header(
@ -399,10 +456,16 @@ async fn check_ownership(
Ok(!res.items.is_empty())
}
#[derive(Debug, Error)]
pub enum GetProfileError {
#[error("Http error: {0}")]
Http(#[from] reqwest::Error),
}
async fn get_profile(
client: &reqwest::Client,
minecraft_access_token: &str,
) -> anyhow::Result<ProfileResponse> {
) -> Result<ProfileResponse, GetProfileError> {
let res = client
.get("https://api.minecraftservices.com/minecraft/profile")
.header(

View file

@ -46,13 +46,15 @@ pub async fn join(
let mut encode_buffer = Uuid::encode_buffer();
let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
let data = json!({
"accessToken": access_token,
"selectedProfile": undashed_uuid,
"serverId": server_hash
});
println!("data: {:?}", data);
let res = client
.post("https://sessionserver.mojang.com/session/minecraft/join")
.json(&json!({
"accessToken": access_token,
"selectedProfile": undashed_uuid,
"serverId": server_hash
}))
.json(&data)
.send()
.await?;

View file

@ -1,8 +1,9 @@
//! Connect to Minecraft servers.
use crate::{client::JoinError, Client, Event};
use crate::{client::JoinError, get_mc_dir, Client, Event};
use azalea_protocol::ServerAddress;
use tokio::sync::mpsc::UnboundedReceiver;
use uuid::Uuid;
/// Something that can join Minecraft servers.
pub struct Account {
@ -22,6 +23,23 @@ impl Account {
}
}
pub async fn microsoft(email: &str) -> Result<Self, azalea_auth::AuthError> {
let minecraft_dir = get_mc_dir::minecraft_dir().unwrap();
let auth_result = azalea_auth::auth(
email,
azalea_auth::AuthOpts {
cache_file: Some(minecraft_dir.join("azalea-auth.json")),
..Default::default()
},
)
.await?;
Ok(Self {
username: auth_result.profile.name,
access_token: Some(auth_result.access_token),
uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")),
})
}
/// Joins the Minecraft server on the given address using this account.
pub async fn join(
&self,

View file

@ -166,13 +166,12 @@ impl Client {
&account
.uuid
.expect("Uuid must be present if access token is present."),
e.secret_key,
p,
)
.await?;
}
// TODO: authenticate with the server here (authenticateServer)
conn.write(
ServerboundKeyPacket {
nonce_or_salt_signature: NonceOrSaltSignature::Nonce(
@ -183,6 +182,7 @@ impl Client {
.get(),
)
.await?;
conn.set_encryption_key(e.secret_key);
}
ClientboundLoginPacket::LoginCompression(p) => {

View file

@ -0,0 +1,34 @@
//! Find out where the user's .minecraft directory is.
//!
//! Used for the auth cache.
use std::path::PathBuf;
/// Return the location of the user's .minecraft directory.
///
/// Windows: `%appdata%\.minecraft`\
/// Mac: `$HOME/Library/Application Support/minecraft`\
/// Linux: `$HOME/.minecraft`
///
/// Anywhere else it'll return None.
pub fn minecraft_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
let appdata = std::env::var("APPDATA").ok()?;
Some(PathBuf::from(appdata).join(".minecraft"))
}
#[cfg(target_os = "macos")]
{
let home = std::env::var("HOME").ok()?;
Some(PathBuf::from(home).join("Library/Application Support/minecraft"))
}
#[cfg(target_os = "linux")]
{
let home = std::env::var("HOME").ok()?;
Some(PathBuf::from(home).join(".minecraft"))
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
None
}
}

View file

@ -2,6 +2,7 @@
mod account;
mod client;
mod get_mc_dir;
mod movement;
pub mod ping;
mod player;

View file

@ -18,8 +18,8 @@ fn generate_secret_key() -> [u8; 16] {
pub fn digest_data(server_id: &[u8], public_key: &[u8], private_key: &[u8]) -> Vec<u8> {
let mut digest = Sha1::new();
digest.update(server_id);
digest.update(public_key);
digest.update(private_key);
digest.update(public_key);
digest.finalize().to_vec()
}

View file

@ -25,7 +25,6 @@ pub struct ReadConnection<R: ProtocolPacket> {
buffer: BytesMut,
compression_threshold: Option<u32>,
dec_cipher: Option<Aes128CfbDec>,
private_key: Option<[u8; 16]>,
_reading: PhantomData<R>,
}
@ -33,7 +32,6 @@ pub struct WriteConnection<W: ProtocolPacket> {
write_stream: OwnedWriteHalf,
compression_threshold: Option<u32>,
enc_cipher: Option<Aes128CfbEnc>,
private_key: Option<[u8; 16]>,
_writing: PhantomData<W>,
}
@ -120,14 +118,12 @@ impl Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> {
buffer: BytesMut::new(),
compression_threshold: None,
dec_cipher: None,
private_key: None,
_reading: PhantomData,
},
writer: WriteConnection {
write_stream,
compression_threshold: None,
enc_cipher: None,
private_key: None,
_writing: PhantomData,
},
})
@ -159,8 +155,6 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key);
self.reader.dec_cipher = Some(dec_cipher);
self.writer.enc_cipher = Some(enc_cipher);
self.reader.private_key = Some(key);
self.writer.private_key = Some(key);
}
pub fn game(self) -> Connection<ClientboundGamePacket, ServerboundGamePacket> {
@ -195,12 +189,13 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> {
&self,
access_token: &str,
uuid: &Uuid,
private_key: [u8; 16],
packet: ClientboundHelloPacket,
) -> Result<(), SessionServerError> {
azalea_auth::sessionserver::join(
access_token,
&packet.public_key,
&self.writer.private_key.expect("encryption key not set"),
&private_key,
uuid,
&packet.server_id,
)
@ -226,14 +221,12 @@ where
buffer: connection.reader.buffer,
compression_threshold: connection.reader.compression_threshold,
dec_cipher: connection.reader.dec_cipher,
private_key: connection.reader.private_key,
_reading: PhantomData,
},
writer: WriteConnection {
compression_threshold: connection.writer.compression_threshold,
write_stream: connection.writer.write_stream,
enc_cipher: connection.writer.enc_cipher,
private_key: connection.writer.private_key,
_writing: PhantomData,
},
}

View file

@ -7,10 +7,10 @@ use std::sync::Arc;
struct State {}
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
env_logger::init();
let account = Account::offline("bot");
let account = Account::microsoft("example@example.com").await?;
azalea::start(azalea::Options {
account,
@ -21,12 +21,14 @@ async fn main() {
})
.await
.unwrap();
}
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
if let Event::Tick = *event {
bot.jump();
}
Ok(())
}
async fn handle(bot: Client, event: Arc<Event>, _state: Arc<Mutex<State>>) -> anyhow::Result<()> {
// if let Event::Tick = *event {
// bot.jump();
// }
Ok(())
}