Add Authentication

This commit is contained in:
Snowiiii
2024-08-04 15:44:53 +02:00
parent 5dfebfafba
commit 41c4d23821
8 changed files with 1137 additions and 37 deletions

1000
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ Pumpkin is currently under heavy development.
- [x] Custom Icon
- [x] Custom Status (MOTD)
- Login
- [ ] Authentication
- [x] Authentication
- [x] Encryption
- [ ] Packet Compression
- Player Configuration
@@ -32,6 +32,8 @@ Pumpkin is currently under heavy development.
- [ ] Set Resource Pack
- [ ] Cookies
- World
- [x] World Joining
- [x] World Loading
- [ ] Chunk Loading
- [ ] World Generation
- [ ] World Borders

View File

@@ -1,3 +1,5 @@
use serde::Deserialize;
use crate::{bytebuf::ByteBuffer, ClientPacket, VarInt};
pub struct CLoginDisconnect<'a> {
@@ -81,11 +83,11 @@ impl<'a> CLoginSuccess<'a> {
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct Property {
name: String,
value: String,
is_signed: bool,
signature: Option<String>,
pub name: String,
pub value: String,
pub signature: String,
}
impl<'a> ClientPacket for CLoginSuccess<'a> {
@@ -97,8 +99,11 @@ impl<'a> ClientPacket for CLoginSuccess<'a> {
bytebuf.put_list::<Property>(self.properties, |p, v| {
p.put_string(&v.name);
p.put_string(&v.value);
p.put_bool(v.is_signed);
p.put_option(&v.signature, |p, v| p.put_string(v.as_str()))
// has signature ?
p.put_bool(true);
// option
p.put_bool(true);
p.put_string(&v.signature);
});
bytebuf.put_bool(self.strict_error_handling);
}

View File

@@ -11,21 +11,33 @@ pumpkin-registry = { path = "../pumpkin-registry"}
# config
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8.19"
pollster = "0.3.0"
rand = "0.8.5"
num-bigint = "0.4.6"
# encryption
rsa = "0.9.6"
rsa-der = "0.3.0"
flate2 = "1.0.30"
# authentication
reqwest = { version = "0.12.5", features = ["json"]}
sha1 = "0.10.6"
digest = "=0.11.0-pre.9"
thiserror = "1.0.63"
# icon loading
base64 = "0.22.1"
image = { version = "0.25", default-features = false, features = ["png"]}
# logging
simple_logger = "5.0.0"
log = "0.4"
@@ -33,8 +45,7 @@ log = "0.4"
mio = { version = "1.0.1", features = ["os-poll", "net"]}
crossbeam-channel = "0.5.13"
uuid = "1.10"
toml = "0.8.19"
uuid = { version = "1.10", features = ["serde"]}

View File

@@ -0,0 +1,62 @@
use std::{error, net::IpAddr};
use num_bigint::BigInt;
use pumpkin_protocol::client::login::Property;
use reqwest::StatusCode;
use serde::Deserialize;
use thiserror::Error;
use uuid::Uuid;
use crate::server::Server;
#[derive(Deserialize, Clone)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<Property>,
}
pub async fn authenticate(
username: &str,
server_hash: &str,
ip: &IpAddr,
server: &mut Server,
) -> Result<GameProfile, AuthError> {
assert!(server.auth_client.is_some());
let address = if server.base_config.prevent_proxy_connections {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}")
} else {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}")
};
let response = server
.auth_client
.as_ref()
.unwrap()
.get(address)
.send()
.await
.map_err(|_| AuthError::FailedResponse)?;
match response.status() {
StatusCode::OK => {}
StatusCode::NO_CONTENT => Err(AuthError::UnverifiedUsername)?,
other => Err(AuthError::UnknownStatusCode(other.as_str().to_string()))?,
}
let profile: GameProfile = response.json().await.map_err(|_| AuthError::FailedParse)?;
Ok(profile)
}
pub fn auth_digest(bytes: &[u8]) -> String {
BigInt::from_signed_bytes_be(bytes).to_str_radix(16)
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Authentication servers are down")]
FailedResponse,
#[error("Failed to verify username")]
UnverifiedUsername,
#[error("Failed to parse JSON into Game Profile")]
FailedParse,
#[error("Unknown Status Code")]
UnknownStatusCode(String),
}

View File

@@ -13,13 +13,16 @@ use pumpkin_protocol::{
ConnectionState, KnownPack,
};
use pumpkin_registry::Registry;
use rsa::Pkcs1v15Encrypt;
use sha1::{Digest, Sha1};
use crate::{
client::authentication::{self, GameProfile},
entity::player::{ChatMode, Hand},
server::Server,
};
use super::{Client, PlayerConfig};
use super::{authentication::auth_digest, Client, EncryptionError, PlayerConfig};
/// Processes incoming Packets from the Client to the Server
/// Implements the `Client` Packets, So everything before the Play state, then will use the `PlayerPacketProcessor`
@@ -84,15 +87,21 @@ impl ClientPacketProcessor for Client {
fn handle_login_start(&mut self, server: &mut Server, login_start: SLoginStart) {
dbg!("login start");
self.name = Some(login_start.name);
self.uuid = Some(login_start.uuid);
// default game profile, when no online mode
self.gameprofile = Some(GameProfile {
id: login_start.uuid,
name: login_start.name,
properties: vec![],
});
// we want encryption
let verify_token: [u8; 4] = rand::random();
let public_key_der = &server.public_key_der;
let packet = CEncryptionRequest::new(
"",
public_key_der,
&verify_token,
false, // TODO
server.base_config.online_mode, // TODO
);
self.send_packet(packet)
.unwrap_or_else(|e| self.kick(&e.to_string()));
@@ -103,20 +112,39 @@ impl ClientPacketProcessor for Client {
server: &mut Server,
encryption_response: SEncryptionResponse,
) {
dbg!("encryption response");
self.enable_encryption(server, encryption_response.shared_secret)
let shared_secret = server
.private_key
.decrypt(Pkcs1v15Encrypt, &encryption_response.shared_secret)
.map_err(|_| EncryptionError::FailedDecrypt)
.unwrap();
if server.base_config.online_mode {
let hash = Sha1::new()
.chain_update(&shared_secret)
.chain_update(&server.public_key_der)
.finalize();
let hash = auth_digest(&hash);
let ip = self.address.ip();
match pollster::block_on(authentication::authenticate(
&self.gameprofile.as_ref().unwrap().name,
&hash,
&ip,
server,
)) {
Ok(p) => self.gameprofile = Some(p),
Err(e) => self.kick(&e.to_string()),
}
}
self.enable_encryption(server, shared_secret)
.unwrap_or_else(|e| self.kick(&e.to_string()));
if let Some(uuid) = self.uuid {
if let Some(name) = &self.name {
let packet = CLoginSuccess::new(uuid, name.clone(), &[], false); // todo
self.send_packet(packet)
.unwrap_or_else(|e| self.kick(&e.to_string()));
} else {
self.kick("Name is none");
}
if let Some(profile) = self.gameprofile.as_ref().cloned() {
let packet = CLoginSuccess::new(profile.id, profile.name, &profile.properties, false);
self.send_packet(packet)
.unwrap_or_else(|e| self.kick(&e.to_string()));
} else {
self.kick("UUID is none");
self.kick("game profile is none");
}
}

View File

@@ -1,6 +1,7 @@
use std::{
collections::VecDeque,
io::{self, Write},
net::SocketAddr,
rc::Rc,
};
@@ -9,6 +10,7 @@ use crate::{
server::Server,
};
use authentication::GameProfile;
use mio::{event::Event, net::TcpStream, Token};
use player_packet::PlayerPacketProcessor;
use pumpkin_protocol::{
@@ -34,6 +36,7 @@ use rsa::Pkcs1v15Encrypt;
use std::io::Read;
use thiserror::Error;
mod authentication;
mod client_packet;
pub mod player_packet;
@@ -53,8 +56,8 @@ pub struct PlayerConfig {
pub struct Client {
pub player: Option<Player>,
pub name: Option<String>,
pub uuid: Option<uuid::Uuid>,
pub gameprofile: Option<GameProfile>,
pub config: Option<PlayerConfig>,
pub brand: Option<String>,
@@ -63,19 +66,20 @@ pub struct Client {
pub closed: bool,
pub token: Rc<Token>,
pub connection: TcpStream,
pub address: SocketAddr,
enc: PacketEncoder,
dec: PacketDecoder,
pub client_packets_queue: VecDeque<RawPacket>,
}
impl Client {
pub fn new(token: Rc<Token>, connection: TcpStream) -> Self {
pub fn new(token: Rc<Token>, connection: TcpStream, address: SocketAddr) -> Self {
Self {
name: None,
uuid: None,
gameprofile: None,
config: None,
brand: None,
token,
address,
player: None,
connection_state: ConnectionState::HandShake,
connection,
@@ -96,7 +100,7 @@ impl Client {
pub fn enable_encryption(
&mut self,
server: &mut Server,
shared_secret: Vec<u8>,
shared_secret: Vec<u8>, // decrypted
) -> Result<(), EncryptionError> {
self.encrytion = true;
let shared_secret = server

View File

@@ -95,7 +95,7 @@ fn main() -> io::Result<()> {
Interest::READABLE.add(Interest::WRITABLE),
)?;
connections.insert(token, Client::new(Rc::new(token), connection));
connections.insert(token, Client::new(Rc::new(token), connection, addr));
},
token => {