Skip to main content

steel_core/player/chat/
profile_key.rs

1//! Player profile public key management for secure chat.
2//!
3//! Ported from net/minecraft/world/entity/player/ProfilePublicKey.java
4
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use rsa::RsaPublicKey;
8use steel_crypto::{
9    CryptError, SignatureValidator, public_key_from_bytes, public_key_to_bytes,
10    signature::RsaPublicKeyValidator,
11};
12use steel_protocol::packets::game::ProtocolRemoteChatSessionData;
13use thiserror::Error;
14use uuid::Uuid;
15
16/// Grace period for expired keys (8 hours, as in vanilla)
17pub const EXPIRY_GRACE_PERIOD: Duration = Duration::from_hours(8);
18
19/// Maximum size of key signature in bytes
20pub const MAX_KEY_SIGNATURE_SIZE: usize = 4096;
21
22/// Errors that can occur during profile key validation
23#[derive(Debug, Error)]
24pub enum ValidationError {
25    /// Invalid public key signature
26    #[error("Invalid public key signature")]
27    InvalidSignature,
28
29    /// Key has expired
30    #[error("Key has expired")]
31    KeyExpired,
32
33    /// Cryptographic error
34    #[error("Cryptographic error: {0}")]
35    CryptoError(#[from] CryptError),
36}
37
38/// Profile public key data containing key, expiry, and Mojang signature.
39///
40/// Equivalent to ProfilePublicKey.Data in Minecraft.
41#[derive(Clone, Debug)]
42pub struct ProfilePublicKeyData {
43    /// When this key expires
44    pub expires_at: SystemTime,
45
46    /// The RSA public key
47    pub key: RsaPublicKey,
48
49    /// Signature of this key signed by Mojang's services
50    pub key_signature: Vec<u8>,
51}
52
53impl ProfilePublicKeyData {
54    /// Creates new profile public key data
55    #[must_use]
56    pub const fn new(expires_at: SystemTime, key: RsaPublicKey, key_signature: Vec<u8>) -> Self {
57        Self {
58            expires_at,
59            key,
60            key_signature,
61        }
62    }
63
64    /// Checks if the key has expired (without grace period)
65    #[must_use]
66    pub fn has_expired(&self) -> bool {
67        // Time has passed since expiry
68        self.expires_at.elapsed().is_ok()
69    }
70
71    /// Checks if the key has expired, with a grace period
72    #[must_use]
73    pub fn has_expired_with_grace(&self, grace_period: Duration) -> bool {
74        let expiry_with_grace = self.expires_at + grace_period;
75        expiry_with_grace.elapsed().is_ok()
76    }
77
78    /// Validates the key signature using Mojang's signature validator.
79    ///
80    /// The signature covers: profileId + expiresAt + key bytes
81    ///
82    /// # Errors
83    /// Returns `ValidationError` if signature validation fails or cryptographic operations fail
84    pub fn validate_signature(
85        &self,
86        profile_id: Uuid,
87        validator: &dyn SignatureValidator,
88    ) -> Result<(), ValidationError> {
89        let payload = self.signed_payload(profile_id)?;
90        let updater = ByteSliceUpdater(&payload);
91
92        let is_valid = validator
93            .validate(&updater, &self.key_signature)
94            .map_err(ValidationError::from)?;
95
96        if is_valid {
97            Ok(())
98        } else {
99            Err(ValidationError::InvalidSignature)
100        }
101    }
102
103    /// Constructs the byte payload that was signed by Mojang.
104    ///
105    /// Format: profileId (16 bytes) + expiresAt (8 bytes) + key bytes
106    fn signed_payload(&self, profile_id: Uuid) -> Result<Vec<u8>, ValidationError> {
107        let key_bytes = public_key_to_bytes(&self.key)?;
108
109        let mut payload = Vec::with_capacity(24 + key_bytes.len());
110
111        // Profile UUID (most significant bits + least significant bits, big-endian)
112        payload.extend_from_slice(&profile_id.as_u128().to_be_bytes());
113
114        // Expiry timestamp (milliseconds since epoch, big-endian)
115        let expiry_millis = self
116            .expires_at
117            .duration_since(UNIX_EPOCH)
118            .unwrap_or_default()
119            .as_millis() as i64;
120        payload.extend_from_slice(&expiry_millis.to_be_bytes());
121
122        // Public key bytes
123        payload.extend_from_slice(&key_bytes);
124
125        Ok(payload)
126    }
127
128    /// Serializes the key data for network transmission
129    ///
130    /// # Errors
131    /// Returns `ValidationError` if key encoding fails
132    pub fn to_bytes(&self) -> Result<Vec<u8>, ValidationError> {
133        let mut bytes = Vec::new();
134
135        // Expiry timestamp (i64 milliseconds)
136        let expiry_millis = self
137            .expires_at
138            .duration_since(UNIX_EPOCH)
139            .unwrap_or_default()
140            .as_millis() as i64;
141        bytes.extend_from_slice(&expiry_millis.to_be_bytes());
142
143        // Public key
144        let key_bytes = public_key_to_bytes(&self.key)?;
145        bytes.extend_from_slice(&(key_bytes.len() as i32).to_be_bytes());
146        bytes.extend_from_slice(&key_bytes);
147
148        // Key signature
149        bytes.extend_from_slice(&(self.key_signature.len() as i32).to_be_bytes());
150        bytes.extend_from_slice(&self.key_signature);
151
152        Ok(bytes)
153    }
154
155    /// Deserializes key data from bytes
156    ///
157    /// # Errors
158    /// Returns `ValidationError` if the byte format is invalid or key decoding fails
159    ///
160    /// # Panics
161    /// Panics if slice-to-array conversion fails (should not happen due to length checks)
162    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
163        if bytes.len() < 16 {
164            return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
165        }
166
167        let mut offset = 0;
168
169        // Read expiry timestamp
170        let expiry_millis = i64::from_be_bytes(
171            bytes[offset..offset + 8]
172                .try_into()
173                .expect("slice is exactly 8 bytes"),
174        );
175        offset += 8;
176        let expires_at = UNIX_EPOCH + Duration::from_millis(expiry_millis as u64);
177
178        // Read public key length
179        if bytes.len() < offset + 4 {
180            return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
181        }
182        let key_len = i32::from_be_bytes(
183            bytes[offset..offset + 4]
184                .try_into()
185                .expect("slice is exactly 4 bytes"),
186        ) as usize;
187        offset += 4;
188
189        // Read public key
190        if bytes.len() < offset + key_len {
191            return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
192        }
193        let key = public_key_from_bytes(&bytes[offset..offset + key_len])?;
194        offset += key_len;
195
196        // Read signature length
197        if bytes.len() < offset + 4 {
198            return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
199        }
200        let sig_len = i32::from_be_bytes(
201            bytes[offset..offset + 4]
202                .try_into()
203                .expect("slice is exactly 4 bytes"),
204        ) as usize;
205        offset += 4;
206
207        // Read signature
208        if bytes.len() < offset + sig_len {
209            return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
210        }
211        let key_signature = bytes[offset..offset + sig_len].to_vec();
212
213        Ok(Self {
214            expires_at,
215            key,
216            key_signature,
217        })
218    }
219}
220
221/// Validated profile public key.
222///
223/// Equivalent to `ProfilePublicKey` in Minecraft.
224#[derive(Clone, Debug)]
225pub struct ProfilePublicKey {
226    data: ProfilePublicKeyData,
227}
228
229impl ProfilePublicKey {
230    /// Creates a new validated profile public key.
231    ///
232    /// This should only be called after validating the signature.
233    #[must_use]
234    pub const fn new(data: ProfilePublicKeyData) -> Self {
235        Self { data }
236    }
237
238    /// Validates and creates a profile public key.
239    ///
240    /// Equivalent to `ProfilePublicKey.createValidated()` in Minecraft.
241    ///
242    /// # Errors
243    /// Returns `ValidationError` if signature validation fails
244    pub fn create_validated(
245        profile_id: Uuid,
246        data: ProfilePublicKeyData,
247        validator: &dyn SignatureValidator,
248    ) -> Result<Self, ValidationError> {
249        data.validate_signature(profile_id, validator)?;
250        Ok(Self::new(data))
251    }
252
253    /// Gets the underlying key data
254    #[must_use]
255    pub const fn data(&self) -> &ProfilePublicKeyData {
256        &self.data
257    }
258
259    /// Creates a signature validator for this key
260    #[must_use]
261    pub fn create_signature_validator(&self) -> RsaPublicKeyValidator {
262        RsaPublicKeyValidator::new(self.data.key.clone())
263    }
264}
265
266/// Remote chat session containing session ID and validated public key.
267///
268/// Equivalent to `RemoteChatSession` in Minecraft.
269#[derive(Clone, Debug)]
270pub struct RemoteChatSession {
271    /// The session ID
272    pub session_id: Uuid,
273    /// The validated profile public key
274    pub profile_public_key: ProfilePublicKey,
275}
276
277impl RemoteChatSession {
278    /// Creates a new remote chat session
279    #[must_use]
280    pub const fn new(session_id: Uuid, profile_public_key: ProfilePublicKey) -> Self {
281        Self {
282            session_id,
283            profile_public_key,
284        }
285    }
286
287    /// Checks if the key has expired
288    #[must_use]
289    pub fn has_expired(&self) -> bool {
290        self.profile_public_key.data().has_expired()
291    }
292
293    /// Converts to data for network transmission
294    #[must_use]
295    pub fn as_data(&self) -> RemoteChatSessionData {
296        RemoteChatSessionData {
297            session_id: self.session_id,
298            profile_public_key: self.profile_public_key.data().clone(),
299        }
300    }
301}
302
303/// Network-serializable chat session data.
304///
305/// Equivalent to `RemoteChatSession.Data` in Minecraft.
306#[derive(Clone, Debug)]
307pub struct RemoteChatSessionData {
308    /// The session ID
309    pub session_id: Uuid,
310    /// The profile public key data
311    pub profile_public_key: ProfilePublicKeyData,
312}
313
314impl RemoteChatSessionData {
315    /// Validates and creates a `RemoteChatSession`
316    ///
317    /// # Errors
318    /// Returns `ValidationError` if signature validation fails
319    pub fn validate(
320        self,
321        profile_id: Uuid,
322        validator: &dyn SignatureValidator,
323    ) -> Result<RemoteChatSession, ValidationError> {
324        let public_key =
325            ProfilePublicKey::create_validated(profile_id, self.profile_public_key, validator)?;
326        Ok(RemoteChatSession::new(self.session_id, public_key))
327    }
328
329    /// Converts to network-serializable format for transmission
330    ///
331    /// # Errors
332    /// Returns `ValidationError` if key encoding fails
333    pub fn to_protocol_data(&self) -> Result<ProtocolRemoteChatSessionData, ValidationError> {
334        let key_bytes = public_key_to_bytes(&self.profile_public_key.key)?;
335
336        Ok(ProtocolRemoteChatSessionData::new(
337            self.session_id,
338            self.profile_public_key.expires_at,
339            key_bytes,
340            self.profile_public_key.key_signature.clone(),
341        ))
342    }
343}
344
345// Helper struct for byte slice updater
346mod signature_helpers {
347    use steel_crypto::{
348        CryptError,
349        signature::{SignatureOutput, SignatureUpdater},
350    };
351
352    pub struct ByteSliceUpdater<'a>(pub &'a [u8]);
353
354    impl SignatureUpdater for ByteSliceUpdater<'_> {
355        fn update(&self, output: &mut dyn SignatureOutput) -> Result<(), CryptError> {
356            output.update(self.0)
357        }
358    }
359}
360
361use signature_helpers::ByteSliceUpdater;