aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'azalea-client/src/plugins')
-rw-r--r--azalea-client/src/plugins/chat/handler.rs74
-rw-r--r--azalea-client/src/plugins/chat_signing.rs182
-rw-r--r--azalea-client/src/plugins/disconnect.rs6
-rw-r--r--azalea-client/src/plugins/mod.rs4
4 files changed, 250 insertions, 16 deletions
diff --git a/azalea-client/src/plugins/chat/handler.rs b/azalea-client/src/plugins/chat/handler.rs
index 356d3fba..a289eb14 100644
--- a/azalea-client/src/plugins/chat/handler.rs
+++ b/azalea-client/src/plugins/chat/handler.rs
@@ -1,5 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH};
+use azalea_crypto::SignChatMessageOptions;
use azalea_protocol::packets::{
Packet,
game::{ServerboundChat, ServerboundChatCommand, s_chat::LastSeenMessagesUpdate},
@@ -7,7 +8,7 @@ use azalea_protocol::packets::{
use bevy_ecs::prelude::*;
use super::ChatKind;
-use crate::packet::game::SendPacketEvent;
+use crate::{Account, chat_signing::ChatSigningSession, packet::game::SendPacketEvent};
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
@@ -30,6 +31,7 @@ pub struct SendChatKindEvent {
pub fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut commands: Commands,
+ mut query: Query<(&Account, &mut ChatSigningSession)>,
) {
for event in events.read() {
let content = event
@@ -38,22 +40,43 @@ pub fn handle_send_chat_kind_event(
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | 'ยง'))
.take(256)
.collect::<String>();
+
+ let timestamp = SystemTime::now();
+
let packet = match event.kind {
- ChatKind::Message => ServerboundChat {
- message: content,
- timestamp: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("Time shouldn't be before epoch")
- .as_millis()
- .try_into()
- .expect("Instant should fit into a u64"),
- salt: azalea_crypto::make_salt(),
- signature: None,
- last_seen_messages: LastSeenMessagesUpdate::default(),
+ ChatKind::Message => {
+ let salt = azalea_crypto::make_salt();
+
+ let signature = if let Ok((account, mut chat_session)) = query.get_mut(event.entity)
+ {
+ Some(create_signature(
+ account,
+ &mut chat_session,
+ salt,
+ timestamp,
+ &content,
+ ))
+ } else {
+ None
+ };
+
+ ServerboundChat {
+ message: content,
+ timestamp: timestamp
+ .duration_since(UNIX_EPOCH)
+ .expect("Time shouldn't be before epoch")
+ .as_millis()
+ .try_into()
+ .expect("Instant should fit into a u64"),
+ salt,
+ signature,
+ // TODO: implement last_seen_messages
+ last_seen_messages: LastSeenMessagesUpdate::default(),
+ }
}
.into_variant(),
ChatKind::Command => {
- // TODO: chat signing
+ // TODO: commands that require chat signing
ServerboundChatCommand { command: content }.into_variant()
}
};
@@ -61,3 +84,28 @@ pub fn handle_send_chat_kind_event(
commands.trigger(SendPacketEvent::new(event.entity, packet));
}
}
+
+pub fn create_signature(
+ account: &Account,
+ chat_session: &mut ChatSigningSession,
+ salt: u64,
+ timestamp: SystemTime,
+ message: &str,
+) -> azalea_crypto::MessageSignature {
+ let certs = account.certs.lock();
+ let certs = certs.as_ref().expect("certs shouldn't be set back to None");
+
+ let signature = azalea_crypto::sign_chat_message(&SignChatMessageOptions {
+ account_uuid: account.uuid.expect("account must have a uuid"),
+ chat_session_uuid: chat_session.session_id,
+ message_index: chat_session.messages_sent,
+ salt,
+ timestamp,
+ message: message.to_owned(),
+ private_key: certs.private_key.clone(),
+ });
+
+ chat_session.messages_sent += 1;
+
+ signature
+}
diff --git a/azalea-client/src/plugins/chat_signing.rs b/azalea-client/src/plugins/chat_signing.rs
new file mode 100644
index 00000000..03dd1a21
--- /dev/null
+++ b/azalea-client/src/plugins/chat_signing.rs
@@ -0,0 +1,182 @@
+use std::time::{Duration, Instant};
+
+use azalea_auth::certs::{Certificates, FetchCertificatesError};
+use azalea_protocol::packets::game::{
+ ServerboundChatSessionUpdate,
+ s_chat_session_update::{ProfilePublicKeyData, RemoteChatSessionData},
+};
+use bevy_app::prelude::*;
+use bevy_ecs::prelude::*;
+use bevy_tasks::{IoTaskPool, Task, futures_lite::future};
+use chrono::Utc;
+use tracing::{debug, error};
+use uuid::Uuid;
+
+use super::{chat, packet::game::SendPacketEvent};
+use crate::{Account, InGameState};
+
+pub struct ChatSigningPlugin;
+impl Plugin for ChatSigningPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ Update,
+ (
+ request_certs_if_needed,
+ poll_request_certs_task,
+ handle_queued_certs_to_send,
+ )
+ .chain()
+ .before(chat::handler::handle_send_chat_kind_event),
+ );
+ }
+}
+
+#[derive(Component)]
+pub struct RequestCertsTask(pub Task<Result<Certificates, FetchCertificatesError>>);
+
+/// A component that makes us have to wait until the given time to refresh the
+/// certs.
+///
+/// This is used to avoid spamming requests if requesting certs fails. Usually,
+/// we just check [`Certificates::expires_at`].
+#[derive(Component)]
+pub struct OnlyRefreshCertsAfter {
+ pub refresh_at: Instant,
+}
+/// A component that's present when that this client has sent its certificates
+/// to the server.
+///
+/// This should be removed if you want to re-send the certs.
+///
+/// If you want to get the client's actual certificates, you can get that from
+/// the `certs` in the [`Account`] component.
+#[derive(Component)]
+pub struct ChatSigningSession {
+ pub session_id: Uuid,
+ pub messages_sent: u32,
+}
+
+pub fn poll_request_certs_task(
+ mut commands: Commands,
+ mut query: Query<(Entity, &mut RequestCertsTask, &Account)>,
+) {
+ for (entity, mut auth_task, account) in query.iter_mut() {
+ if let Some(poll_res) = future::block_on(future::poll_once(&mut auth_task.0)) {
+ debug!("Finished requesting certs");
+ commands.entity(entity).remove::<RequestCertsTask>();
+
+ match poll_res {
+ Ok(certs) => {
+ commands.entity(entity).insert(QueuedCertsToSend {
+ certs: certs.clone(),
+ });
+ *account.certs.lock() = Some(certs);
+ }
+ Err(err) => {
+ error!("Error requesting certs: {err:?}. Retrying in an hour.");
+
+ commands.entity(entity).insert(OnlyRefreshCertsAfter {
+ refresh_at: Instant::now() + Duration::from_secs(60 * 60),
+ });
+ }
+ }
+ }
+ }
+}
+
+#[allow(clippy::type_complexity)]
+pub fn request_certs_if_needed(
+ mut commands: Commands,
+ mut query: Query<
+ (
+ Entity,
+ &Account,
+ Option<&OnlyRefreshCertsAfter>,
+ Option<&ChatSigningSession>,
+ ),
+ (Without<RequestCertsTask>, With<InGameState>),
+ >,
+) {
+ for (entity, account, only_refresh_certs_after, chat_signing_session) in query.iter_mut() {
+ if let Some(only_refresh_certs_after) = only_refresh_certs_after {
+ if only_refresh_certs_after.refresh_at > Instant::now() {
+ continue;
+ }
+ }
+
+ let certs = account.certs.lock();
+ let should_refresh = if let Some(certs) = &*certs {
+ // certs were already requested and we're waiting for them to refresh
+
+ // but maybe they weren't sent yet, in which case we still want to send the
+ // certs
+ if chat_signing_session.is_none() {
+ true
+ } else {
+ Utc::now() > certs.expires_at
+ }
+ } else {
+ true
+ };
+ drop(certs);
+
+ if should_refresh {
+ if let Some(access_token) = &account.access_token {
+ let task_pool = IoTaskPool::get();
+
+ let access_token = access_token.lock().clone();
+ debug!("Started task to fetch certs");
+ let task = task_pool.spawn(async_compat::Compat::new(async move {
+ azalea_auth::certs::fetch_certificates(&access_token).await
+ }));
+ commands
+ .entity(entity)
+ .insert(RequestCertsTask(task))
+ .remove::<OnlyRefreshCertsAfter>();
+ }
+ }
+ }
+}
+
+/// A component that's present on players that should send their chat signing
+/// certificates as soon as possible.
+///
+/// This is removed when the certificates get sent.
+#[derive(Component)]
+pub struct QueuedCertsToSend {
+ pub certs: Certificates,
+}
+
+pub fn handle_queued_certs_to_send(
+ mut commands: Commands,
+ query: Query<(Entity, &QueuedCertsToSend)>,
+) {
+ for (entity, queued_certs) in &query {
+ let certs = &queued_certs.certs;
+
+ let session_id = Uuid::new_v4();
+
+ let chat_session = RemoteChatSessionData {
+ session_id,
+ profile_public_key: ProfilePublicKeyData {
+ expires_at: certs.expires_at.timestamp_millis() as u64,
+ key: certs.public_key_der.clone(),
+ key_signature: certs.signature_v2.clone(),
+ },
+ };
+
+ debug!("Sending chat signing certs to server");
+
+ commands.trigger(SendPacketEvent::new(
+ entity,
+ ServerboundChatSessionUpdate { chat_session },
+ ));
+ commands
+ .entity(entity)
+ .remove::<QueuedCertsToSend>()
+ .insert(ChatSigningSession {
+ session_id,
+ messages_sent: 0,
+ });
+ }
+}
diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs
index 987007c2..3e27eba3 100644
--- a/azalea-client/src/plugins/disconnect.rs
+++ b/azalea-client/src/plugins/disconnect.rs
@@ -7,7 +7,7 @@ use bevy_ecs::prelude::*;
use derive_more::Deref;
use tracing::info;
-use crate::{InstanceHolder, client::JoinedClientBundle, connection::RawConnection};
+use crate::{InstanceHolder, chat_signing, client::JoinedClientBundle, connection::RawConnection};
pub struct DisconnectPlugin;
impl Plugin for DisconnectPlugin {
@@ -70,7 +70,9 @@ pub fn remove_components_from_disconnected_players(
// this makes it close the tcp connection
.remove::<RawConnection>()
// this makes it not send DisconnectEvent again
- .remove::<IsConnectionAlive>();
+ .remove::<IsConnectionAlive>()
+ // resend our chat signing certs next time
+ .remove::<chat_signing::ChatSigningSession>();
// note that we don't remove the client from the ECS, so if they decide
// to reconnect they'll keep their state
diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs
index f657b9e9..dea2fdb8 100644
--- a/azalea-client/src/plugins/mod.rs
+++ b/azalea-client/src/plugins/mod.rs
@@ -4,6 +4,7 @@ pub mod attack;
pub mod auto_reconnect;
pub mod brand;
pub mod chat;
+pub mod chat_signing;
pub mod chunks;
pub mod connection;
pub mod disconnect;
@@ -53,7 +54,8 @@ impl PluginGroup for DefaultPlugins {
.add(connection::ConnectionPlugin)
.add(login::LoginPlugin)
.add(join::JoinPlugin)
- .add(auto_reconnect::AutoReconnectPlugin);
+ .add(auto_reconnect::AutoReconnectPlugin)
+ .add(chat_signing::ChatSigningPlugin);
#[cfg(feature = "log")]
{
group = group.add(bevy_log::LogPlugin::default());