Skip to main content

steel_protocol/packets/game/chat/
session.rs

1use std::{
2    io::Cursor,
3    time::{SystemTime, UNIX_EPOCH},
4};
5
6use steel_macros::ServerPacket;
7use steel_utils::codec::VarInt;
8use steel_utils::serial::PrefixedRead;
9use uuid::Uuid;
10
11const MAX_PUBLIC_KEY_SIZE: usize = 512;
12const MAX_KEY_SIGNATURE_SIZE: usize = 4096;
13
14/// Network-serializable chat session data.
15///
16/// This is a simplified version that holds raw byte data for transmission.
17/// The full version with validated keys lives in steel-core.
18#[derive(Clone, Debug)]
19pub struct ProtocolRemoteChatSessionData {
20    /// The session ID
21    pub session_id: Uuid,
22    /// When the key expires (as milliseconds since UNIX epoch)
23    pub expires_at_millis: i64,
24    /// The public key bytes
25    pub public_key_bytes: Vec<u8>,
26    /// The key signature bytes
27    pub key_signature: Vec<u8>,
28}
29
30impl ProtocolRemoteChatSessionData {
31    /// Creates new chat session data from raw components
32    #[must_use]
33    pub fn new(
34        session_id: Uuid,
35        expires_at: SystemTime,
36        public_key_bytes: Vec<u8>,
37        key_signature: Vec<u8>,
38    ) -> Self {
39        let expires_at_millis = expires_at
40            .duration_since(UNIX_EPOCH)
41            .unwrap_or_default()
42            .as_millis() as i64;
43
44        Self {
45            session_id,
46            expires_at_millis,
47            public_key_bytes,
48            key_signature,
49        }
50    }
51}
52
53impl steel_utils::serial::WriteTo for ProtocolRemoteChatSessionData {
54    fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
55        self.session_id.write(writer)?;
56
57        // Write expires_at (i64 millis)
58        self.expires_at_millis.write(writer)?;
59
60        // Write public key (length as VarInt, then bytes)
61        VarInt(self.public_key_bytes.len() as i32).write(writer)?;
62        writer.write_all(&self.public_key_bytes)?;
63
64        // Write key signature (length as VarInt, then bytes)
65        VarInt(self.key_signature.len() as i32).write(writer)?;
66        writer.write_all(&self.key_signature)?;
67
68        Ok(())
69    }
70}
71
72/// Client -> Server: Updates the player's chat session with their public key.
73///
74/// Sent when the player first joins or when their key needs to be updated.
75/// Contains the session ID and the player's public key signed by Mojang.
76///
77/// Equivalent to `ServerboundChatSessionUpdatePacket` in Minecraft.
78#[derive(ServerPacket, Clone, Debug)]
79pub struct SChatSessionUpdate {
80    /// The session ID for this chat session
81    pub session_id: Uuid,
82
83    /// Public key expiry timestamp (milliseconds since epoch)
84    pub expires_at: i64,
85
86    /// The player's RSA public key (DER encoded)
87    pub public_key: Vec<u8>,
88
89    /// Mojang's signature of the key (validates authenticity)
90    pub key_signature: Vec<u8>,
91}
92
93impl steel_utils::serial::ReadFrom for SChatSessionUpdate {
94    fn read(reader: &mut Cursor<&[u8]>) -> std::io::Result<Self> {
95        let session_id = Uuid::read(reader)?;
96        let expires_at = i64::read(reader)?;
97
98        let public_key = Vec::<u8>::read_prefixed_bound::<VarInt>(reader, MAX_PUBLIC_KEY_SIZE)?;
99        let key_signature =
100            Vec::<u8>::read_prefixed_bound::<VarInt>(reader, MAX_KEY_SIGNATURE_SIZE)?;
101
102        Ok(Self {
103            session_id,
104            expires_at,
105            public_key,
106            key_signature,
107        })
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::io::{Cursor, ErrorKind};
114
115    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
116    use uuid::Uuid;
117
118    use super::{MAX_KEY_SIGNATURE_SIZE, MAX_PUBLIC_KEY_SIZE, SChatSessionUpdate, VarInt};
119
120    fn encoded_packet(
121        public_key_length: i32,
122        public_key: &[u8],
123        signature: Option<(i32, &[u8])>,
124    ) -> Vec<u8> {
125        let mut packet = Vec::new();
126        Uuid::nil()
127            .write(&mut packet)
128            .expect("test session ID should encode");
129        0_i64
130            .write(&mut packet)
131            .expect("test expiration should encode");
132        VarInt(public_key_length)
133            .write(&mut packet)
134            .expect("test public key length should encode");
135        packet.extend_from_slice(public_key);
136
137        if let Some((signature_length, signature)) = signature {
138            VarInt(signature_length)
139                .write(&mut packet)
140                .expect("test signature length should encode");
141            packet.extend_from_slice(signature);
142        }
143
144        packet
145    }
146
147    fn decode(packet: &[u8]) -> std::io::Result<SChatSessionUpdate> {
148        SChatSessionUpdate::read(&mut Cursor::new(packet))
149    }
150
151    #[test]
152    fn accepts_vanilla_maximum_field_lengths() {
153        let public_key = vec![1; MAX_PUBLIC_KEY_SIZE];
154        let signature = vec![2; MAX_KEY_SIGNATURE_SIZE];
155        let packet = encoded_packet(
156            MAX_PUBLIC_KEY_SIZE as i32,
157            &public_key,
158            Some((MAX_KEY_SIGNATURE_SIZE as i32, &signature)),
159        );
160
161        let decoded = decode(&packet).expect("fields at the vanilla limits should decode");
162
163        assert_eq!(decoded.public_key, public_key);
164        assert_eq!(decoded.key_signature, signature);
165    }
166
167    #[test]
168    fn rejects_fields_above_vanilla_limits() {
169        let oversized_public_key =
170            encoded_packet(MAX_PUBLIC_KEY_SIZE as i32 + 1, &[], Some((0, &[])));
171        decode(&oversized_public_key).expect_err("an oversized public key should be rejected");
172
173        let oversized_signature =
174            encoded_packet(0, &[], Some((MAX_KEY_SIGNATURE_SIZE as i32 + 1, &[])));
175        decode(&oversized_signature).expect_err("an oversized signature should be rejected");
176    }
177
178    #[test]
179    fn rejects_negative_and_extreme_field_lengths() {
180        for length in [-1, i32::MAX] {
181            let public_key = encoded_packet(length, &[], Some((0, &[])));
182            decode(&public_key).expect_err("an invalid public key length should be rejected");
183
184            let signature = encoded_packet(0, &[], Some((length, &[])));
185            decode(&signature).expect_err("an invalid signature length should be rejected");
186        }
187    }
188
189    #[test]
190    fn rejects_truncated_field_bodies() {
191        let truncated_public_key = encoded_packet(1, &[], None);
192        let error = decode(&truncated_public_key)
193            .expect_err("a truncated public key body should be rejected");
194        assert_eq!(error.kind(), ErrorKind::UnexpectedEof);
195
196        let truncated_signature = encoded_packet(0, &[], Some((1, &[])));
197        let error = decode(&truncated_signature)
198            .expect_err("a truncated signature body should be rejected");
199        assert_eq!(error.kind(), ErrorKind::UnexpectedEof);
200    }
201}