Don't tamper with the texture payload

The texture payload (skin + cape URL) that we get from the auth server
needs to stay intact so the signature is not invalidated. However, skins
still aren't loading. Not sure what's up with that.
This commit is contained in:
Ryan 2022-06-28 18:29:29 -07:00
parent 055dd03ffc
commit e97df76a75
8 changed files with 153 additions and 123 deletions

View file

@ -97,6 +97,15 @@ impl Config for Game {
if client.created_tick() == server.current_tick() {
client.set_game_mode(GameMode::Creative);
client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
world.meta.player_list_mut().insert(
client.uuid(),
client.username().to_string(),
client.textures().cloned(),
client.game_mode(),
0,
None,
);
}
if client.is_disconnected() {

View file

@ -54,7 +54,7 @@ impl Config for Game {
fn online_mode(&self) -> bool {
// You'll want this to be true on real servers.
true
false
}
async fn server_list_ping(&self, _server: &Server, _remote_addr: SocketAddr) -> ServerListPing {
@ -105,12 +105,11 @@ impl Config for Game {
client.set_max_view_distance(32);
client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
world.meta.player_list_mut().add_player(
world.meta.player_list_mut().insert(
client.uuid(),
client.username().to_string(),
None,
None,
GameMode::Creative,
client.textures().cloned(),
client.game_mode(),
0,
None,
);

View file

@ -25,14 +25,15 @@ use crate::packets::play::s2c::{
PlayerPositionAndLook, PlayerPositionAndLookFlags, RegistryCodec, S2cPlayPacket, SpawnPosition,
UnloadChunk, UpdateViewDistance, UpdateViewPosition,
};
use crate::player_textures::SignedPlayerTextures;
use crate::protocol::{BoundedInt, Nbt};
use crate::server::C2sPacketChannels;
use crate::slotmap::{Key, SlotMap};
use crate::util::{chunks_in_view_distance, is_chunk_in_view_distance};
use crate::var_int::VarInt;
use crate::{
ident, ChunkPos, Chunks, DimensionId, Entities, EntityId, Server, SpatialIndex, Text, Ticks,
WorldMeta, LIBRARY_NAMESPACE,
ident, ChunkPos, Chunks, DimensionId, Entities, EntityId, NewClientData, Server, SpatialIndex,
Text, Ticks, WorldMeta, LIBRARY_NAMESPACE,
};
pub struct Clients {
@ -120,8 +121,9 @@ pub struct Client {
recv: Receiver<C2sPlayPacket>,
/// The tick this client was created.
created_tick: Ticks,
username: String,
uuid: Uuid,
username: String,
textures: Option<SignedPlayerTextures>,
on_ground: bool,
new_position: Vec3<f64>,
old_position: Vec3<f64>,
@ -176,9 +178,8 @@ impl<'a> Deref for ClientMut<'a> {
impl Client {
pub(crate) fn new(
packet_channels: C2sPacketChannels,
username: String,
uuid: Uuid,
server: &Server,
ncd: NewClientData,
) -> Self {
let (send, recv) = packet_channels;
@ -186,8 +187,9 @@ impl Client {
send: Some(send),
recv,
created_tick: server.current_tick(),
username,
uuid,
uuid: ncd.uuid,
username: ncd.username,
textures: ncd.textures,
on_ground: false,
new_position: Vec3::default(),
old_position: Vec3::default(),
@ -218,12 +220,16 @@ impl Client {
self.created_tick
}
pub fn uuid(&self) -> Uuid {
self.uuid
}
pub fn username(&self) -> &str {
&self.username
}
pub fn uuid(&self) -> Uuid {
self.uuid
pub fn textures(&self) -> Option<&SignedPlayerTextures> {
self.textures.as_ref()
}
pub fn position(&self) -> Vec3<f64> {
@ -602,6 +608,9 @@ impl<'a> ClientMut<'a> {
// Send the join game packet and other initial packets. We defer this until now
// so that the user can set the client's location, game mode, etc.
if self.created_tick == current_tick {
meta.player_list()
.initial_packets(|pkt| self.send_packet(pkt));
self.send_packet(JoinGame {
entity_id: 0, // EntityId 0 is reserved for clients.
is_hardcore: false, // TODO
@ -621,7 +630,7 @@ impl<'a> ClientMut<'a> {
max_players: VarInt(0),
view_distance: BoundedInt(VarInt(self.new_max_view_distance as i32)),
simulation_distance: VarInt(16),
reduced_debug_info: false, // TODO
reduced_debug_info: false,
enable_respawn_screen: false,
is_debug: false,
is_flat: meta.is_flat(),
@ -630,9 +639,6 @@ impl<'a> ClientMut<'a> {
.map(|(id, pos)| (ident!("{LIBRARY_NAMESPACE}:dimension_{}", id.0), pos)),
});
meta.player_list()
.initial_packets(|pkt| self.send_packet(pkt));
self.teleport(self.position(), self.yaw(), self.pitch());
} else {
if self.0.old_game_mode != self.0.new_game_mode {

View file

@ -31,6 +31,7 @@ pub mod util;
mod var_int;
mod var_long;
pub mod world;
pub mod player_textures;
pub use async_trait::async_trait;
pub use biome::{Biome, BiomeId};

View file

@ -329,6 +329,16 @@ mod private {
pub trait Sealed {}
}
def_struct! {
#[derive(PartialEq, Serialize, Deserialize)]
Property {
name: String,
value: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>
}
}
/// Packets and types used during the handshaking state.
pub mod handshake {
use super::*;
@ -413,14 +423,6 @@ pub mod login {
}
}
def_struct! {
Property {
name: String,
value: String,
signature: Option<String>,
}
}
def_struct! {
SetCompression 0x03 {
threshold: VarInt
@ -474,7 +476,6 @@ pub mod play {
pub mod s2c {
use super::super::*;
use crate::packets::login::c2s::SignatureData;
use crate::packets::login::s2c::Property;
def_struct! {
SpawnEntity 0x00 {

View file

@ -3,15 +3,14 @@ use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use bitfield_struct::bitfield;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::client::GameMode;
use crate::packets::login::s2c::Property;
use crate::packets::play::s2c::{
PlayerInfo, PlayerInfoAddPlayer, PlayerListHeaderFooter, S2cPlayPacket,
};
use crate::packets::Property;
use crate::player_textures::SignedPlayerTextures;
use crate::var_int::VarInt;
use crate::Text;
@ -55,16 +54,11 @@ impl PlayerList {
username: e.username.clone().into(),
properties: {
let mut properties = Vec::new();
if e.skin().is_some() || e.cape().is_some() {
let textures = PlayerTextures {
skin: e.skin().cloned().map(TextureUrl::new),
cape: e.cape().cloned().map(TextureUrl::new),
};
if let Some(textures) = &e.textures {
properties.push(Property {
name: "textures".into(),
value: base64::encode(serde_json::to_string(&textures).unwrap()),
signature: None,
value: base64::encode(textures.payload()),
signature: Some(base64::encode(textures.signature())),
});
}
properties
@ -104,16 +98,11 @@ impl PlayerList {
for (&uuid, e) in self.entries.iter() {
if e.flags.created_this_tick() {
let mut properties = Vec::new();
if e.skin().is_some() || e.cape().is_some() {
let textures = PlayerTextures {
skin: e.skin().cloned().map(TextureUrl::new),
cape: e.cape().cloned().map(TextureUrl::new),
};
if let Some(textures) = &e.textures {
properties.push(Property {
name: "textures".into(),
value: base64::encode(serde_json::to_string(&textures).unwrap()),
signature: None,
value: base64::encode(textures.payload()),
signature: Some(base64::encode(textures.signature())),
});
}
@ -186,51 +175,41 @@ impl<'a> PlayerListMut<'a> {
PlayerListMut(self.0)
}
pub fn add_player(
pub fn insert(
&mut self,
uuid: Uuid,
username: impl Into<String>,
skin: Option<Url>,
cape: Option<Url>,
textures: Option<SignedPlayerTextures>,
game_mode: GameMode,
ping: i32,
display_name: impl Into<Option<Text>>,
) {
match self.0.entries.entry(uuid) {
Entry::Occupied(mut oe) => {
let mut entry = PlayerListEntryMut(oe.get_mut());
let mut e = PlayerListEntryMut(oe.get_mut());
let username = username.into();
if entry.username() != username
|| entry.skin() != skin.as_ref()
|| entry.cape() != cape.as_ref()
{
if e.username() != username || e.textures != textures {
self.0.removed.insert(*oe.key());
oe.insert(PlayerListEntry {
username,
textures: PlayerTextures {
skin: skin.map(TextureUrl::new),
cape: cape.map(TextureUrl::new),
},
textures,
game_mode,
ping,
display_name: display_name.into(),
flags: EntryFlags::new().with_created_this_tick(true),
});
} else {
entry.set_game_mode(game_mode);
entry.set_ping(ping);
entry.set_display_name(display_name);
e.set_game_mode(game_mode);
e.set_ping(ping);
e.set_display_name(display_name);
}
}
Entry::Vacant(ve) => {
ve.insert(PlayerListEntry {
username: username.into(),
textures: PlayerTextures {
skin: skin.map(TextureUrl::new),
cape: cape.map(TextureUrl::new),
},
textures,
game_mode,
ping,
display_name: display_name.into(),
@ -240,7 +219,7 @@ impl<'a> PlayerListMut<'a> {
}
}
pub fn remove_player(&mut self, uuid: Uuid) -> bool {
pub fn remove(&mut self, uuid: Uuid) -> bool {
if self.0.entries.remove(&uuid).is_some() {
self.0.removed.insert(uuid);
true
@ -283,7 +262,7 @@ impl<'a> PlayerListMut<'a> {
pub struct PlayerListEntry {
username: String,
textures: PlayerTextures,
textures: Option<SignedPlayerTextures>,
game_mode: GameMode,
ping: i32,
display_name: Option<Text>,
@ -295,12 +274,8 @@ impl PlayerListEntry {
&self.username
}
pub fn skin(&self) -> Option<&Url> {
self.textures.skin.as_ref().map(|t| &t.url)
}
pub fn cape(&self) -> Option<&Url> {
self.textures.cape.as_ref().map(|t| &t.url)
pub fn textures(&self) -> Option<&SignedPlayerTextures> {
self.textures.as_ref()
}
pub fn game_mode(&self) -> GameMode {
@ -363,23 +338,3 @@ struct EntryFlags {
#[bits(4)]
_pad: u8,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub(crate) struct PlayerTextures {
#[serde(default, skip_serializing_if = "Option::is_none")]
skin: Option<TextureUrl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cape: Option<TextureUrl>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
struct TextureUrl {
url: Url,
}
impl TextureUrl {
fn new(url: Url) -> Self {
Self { url }
}
}

76
src/player_textures.rs Normal file
View file

@ -0,0 +1,76 @@
use anyhow::Context;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, PartialEq, Debug)]
pub struct SignedPlayerTextures {
payload: Box<[u8]>,
signature: Box<[u8]>,
}
impl SignedPlayerTextures {
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn signature(&self) -> &[u8] {
&self.signature
}
pub fn to_textures(&self) -> PlayerTextures {
self.to_textures_fallible()
.expect("payload should have been validated earlier")
}
fn to_textures_fallible(&self) -> anyhow::Result<PlayerTextures> {
#[derive(Debug, Deserialize)]
struct Textures {
textures: PlayerTexturesPayload,
}
let textures: Textures = serde_json::from_slice(&self.payload)?;
Ok(PlayerTextures {
skin: textures.textures.skin.map(|t| t.url),
cape: textures.textures.cape.map(|t| t.url),
})
}
pub(crate) fn from_base64(payload: String, signature: String) -> anyhow::Result<Self> {
let res = Self {
payload: base64::decode(payload)?.into_boxed_slice(),
signature: base64::decode(signature)?.into_boxed_slice(),
};
match res.to_textures_fallible() {
Ok(_) => Ok(res),
Err(e) => Err(e).context("failed to parse textures payload"),
}
}
}
#[derive(Clone, PartialEq, Default, Debug)]
pub struct PlayerTextures {
pub skin: Option<Url>,
pub cape: Option<Url>,
}
impl From<SignedPlayerTextures> for PlayerTextures {
fn from(spt: SignedPlayerTextures) -> Self {
spt.to_textures()
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct PlayerTexturesPayload {
#[serde(default, skip_serializing_if = "Option::is_none")]
skin: Option<TextureUrl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cape: Option<TextureUrl>,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
struct TextureUrl {
url: Url,
}

View file

@ -29,14 +29,14 @@ use uuid::Uuid;
use crate::codec::{Decoder, Encoder};
use crate::config::{Config, ServerListPing};
use crate::packets::handshake::{Handshake, HandshakeNextState};
use crate::packets::login;
use crate::packets::login::c2s::{EncryptionResponse, LoginStart, VerifyTokenOrMsgSig};
use crate::packets::login::s2c::{EncryptionRequest, LoginSuccess, SetCompression};
use crate::packets::play::c2s::C2sPlayPacket;
use crate::packets::play::s2c::S2cPlayPacket;
use crate::packets::status::c2s::{Ping, Request};
use crate::packets::status::s2c::{Pong, Response};
use crate::player_list::PlayerTextures;
use crate::packets::{login, Property};
use crate::player_textures::SignedPlayerTextures;
use crate::protocol::{BoundedArray, BoundedString};
use crate::util::valid_username;
use crate::var_int::VarInt;
@ -90,6 +90,7 @@ struct ServerInner {
pub struct NewClientData {
pub uuid: Uuid,
pub username: String,
pub textures: Option<SignedPlayerTextures>,
pub remote_addr: SocketAddr,
}
@ -414,17 +415,12 @@ fn join_player(server: &Server, mut worlds: WorldsMut, msg: NewClientMessage) {
let (clientbound_tx, clientbound_rx) = flume::bounded(server.0.outgoing_packet_capacity);
let (serverbound_tx, serverbound_rx) = flume::bounded(server.0.incoming_packet_capacity);
let client_packet_channels: S2cPacketChannels = (serverbound_tx, clientbound_rx);
let server_packet_channels: C2sPacketChannels = (clientbound_tx, serverbound_rx);
let s2c_packet_channels: S2cPacketChannels = (serverbound_tx, clientbound_rx);
let c2s_packet_channels: C2sPacketChannels = (clientbound_tx, serverbound_rx);
let _ = msg.reply.send(client_packet_channels);
let _ = msg.reply.send(s2c_packet_channels);
let mut client = Client::new(
server_packet_channels,
msg.ncd.username,
msg.ncd.uuid,
server,
);
let mut client = Client::new(c2s_packet_channels, server, msg.ncd);
let mut client_mut = ClientMut::new(&mut client);
match server
@ -641,12 +637,6 @@ async fn handle_login(
properties: Vec<Property>,
}
#[derive(Debug, Deserialize)]
struct Property {
name: String,
value: String,
}
let hash = Sha1::new()
.chain(&shared_secret)
.chain(&server.0.public_key_der)
@ -670,8 +660,11 @@ async fn handle_login(
let uuid = Uuid::parse_str(&data.id).context("failed to parse player's UUID")?;
let textures = match data.properties.into_iter().find(|p| p.name == "textures") {
Some(p) => decode_textures(p.value).context("failed to decode skin blob")?,
None => bail!("failed to find skin blob in auth response"),
Some(p) => SignedPlayerTextures::from_base64(
p.value,
p.signature.context("missing signature for textures")?,
)?,
None => bail!("failed to find textures in auth response"),
};
(uuid, Some(textures))
@ -694,6 +687,7 @@ async fn handle_login(
let npd = NewClientData {
uuid,
username,
textures,
remote_addr,
};
@ -713,17 +707,6 @@ async fn handle_login(
Ok(Some(npd))
}
fn decode_textures(encoded: String) -> anyhow::Result<PlayerTextures> {
#[derive(Debug, Deserialize)]
struct Textures {
textures: PlayerTextures,
}
let bytes = base64::decode(encoded)?;
let textures: Textures = serde_json::from_slice(&bytes)?;
Ok(textures.textures)
}
async fn handle_play(server: &Server, c: Codec, ncd: NewClientData) -> anyhow::Result<()> {
let (reply_tx, reply_rx) = oneshot::channel();