diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 6f7442572..d13d4bda6 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -3,7 +3,6 @@ use tracing::warn; use valence::client::despawn_disconnected_clients; use valence::client::misc::CommandExecution; -use valence::entity::player::PlayerEntityBundle; use valence::prelude::*; const SPAWN_Y: i32 = 64; @@ -46,20 +45,15 @@ fn setup( } fn init_clients( - mut clients: Query<(Entity, &UniqueId, &mut Client, &mut GameMode), Added>, + mut clients: Query<(&mut Client, &mut Location, &mut Position, &mut GameMode), Added>, instances: Query>, - mut commands: Commands, ) { - for (entity, uuid, mut client, mut game_mode) in &mut clients { + for (mut client, mut loc, mut pos, mut game_mode) in &mut clients { *game_mode = GameMode::Creative; - client.send_message("Welcome to Valence! Say something.".italic()); + loc.0 = instances.single(); + pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - commands.entity(entity).insert(PlayerEntityBundle { - location: Location(instances.single()), - position: Position::new([0.0, SPAWN_Y as f64 + 1.0, 0.0]), - uuid: *uuid, - ..Default::default() - }); + client.send_message("Welcome to Valence! Say something.".italic()); } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 6b7045e32..b2f116140 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -29,7 +29,7 @@ mod tests; #[cfg(feature = "anvil")] pub use valence_anvil as anvil; #[cfg(feature = "chat")] -pub use valence_chat as secure_chat; +pub use valence_chat as chat; pub use valence_core::*; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; @@ -61,6 +61,8 @@ pub mod prelude { pub use biome::{Biome, BiomeId, BiomeRegistry}; pub use block::{BlockKind, BlockState, PropName, PropValue}; pub use block_pos::BlockPos; + #[cfg(feature = "chat")] + pub use chat::chat_type::{ChatType, ChatTypeRegistry}; pub use chunk_pos::{ChunkPos, ChunkView}; pub use client::action::*; pub use client::command::*; @@ -98,8 +100,6 @@ pub mod prelude { pub use packet::s2c::play::particle::Particle; #[cfg(feature = "player_list")] pub use player_list::{PlayerList, PlayerListEntry}; - #[cfg(feature = "chat")] - pub use secure_chat::chat_type::{ChatType, ChatTypeRegistry}; pub use text::{Color, Text, TextFormat}; #[cfg(feature = "advancement")] pub use valence_advancement::{ @@ -163,7 +163,7 @@ impl PluginGroup for DefaultPlugins { #[cfg(feature = "chat")] { - group = group.add(valence_chat::SecureChatPlugin); + group = group.add(valence_chat::ChatPlugin); } group diff --git a/crates/valence_chat/README.md b/crates/valence_chat/README.md index b26f0bb3e..c814f8553 100644 --- a/crates/valence_chat/README.md +++ b/crates/valence_chat/README.md @@ -4,7 +4,5 @@ Provides support for cryptographically verified chat messaging on the server. This crate contains the secure chat plugin as well as chat types and the chat type registry. Minecraft's default chat types are added to the registry by default. Chat types contain information about how chat is styled, such as the chat color. -### **NOTE:** -- Modifying the chat type registry after the server has started can -break invariants within instances and clients! Make sure there are no -instances or clients spawned before mutating. \ No newline at end of file + +This crate also contains the `yggdrasil_session_pubkey.der` file which is an encoded format of Mojang's public key. This is necessary to verify the integrity of our clients' public session key, which is used for validating chat messages. In reality Mojang's key should never change in order to maintain backwards compatibility with older versions, but if it does it can be extracted from any minecraft server jar. \ No newline at end of file diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index ca3e250ef..0c6d05f92 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -22,6 +22,7 @@ pub mod chat_type; use std::collections::VecDeque; use std::time::SystemTime; +use anyhow::bail; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use chat_type::ChatTypePlugin; @@ -30,7 +31,7 @@ use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; use rustc_hash::{FxHashMap, FxHashSet}; use sha1::{Digest, Sha1}; use sha2::Sha256; -use tracing::{info, trace, warn}; +use tracing::{info, warn}; use uuid::Uuid; use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; use valence_client::settings::ClientSettings; @@ -51,7 +52,33 @@ use valence_core::translation_key::{ use valence_core::uuid::UniqueId; use valence_player_list::{ChatSession, PlayerListEntry}; -const MOJANG_KEY_DATA: &[u8] = include_bytes!("../../../assets/yggdrasil_session_pubkey.der"); +const MOJANG_KEY_DATA: &[u8] = include_bytes!("../yggdrasil_session_pubkey.der"); + +pub struct ChatPlugin; + +impl Plugin for ChatPlugin { + fn build(&self, app: &mut bevy_app::App) { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + app.add_plugin(ChatTypePlugin) + .insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_systems( + ( + init_chat_states, + handle_session_events + .after(init_chat_states) + .before(handle_message_events), + handle_message_acknowledgement + .after(init_chat_states) + .before(handle_message_events), + handle_message_events.after(init_chat_states), + ) + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ); + } +} #[derive(Resource)] struct MojangServicesState { @@ -149,10 +176,9 @@ impl AcknowledgementValidator { &mut self, acknowledgements: &[u8; 3], message_index: i32, - ) -> Option> { + ) -> anyhow::Result> { if !self.remove_until(message_index) { - // Invalid message index - return None; + bail!("Invalid message index"); } let acknowledged_count = { @@ -164,15 +190,13 @@ impl AcknowledgementValidator { }; if acknowledged_count > 20 { - // Too many message acknowledgements, protocol error? - return None; + bail!("Too many message acknowledgements, protocol error?"); } let mut list = VecDeque::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; - // SAFETY: The length of messages is never less than 20 - let acknowledged_message = unsafe { self.messages.get_unchecked_mut(i) }; + let acknowledged_message = &mut self.messages[i]; // Client has acknowledged the i-th message if acknowledgement { // The validator has the i-th message @@ -181,27 +205,23 @@ impl AcknowledgementValidator { list.push_back(m.signature); } else { // Client has acknowledged a non-existing message - trace!("Client has acknowledged a non-existing message"); - return None; + bail!("Client has acknowledged a non-existing message"); } } else { // Client has not acknowledged the i-th message if matches!(acknowledged_message, Some(m) if !m.pending) { // The validator has an i-th message that has been validated but the client // claims that it hasn't been validated yet - trace!( + bail!( "The validator has an i-th message that has been validated but the client \ claims that it hasn't been validated yet" ); - return None; } // Honestly not entirely sure why this is done - if acknowledged_message.is_some() { - *acknowledged_message = None; - } + *acknowledged_message = None; } } - Some(list) + Ok(list) } /// The number of pending messages in the validator. @@ -310,32 +330,6 @@ impl MessageSignatureStorage { } } -pub struct SecureChatPlugin; - -impl Plugin for SecureChatPlugin { - fn build(&self, app: &mut bevy_app::App) { - let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) - .expect("Error creating Mojang public key"); - - app.add_plugin(ChatTypePlugin) - .insert_resource(MojangServicesState::new(mojang_pub_key)) - .add_systems( - ( - init_chat_states, - handle_session_events - .after(init_chat_states) - .before(handle_message_events), - handle_message_acknowledgement - .after(init_chat_states) - .before(handle_message_events), - handle_message_events.after(init_chat_states), - ) - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), - ); - } -} - fn init_chat_states(clients: Query>, mut commands: Commands) { for entity in clients.iter() { commands.entity(entity).insert(ChatState::default()); @@ -369,15 +363,11 @@ fn handle_session_events( continue; } - // Serialize the session data. - let mut serialized = Vec::with_capacity(318); - serialized.extend_from_slice(uuid.0.into_bytes().as_slice()); - serialized.extend_from_slice(session.session_data.expires_at.to_be_bytes().as_ref()); - serialized.extend_from_slice(session.session_data.public_key_data.as_ref()); - // Hash the session data using the SHA-1 algorithm. let mut hasher = Sha1::new(); - hasher.update(&serialized); + hasher.update(uuid.0.into_bytes()); + hasher.update(session.session_data.expires_at.to_be_bytes()); + hasher.update(&session.session_data.public_key_data); let hash = hasher.finalize(); // Verify the session data using Mojang's public key and the hashed session data @@ -507,15 +497,18 @@ fn handle_message_events( .validator .validate(&message.acknowledgements, message.message_index) { - None => { - warn!("Failed to validate acknowledgements from `{}`", username.0); + Err(error) => { + warn!( + "Failed to validate acknowledgements from `{}`: {}", + username.0, error + ); commands.add(DisconnectClient { client: message.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; } - Some(last_seen) => last_seen, + Ok(last_seen) => last_seen, }; let Some(link) = &state.chain.next_link() else { diff --git a/assets/yggdrasil_session_pubkey.der b/crates/valence_chat/yggdrasil_session_pubkey.der similarity index 100% rename from assets/yggdrasil_session_pubkey.der rename to crates/valence_chat/yggdrasil_session_pubkey.der