From 59391efd9bce171f3c3ac4a03e72680273e50401 Mon Sep 17 00:00:00 2001 From: Guac Date: Mon, 25 Sep 2023 10:38:50 -0600 Subject: [PATCH] Transfer Some minor changes to retain before updating --- crates/README.md | 1 + crates/valence/Cargo.toml | 5 +- crates/valence/examples/block_entities.rs | 3 +- crates/valence/examples/chat.rs | 22 +- crates/valence_chat/Cargo.toml | 13 +- crates/valence_chat/src/chat_type.rs | 34 +- crates/valence_chat/src/lib.rs | 444 +++++++++++++++------- crates/valence_client/Cargo.toml | 4 + crates/valence_client/src/chat.rs | 32 ++ crates/valence_client/src/lib.rs | 2 + crates/valence_client/src/misc.rs | 69 +--- 11 files changed, 390 insertions(+), 239 deletions(-) create mode 100644 crates/valence_client/src/chat.rs diff --git a/crates/README.md b/crates/README.md index f17011ef7..9aa54c553 100644 --- a/crates/README.md +++ b/crates/README.md @@ -22,4 +22,5 @@ graph TD anvil --> instance entity --> block advancement --> client + chat --> client ``` diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index 995cb3f07..91507b616 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,13 +11,14 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil", "advancement", "chat"] +default = ["network", "player_list", "inventory", "anvil", "advancement", "secure_chat"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] advancement = ["dep:valence_advancement"] -chat = ["dep:valence_chat"] +chat = ["dep:valence_chat", "valence_client/chat"] +secure_chat = ["chat", "valence_chat?/secure", "valence_client/secure_chat"] [dependencies] bevy_app.workspace = true diff --git a/crates/valence/examples/block_entities.rs b/crates/valence/examples/block_entities.rs index b94a89d75..5974c1f0a 100644 --- a/crates/valence/examples/block_entities.rs +++ b/crates/valence/examples/block_entities.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] -use valence::client::misc::{ChatMessage, InteractBlock}; +use valence::client::chat::ChatMessage; +use valence::client::misc::InteractBlock; use valence::nbt::{compound, List}; use valence::prelude::*; diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index d13d4bda6..a11c94f82 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -1,8 +1,9 @@ #![allow(clippy::type_complexity)] use tracing::warn; +use valence::chat::ChatState; +use valence::client::chat::{ChatMessage, CommandExecution}; use valence::client::despawn_disconnected_clients; -use valence::client::misc::CommandExecution; use valence::prelude::*; const SPAWN_Y: i32 = 64; @@ -16,6 +17,7 @@ pub fn main() { .add_systems(( init_clients, despawn_disconnected_clients, + handle_message_events.in_schedule(EventLoopSchedule), handle_command_events.in_schedule(EventLoopSchedule), )) .run(); @@ -57,6 +59,24 @@ fn init_clients( } } +fn handle_message_events( + mut clients: Query<(&mut Client, &mut ChatState)>, + names: Query<&Username>, + mut messages: EventReader, +) { + for message in messages.iter() { + let sender_name = names.get(message.client).expect("Error getting username"); + // Need to find better way. Username is sender, while client and chat state are + // recievers. Maybe try to add a chat feature to Client. + for (mut client, mut state) in clients.iter_mut() { + state + .as_mut() + .send_chat_message(client.as_mut(), sender_name, message) + .expect("Error sending message"); + } + } +} + fn handle_command_events( mut clients: Query<&mut Client>, mut commands: EventReader, diff --git a/crates/valence_chat/Cargo.toml b/crates/valence_chat/Cargo.toml index 2ea10e7f7..ee66a3b51 100644 --- a/crates/valence_chat/Cargo.toml +++ b/crates/valence_chat/Cargo.toml @@ -3,17 +3,20 @@ name = "valence_chat" version.workspace = true edition.workspace = true +[features] +secure = ["dep:rsa", "dep:rustc-hash", "dep:sha1", "dep:sha2", "valence_client/secure_chat"] + [dependencies] anyhow.workspace = true bevy_app.workspace = true bevy_ecs.workspace = true -rsa.workspace = true -rustc-hash.workspace = true -sha1 = { workspace = true, features = ["oid"] } -sha2 = { workspace = true, features = ["oid"] } +rsa = { workspace = true, optional = true } +rustc-hash = { workspace = true, optional = true } +sha1 = { workspace = true, features = ["oid"], optional = true } +sha2 = { workspace = true, features = ["oid"], optional = true } tracing.workspace = true valence_core.workspace = true -valence_client.workspace = true +valence_client = { workspace = true, features = ["chat"], optional = true } valence_instance.workspace = true valence_nbt.workspace = true valence_player_list.workspace = true diff --git a/crates/valence_chat/src/chat_type.rs b/crates/valence_chat/src/chat_type.rs index 2222cd7e5..c03cb094e 100644 --- a/crates/valence_chat/src/chat_type.rs +++ b/crates/valence_chat/src/chat_type.rs @@ -18,6 +18,23 @@ use valence_core::text::Color; use valence_nbt::{compound, Compound, List, Value}; use valence_registry::{RegistryCodec, RegistryCodecSet, RegistryValue}; +pub(crate) struct ChatTypePlugin; + +impl Plugin for ChatTypePlugin { + fn build(&self, app: &mut bevy_app::App) { + app.insert_resource(ChatTypeRegistry { + id_to_chat_type: vec![], + }) + .add_systems( + (update_chat_type_registry, remove_chat_types_from_registry) + .chain() + .in_base_set(CoreSet::PostUpdate) + .before(RegistryCodecSet), + ) + .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); + } +} + #[derive(Resource)] pub struct ChatTypeRegistry { id_to_chat_type: Vec, @@ -119,23 +136,6 @@ pub struct ChatTypeParameters { content: bool, } -pub(crate) struct ChatTypePlugin; - -impl Plugin for ChatTypePlugin { - fn build(&self, app: &mut bevy_app::App) { - app.insert_resource(ChatTypeRegistry { - id_to_chat_type: vec![], - }) - .add_systems( - (update_chat_type_registry, remove_chat_types_from_registry) - .chain() - .in_base_set(CoreSet::PostUpdate) - .before(RegistryCodecSet), - ) - .add_startup_system(load_default_chat_types.in_base_set(StartupSet::PreStartup)); - } -} - fn load_default_chat_types( mut reg: ResMut, codec: Res, diff --git a/crates/valence_chat/src/lib.rs b/crates/valence_chat/src/lib.rs index 0c6d05f92..00ba27a8e 100644 --- a/crates/valence_chat/src/lib.rs +++ b/crates/valence_chat/src/lib.rs @@ -19,72 +19,101 @@ pub mod chat_type; +#[cfg(feature = "secure")] use std::collections::VecDeque; use std::time::SystemTime; -use anyhow::bail; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use chat_type::ChatTypePlugin; -use rsa::pkcs8::DecodePublicKey; -use rsa::{PaddingScheme, PublicKey, RsaPublicKey}; -use rustc_hash::{FxHashMap, FxHashSet}; -use sha1::{Digest, Sha1}; -use sha2::Sha256; -use tracing::{info, warn}; -use uuid::Uuid; -use valence_client::misc::{ChatMessage, MessageAcknowledgment, PlayerSession}; +use tracing::warn; +use valence_client::chat::{ChatMessage, CommandExecution}; +use valence_client::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent, RunEventLoopSet}; use valence_client::settings::ClientSettings; -use valence_client::{Client, DisconnectClient, FlushPacketsSet, Username}; +use valence_client::{Client, SpawnClientsSet}; use valence_core::packet::c2s::play::client_settings::ChatMode; +use valence_core::packet::c2s::play::{ChatMessageC2s, CommandExecutionC2s}; use valence_core::packet::encode::WritePacket; use valence_core::packet::message_signature::MessageSignature; use valence_core::packet::s2c::play::chat_message::MessageFilterType; -use valence_core::packet::s2c::play::ChatMessageS2c; +use valence_core::packet::s2c::play::{ChatMessageS2c, ProfilelessChatMessageS2c}; use valence_core::text::{Color, Text, TextFormat}; -use valence_core::translation_key::{ - CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, - CHAT_DISABLED_MISSING_PROFILE_KEY, CHAT_DISABLED_OPTIONS, - MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, - MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, - MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, +use valence_core::translation_key::{CHAT_DISABLED_OPTIONS, DISCONNECT_GENERIC_REASON}; +#[cfg(feature = "secure")] +use { + anyhow::bail, + rsa::pkcs8::DecodePublicKey, + rsa::{PaddingScheme, PublicKey, RsaPublicKey}, + rustc_hash::{FxHashMap, FxHashSet}, + sha1::{Digest, Sha1}, + sha2::Sha256, + uuid::Uuid, + valence_client::chat::ChatMessageType, + valence_client::{DisconnectClient, Username}, + valence_core::packet::c2s::play::{MessageAcknowledgmentC2s, PlayerSessionC2s}, + valence_core::translation_key::{ + CHAT_DISABLED_CHAIN_BROKEN, CHAT_DISABLED_EXPIRED_PROFILE_KEY, + CHAT_DISABLED_MISSING_PROFILE_KEY, MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, + MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, + MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, + MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, + }, + valence_core::uuid::UniqueId, + valence_player_list::{ChatSession, PlayerListEntry}, }; -use valence_core::uuid::UniqueId; -use valence_player_list::{ChatSession, PlayerListEntry}; +#[cfg(feature = "secure")] 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_event::() + .add_event::() + .add_system( + init_chat_states + .in_base_set(CoreSet::PreUpdate) + .after(SpawnClientsSet) + .before(RunEventLoopSet), + ) .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), + #[cfg(feature = "secure")] + handle_acknowledgement_packets, + #[cfg(not(feature = "secure"))] + handle_message_packets, + handle_command_packets, ) - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), + .in_base_set(EventLoopSet::PreUpdate) + .in_schedule(EventLoopSchedule), ); + + #[cfg(feature = "secure")] + { + let mojang_pub_key = RsaPublicKey::from_public_key_der(MOJANG_KEY_DATA) + .expect("Error creating Mojang public key"); + + app.insert_resource(MojangServicesState::new(mojang_pub_key)) + .add_systems( + (handle_session_packets, handle_message_packets) + .chain() + .in_base_set(EventLoopSet::PreUpdate) + .in_schedule(EventLoopSchedule), + ); + } } } +#[cfg(feature = "secure")] #[derive(Resource)] struct MojangServicesState { public_key: RsaPublicKey, } +#[cfg(feature = "secure")] impl MojangServicesState { fn new(public_key: RsaPublicKey) -> Self { Self { public_key } @@ -92,10 +121,13 @@ impl MojangServicesState { } #[derive(Debug, Component)] -struct ChatState { - last_message_timestamp: u64, +pub struct ChatState { + pub last_message_timestamp: u64, + #[cfg(feature = "secure")] validator: AcknowledgementValidator, + #[cfg(feature = "secure")] chain: MessageChain, + #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage, } @@ -106,29 +138,88 @@ impl Default for ChatState { .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() as u64, + #[cfg(feature = "secure")] validator: AcknowledgementValidator::new(), + #[cfg(feature = "secure")] chain: MessageChain::new(), + #[cfg(feature = "secure")] signature_storage: MessageSignatureStorage::new(), } } } impl ChatState { + pub fn send_chat_message( + &mut self, + client: &mut Client, + username: &Username, + message: &ChatMessage, + ) -> anyhow::Result<()> { + match &message.message_type { + ChatMessageType::Signed { + salt, + signature, + message_index, + last_seen, + sender, + } => { + // Create a list of messages that have been seen by the client. + let previous = last_seen + .iter() + .map(|sig| match self.signature_storage.index_of(sig) { + Some(index) => MessageSignature::ByIndex(index), + None => MessageSignature::BySignature(sig), + }) + .collect::>(); + + client.write_packet(&ChatMessageS2c { + sender: *sender, + index: (*message_index).into(), + message_signature: Some((*signature).as_ref()), + message: message.message.as_ref(), + time_stamp: message.timestamp, + salt: *salt, + previous_messages: previous, + unsigned_content: None, + filter_type: MessageFilterType::PassThrough, + chat_type: 0.into(), // TODO: Make chat type for player messages selectable + network_name: Text::from(username.0.clone()).into(), + network_target_name: None, + }); + // Add pending acknowledgement. + self.add_pending(last_seen, signature); + if self.validator.message_count() > 4096 { + warn!("User has too many pending chats `{}`", username.0); + bail!(MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS); + } + } + ChatMessageType::Unsigned => client.write_packet(&ProfilelessChatMessageS2c { + message: Text::from(message.message.to_string()).into(), + chat_type: 0.into(), + chat_type_name: Text::from(username.0.clone()).into(), + target_name: None, + }), + } + Ok(()) + } + /// Updates the chat state's previously seen signatures with a new one /// `signature`. - /// Warning this modifies `last_seen`. - fn add_pending(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: [u8; 256]) { - self.signature_storage.add(last_seen, &signature); - self.validator.add_pending(&signature); + #[cfg(feature = "secure")] + fn add_pending(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { + self.signature_storage.add(last_seen, signature); + self.validator.add_pending(signature); } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct AcknowledgementValidator { messages: Vec>, last_signature: Option<[u8; 256]>, } +#[cfg(feature = "secure")] impl AcknowledgementValidator { fn new() -> Self { Self { @@ -176,7 +267,7 @@ impl AcknowledgementValidator { &mut self, acknowledgements: &[u8; 3], message_index: i32, - ) -> anyhow::Result> { + ) -> anyhow::Result> { if !self.remove_until(message_index) { bail!("Invalid message index"); } @@ -193,7 +284,7 @@ impl AcknowledgementValidator { bail!("Too many message acknowledgements, protocol error?"); } - let mut list = VecDeque::with_capacity(acknowledged_count); + let mut list = Vec::with_capacity(acknowledged_count); for i in 0..20 { let acknowledgement = acknowledgements[i >> 3] & (0b1 << (i % 8)) != 0; let acknowledged_message = &mut self.messages[i]; @@ -202,7 +293,7 @@ impl AcknowledgementValidator { // The validator has the i-th message if let Some(m) = acknowledged_message { m.pending = false; - list.push_back(m.signature); + list.push(m.signature); } else { // Client has acknowledged a non-existing message bail!("Client has acknowledged a non-existing message"); @@ -230,17 +321,20 @@ impl AcknowledgementValidator { } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct AcknowledgedMessage { signature: [u8; 256], pending: bool, } +#[cfg(feature = "secure")] #[derive(Clone, Default, Debug)] struct MessageChain { link: Option, } +#[cfg(feature = "secure")] impl MessageChain { fn new() -> Self { Self::default() @@ -258,6 +352,7 @@ impl MessageChain { } } +#[cfg(feature = "secure")] #[derive(Copy, Clone, Debug)] struct MessageLink { index: i32, @@ -265,6 +360,7 @@ struct MessageLink { session_id: Uuid, } +#[cfg(feature = "secure")] impl MessageLink { fn update_hash(&self, hasher: &mut impl Digest) { hasher.update(self.sender.into_bytes()); @@ -273,12 +369,14 @@ impl MessageLink { } } +#[cfg(feature = "secure")] #[derive(Clone, Debug)] struct MessageSignatureStorage { signatures: [Option<[u8; 256]>; 128], indices: FxHashMap<[u8; 256], i32>, } +#[cfg(feature = "secure")] impl Default for MessageSignatureStorage { fn default() -> Self { Self { @@ -288,6 +386,7 @@ impl Default for MessageSignatureStorage { } } +#[cfg(feature = "secure")] impl MessageSignatureStorage { fn new() -> Self { Self::default() @@ -302,30 +401,39 @@ impl MessageSignatureStorage { /// `signature` to the storage. /// /// Warning: this consumes `last_seen`. - fn add(&mut self, last_seen: &mut VecDeque<[u8; 256]>, signature: &[u8; 256]) { - last_seen.push_back(*signature); + fn add(&mut self, last_seen: &Vec<[u8; 256]>, signature: &[u8; 256]) { let mut sig_set = FxHashSet::default(); - for sig in last_seen.iter() { - sig_set.insert(*sig); - } - for i in 0..128 { - if last_seen.is_empty() { + + last_seen + .iter() + .chain(std::iter::once(signature)) + .for_each(|sig| { + sig_set.insert(*sig); + }); + + let mut retained_sigs = VecDeque::new(); + let mut index = 0usize; + let mut seen_iter = last_seen.iter().chain(std::iter::once(signature)).rev(); + + while let Some(seen_sig) = seen_iter.next().or(retained_sigs.pop_front().as_ref()) { + if index > 127 { return; } - // Remove old message - let message_sig_data = self.signatures[i]; - // Add previously seen message - self.signatures[i] = last_seen.pop_back(); - if let Some(data) = self.signatures[i] { - self.indices.insert(data, i as i32); - } - // Reinsert old message if it is not already in last_seen - if let Some(data) = message_sig_data { + // Remove the old signature + let previous_sig = self.signatures[index]; + // Add the new signature + self.signatures[index] = Some(*seen_sig); + self.indices.insert(*seen_sig, index as i32); + // Reinsert old signature if it is not already in `last_seen` + if let Some(data) = previous_sig { + // Remove the index for the old sig self.indices.remove(&data); + // If the old sig is still unique, reinsert if sig_set.insert(data) { - last_seen.push_front(data); + retained_sigs.push_back(data); } } + index += 1; } } } @@ -336,14 +444,19 @@ fn init_chat_states(clients: Query>, mut commands: Command } } -fn handle_session_events( +#[cfg(feature = "secure")] +fn handle_session_packets( services_state: Res, mut clients: Query<(&UniqueId, &Username, &mut ChatState), With>, - mut sessions: EventReader, + mut packets: EventReader, mut commands: Commands, ) { - for session in sessions.iter() { - let Ok((uuid, username, mut state)) = clients.get_mut(session.client) else { + for packet in packets.iter() { + let Some(session) = packet.decode::() else { + continue; + }; + + let Ok((uuid, username, mut state)) = clients.get_mut(packet.client) else { warn!("Unable to find client in player list for session"); continue; }; @@ -353,11 +466,11 @@ fn handle_session_events( .duration_since(SystemTime::UNIX_EPOCH) .expect("Unable to get Unix time") .as_millis() - >= session.session_data.expires_at as u128 + >= session.0.expires_at as u128 { warn!("Failed to validate profile key: expired public key"); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, []), }); continue; @@ -366,8 +479,8 @@ fn handle_session_events( // Hash the session data using the SHA-1 algorithm. let mut hasher = Sha1::new(); hasher.update(uuid.0.into_bytes()); - hasher.update(session.session_data.expires_at.to_be_bytes()); - hasher.update(&session.session_data.public_key_data); + hasher.update(session.0.expires_at.to_be_bytes()); + hasher.update(&session.0.public_key_data); let hash = hasher.finalize(); // Verify the session data using Mojang's public key and the hashed session data @@ -377,33 +490,33 @@ fn handle_session_events( .verify( PaddingScheme::new_pkcs1v15_sign::(), &hash, - session.session_data.key_signature.as_ref(), + session.0.key_signature.as_ref(), ) .is_err() { warn!("Failed to validate profile key: invalid public key signature"); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, []), }); } // Decode the player's session public key from the data. if let Ok(public_key) = - RsaPublicKey::from_public_key_der(session.session_data.public_key_data.as_ref()) + RsaPublicKey::from_public_key_der(session.0.public_key_data.as_ref()) { // Update the player's chat state data with the new player session data. state.chain.link = Some(MessageLink { index: 0, sender: uuid.0, - session_id: session.session_data.session_id, + session_id: session.0.session_id, }); // Add the chat session data to player. // The player list will then send this new session data to the other clients. - commands.entity(session.client).insert(ChatSession { + commands.entity(packet.client).insert(ChatSession { public_key, - session_data: session.session_data.clone(), + session_data: session.0.into_owned(), }); } else { // This shouldn't happen considering that it is highly unlikely that Mojang @@ -411,9 +524,9 @@ fn handle_session_events( // key signature has been verified. warn!("Received malformed profile key data from '{}'", username.0); commands.add(DisconnectClient { - client: session.client, + client: packet.client, reason: Text::translate( - MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, + DISCONNECT_GENERIC_REASON, ["Malformed profile key data".color(Color::RED)], ), }); @@ -421,24 +534,32 @@ fn handle_session_events( } } -fn handle_message_acknowledgement( +#[cfg(feature = "secure")] +fn handle_acknowledgement_packets( mut clients: Query<(&Username, &mut ChatState)>, - mut acknowledgements: EventReader, + mut packets: EventReader, mut commands: Commands, ) { - for acknowledgement in acknowledgements.iter() { - let Ok((username, mut state)) = clients.get_mut(acknowledgement.client) else { + for packet in packets.iter() { + let Some(acknowledgement) = packet.decode::() else { + continue; + }; + + let Ok((username, mut state)) = clients.get_mut(packet.client) else { warn!("Unable to find client for acknowledgement"); continue; }; - if !state.validator.remove_until(acknowledgement.message_index) { + if !state + .validator + .remove_until(acknowledgement.message_index.0) + { warn!( "Failed to validate message acknowledgement from '{:?}'", username.0 ); commands.add(DisconnectClient { - client: acknowledgement.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; @@ -446,27 +567,24 @@ fn handle_message_acknowledgement( } } -fn handle_message_events( +#[cfg(feature = "secure")] +fn handle_message_packets( mut clients: Query< - (&mut Client, &mut ChatState, &Username, &ClientSettings), + (&mut ChatState, &mut Client, &Username, &ClientSettings), With, >, sessions: Query<&ChatSession, With>, - mut messages: EventReader, + mut packets: EventReader, + mut message_events: EventWriter, mut commands: Commands, ) { - for message in messages.iter() { - let Ok((mut client, mut state, username, settings)) = clients.get_mut(message.client) else { - warn!("Unable to find client for message '{:?}'", message); + for packet in packets.iter() { + let Some(message) = packet.decode::() else { continue; }; - let Ok(chat_session) = sessions.get_component::(message.client) else { - warn!("Player `{}` doesn't have a chat session", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) - }); + let Ok((mut state, mut client, username, settings)) = clients.get_mut(packet.client) else { + warn!("Unable to find client for message '{:?}'", message); continue; }; @@ -480,11 +598,10 @@ fn handle_message_events( if message.timestamp < state.last_message_timestamp { warn!( "{:?} sent out-of-order chat: '{:?}'", - username.0, - message.message.as_ref() + username.0, message.message ); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, []), }); continue; @@ -492,10 +609,27 @@ fn handle_message_events( state.last_message_timestamp = message.timestamp; + // Check if the message is signed + let Some(message_signature) = message.signature else { + // TODO: Cleanup + warn!("Received unsigned chat message from `{}`", username.0); + /*commands.add(DisconnectClient { + client: packet.client, + reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) + });*/ + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + message_type: ChatMessageType::Unsigned, + }); + continue; + }; + // Validate the message acknowledgements. let last_seen = match state .validator - .validate(&message.acknowledgements, message.message_index) + .validate(&message.acknowledgement, message.message_index.0) { Err(error) => { warn!( @@ -503,7 +637,7 @@ fn handle_message_events( username.0, error ); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, []), }); continue; @@ -519,6 +653,15 @@ fn handle_message_events( continue; }; + let Ok(chat_session) = sessions.get(packet.client) else { + warn!("Player `{}` doesn't have a chat session", username.0); + commands.add(DisconnectClient { + client: packet.client, + reason: Text::translate(CHAT_DISABLED_MISSING_PROFILE_KEY, []) + }); + continue; + }; + // Verify that the player's session has not expired. if SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -528,22 +671,12 @@ fn handle_message_events( { warn!("Player `{}` has an expired chat session", username.0); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(CHAT_DISABLED_EXPIRED_PROFILE_KEY, []), }); continue; } - // Verify that the chat message is signed. - let Some(message_signature) = &message.signature else { - warn!("Received unsigned chat message from `{}`", username.0); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []) - }); - continue; - }; - // Create the hash digest used to verify the chat message. let mut hasher = Sha256::new_with_prefix([0u8, 0, 0, 1]); @@ -575,51 +708,70 @@ fn handle_message_events( { warn!("Failed to verify chat message from `{}`", username.0); commands.add(DisconnectClient { - client: message.client, + client: packet.client, reason: Text::translate(MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, []), }); continue; } - info!("{}: {}", username.0, message.message.as_ref()); + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + message_type: ChatMessageType::Signed { + salt: message.salt, + signature: (*message_signature).into(), + message_index: link.index, + sender: link.sender, + last_seen, + }, + }); + } +} - let username = username.0.clone(); +#[cfg(not(feature = "secure"))] +fn handle_message_packets( + mut clients: Query<(&mut Client, &mut ChatState, &ClientSettings)>, + mut packets: EventReader, + mut message_events: EventWriter, +) { + for packet in packets.iter() { + let Some(message) = packet.decode::() else { + continue; + }; - // Broadcast the chat message to other clients. - for (mut client, mut state, ..) in clients.iter_mut() { - // Create a list of messages that have been seen by the client. - let previous = last_seen - .iter() - .map(|sig| match state.signature_storage.index_of(sig) { - Some(index) => MessageSignature::ByIndex(index), - None => MessageSignature::BySignature(sig), - }) - .collect::>(); + let Ok((mut client, mut state, settings)) = clients.get_mut(packet.client) else { + warn!("Unable to find client for message '{:?}'", message); + continue; + }; - client.write_packet(&ChatMessageS2c { - sender: link.sender, - index: link.index.into(), - message_signature: Some(message_signature.as_ref()), - message: message.message.as_ref(), - time_stamp: message.timestamp, - salt: message.salt, - previous_messages: previous, - unsigned_content: None, - filter_type: MessageFilterType::PassThrough, - chat_type: 0.into(), // TODO: Make chat type for player messages selectable - network_name: Text::from(username.clone()).into(), - network_target_name: None, - }); - // Add pending acknowledgement. - state.add_pending(&mut last_seen.clone(), *message_signature.as_ref()); - if state.validator.message_count() > 4096 { - warn!("User has too many pending chats `{}`", username); - commands.add(DisconnectClient { - client: message.client, - reason: Text::translate(MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, []), - }); - continue; - } + // Ensure that the client isn't sending messages while their chat is hidden. + if settings.chat_mode == ChatMode::Hidden { + client.send_message(Text::translate(CHAT_DISABLED_OPTIONS, []).color(Color::RED)); + continue; + } + + state.last_message_timestamp = message.timestamp; + + message_events.send(ChatMessage { + client: packet.client, + message: message.message.into(), + timestamp: message.timestamp, + }) + } +} + +fn handle_command_packets( + mut packets: EventReader, + mut command_events: EventWriter, +) { + for packet in packets.iter() { + if let Some(command) = packet.decode::() { + command_events.send(CommandExecution { + client: packet.client, + command: command.command.into(), + timestamp: command.timestamp, + }) } } } diff --git a/crates/valence_client/Cargo.toml b/crates/valence_client/Cargo.toml index 8394490c9..5d50ac371 100644 --- a/crates/valence_client/Cargo.toml +++ b/crates/valence_client/Cargo.toml @@ -3,6 +3,10 @@ name = "valence_client" version.workspace = true edition.workspace = true +[features] +chat = [] +secure_chat = ["chat"] + [dependencies] anyhow.workspace = true bevy_app.workspace = true diff --git a/crates/valence_client/src/chat.rs b/crates/valence_client/src/chat.rs new file mode 100644 index 000000000..6b8fd5f73 --- /dev/null +++ b/crates/valence_client/src/chat.rs @@ -0,0 +1,32 @@ +use bevy_ecs::prelude::*; +#[cfg(feature = "secure_chat")] +use uuid::Uuid; + +#[derive(Clone, Debug)] +pub struct CommandExecution { + pub client: Entity, + pub command: Box, + pub timestamp: u64, +} + +#[derive(Clone, Debug)] +pub struct ChatMessage { + pub client: Entity, + pub message: Box, + pub timestamp: u64, + #[cfg(feature = "secure_chat")] + pub message_type: ChatMessageType, +} + +#[cfg(feature = "secure_chat")] +#[derive(Clone, Debug)] +pub enum ChatMessageType { + Signed { + salt: u64, + signature: Box<[u8; 256]>, + message_index: i32, + sender: Uuid, + last_seen: Vec<[u8; 256]>, + }, + Unsigned, +} diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 37c6eb64f..f4b1d71c5 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -69,6 +69,8 @@ use valence_instance::{ClearInstanceChangesSet, Instance, WriteUpdatePacketsToIn use valence_registry::{RegistryCodec, RegistryCodecSet}; pub mod action; +#[cfg(feature = "chat")] +pub mod chat; pub mod command; pub mod event_loop; pub mod interact_entity; diff --git a/crates/valence_client/src/misc.rs b/crates/valence_client/src/misc.rs index d499fb465..fa73a930d 100644 --- a/crates/valence_client/src/misc.rs +++ b/crates/valence_client/src/misc.rs @@ -4,10 +4,9 @@ use glam::Vec3; use valence_core::block_pos::BlockPos; use valence_core::direction::Direction; use valence_core::hand::Hand; -use valence_core::packet::c2s::play::player_session::PlayerSessionData; use valence_core::packet::c2s::play::{ - ChatMessageC2s, ClientStatusC2s, CommandExecutionC2s, HandSwingC2s, MessageAcknowledgmentC2s, - PlayerInteractBlockC2s, PlayerInteractItemC2s, PlayerSessionC2s, ResourcePackStatusC2s, + ClientStatusC2s, HandSwingC2s, PlayerInteractBlockC2s, PlayerInteractItemC2s, + ResourcePackStatusC2s, }; use valence_entity::{EntityAnimation, EntityAnimations}; @@ -17,10 +16,6 @@ use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent}; pub(super) fn build(app: &mut App) { app.add_event::() .add_event::() - .add_event::() - .add_event::() - .add_event::() - .add_event::() .add_event::() .add_event::() .add_event::() @@ -54,36 +49,6 @@ pub struct InteractBlock { pub sequence: i32, } -#[derive(Clone, Debug)] -pub struct CommandExecution { - pub client: Entity, - pub command: Box, - pub timestamp: u64, -} - -#[derive(Clone, Debug)] -pub struct ChatMessage { - pub client: Entity, - pub message: Box, - pub timestamp: u64, - pub salt: u64, - pub signature: Option>, - pub message_index: i32, - pub acknowledgements: [u8; 3], -} - -#[derive(Copy, Clone, Debug)] -pub struct MessageAcknowledgment { - pub client: Entity, - pub message_index: i32, -} - -#[derive(Clone, Debug)] -pub struct PlayerSession { - pub client: Entity, - pub session_data: PlayerSessionData, -} - #[derive(Copy, Clone, Debug)] pub struct Respawn { pub client: Entity, @@ -118,10 +83,6 @@ fn handle_misc_packets( mut clients: Query<(&mut ActionSequence, &mut EntityAnimations)>, mut hand_swing_events: EventWriter, mut interact_block_events: EventWriter, - mut command_execution_events: EventWriter, - mut chat_message_events: EventWriter, - mut message_acknowledgement_events: EventWriter, - mut player_session_events: EventWriter, mut respawn_events: EventWriter, mut request_stats_events: EventWriter, mut resource_pack_status_change_events: EventWriter, @@ -159,32 +120,6 @@ fn handle_misc_packets( } // TODO - } else if let Some(pkt) = packet.decode::() { - command_execution_events.send(CommandExecution { - client: packet.client, - command: pkt.command.into(), - timestamp: pkt.timestamp, - }); - } else if let Some(pkt) = packet.decode::() { - chat_message_events.send(ChatMessage { - client: packet.client, - message: pkt.message.into(), - timestamp: pkt.timestamp, - salt: pkt.salt, - signature: pkt.signature.copied().map(Box::new), - message_index: pkt.message_index.0, - acknowledgements: pkt.acknowledgement, - }); - } else if let Some(pkt) = packet.decode::() { - message_acknowledgement_events.send(MessageAcknowledgment { - client: packet.client, - message_index: pkt.message_index.0, - }); - } else if let Some(pkt) = packet.decode::() { - player_session_events.send(PlayerSession { - client: packet.client, - session_data: pkt.0.into_owned(), - }); } else if let Some(pkt) = packet.decode::() { match pkt { ClientStatusC2s::PerformRespawn => respawn_events.send(Respawn {