1use 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
16pub const EXPIRY_GRACE_PERIOD: Duration = Duration::from_hours(8);
18
19pub const MAX_KEY_SIGNATURE_SIZE: usize = 4096;
21
22#[derive(Debug, Error)]
24pub enum ValidationError {
25 #[error("Invalid public key signature")]
27 InvalidSignature,
28
29 #[error("Key has expired")]
31 KeyExpired,
32
33 #[error("Cryptographic error: {0}")]
35 CryptoError(#[from] CryptError),
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct ProfilePublicKeyData {
43 pub expires_at: SystemTime,
45
46 pub key: RsaPublicKey,
48
49 pub key_signature: Vec<u8>,
51}
52
53impl ProfilePublicKeyData {
54 #[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 #[must_use]
66 pub fn has_expired(&self) -> bool {
67 self.expires_at.elapsed().is_ok()
69 }
70
71 #[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 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 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 payload.extend_from_slice(&profile_id.as_u128().to_be_bytes());
113
114 let expiry_millis = system_time_to_millis(self.expires_at);
116 payload.extend_from_slice(&expiry_millis.to_be_bytes());
117
118 payload.extend_from_slice(&key_bytes);
120
121 Ok(payload)
122 }
123
124 pub fn to_bytes(&self) -> Result<Vec<u8>, ValidationError> {
129 let mut bytes = Vec::new();
130
131 let expiry_millis = system_time_to_millis(self.expires_at);
133 bytes.extend_from_slice(&expiry_millis.to_be_bytes());
134
135 let key_bytes = public_key_to_bytes(&self.key)?;
137 bytes.extend_from_slice(&(key_bytes.len() as i32).to_be_bytes());
138 bytes.extend_from_slice(&key_bytes);
139
140 bytes.extend_from_slice(&(self.key_signature.len() as i32).to_be_bytes());
142 bytes.extend_from_slice(&self.key_signature);
143
144 Ok(bytes)
145 }
146
147 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
155 if bytes.len() < 16 {
156 return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
157 }
158
159 let mut offset = 0;
160
161 let expiry_millis = i64::from_be_bytes(
163 bytes[offset..offset + 8]
164 .try_into()
165 .expect("slice is exactly 8 bytes"),
166 );
167 offset += 8;
168 let expires_at = system_time_from_millis(expiry_millis);
169
170 if bytes.len() < offset + 4 {
172 return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
173 }
174 let key_len = i32::from_be_bytes(
175 bytes[offset..offset + 4]
176 .try_into()
177 .expect("slice is exactly 4 bytes"),
178 ) as usize;
179 offset += 4;
180
181 if bytes.len() < offset + key_len {
183 return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
184 }
185 let key = public_key_from_bytes(&bytes[offset..offset + key_len])?;
186 offset += key_len;
187
188 if bytes.len() < offset + 4 {
190 return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
191 }
192 let sig_len = i32::from_be_bytes(
193 bytes[offset..offset + 4]
194 .try_into()
195 .expect("slice is exactly 4 bytes"),
196 ) as usize;
197 offset += 4;
198
199 if bytes.len() < offset + sig_len {
201 return Err(ValidationError::CryptoError(CryptError::InvalidKeyFormat));
202 }
203 let key_signature = bytes[offset..offset + sig_len].to_vec();
204
205 Ok(Self {
206 expires_at,
207 key,
208 key_signature,
209 })
210 }
211}
212
213#[derive(Clone, Debug)]
217pub struct ProfilePublicKey {
218 data: ProfilePublicKeyData,
219}
220
221impl ProfilePublicKey {
222 #[must_use]
226 pub const fn new(data: ProfilePublicKeyData) -> Self {
227 Self { data }
228 }
229
230 pub fn create_validated(
237 profile_id: Uuid,
238 data: ProfilePublicKeyData,
239 validator: &dyn SignatureValidator,
240 ) -> Result<Self, ValidationError> {
241 data.validate_signature(profile_id, validator)?;
242 Ok(Self::new(data))
243 }
244
245 #[must_use]
247 pub const fn data(&self) -> &ProfilePublicKeyData {
248 &self.data
249 }
250
251 #[must_use]
253 pub fn create_signature_validator(&self) -> RsaPublicKeyValidator {
254 RsaPublicKeyValidator::new(self.data.key.clone())
255 }
256}
257
258#[derive(Clone, Debug)]
262pub struct RemoteChatSession {
263 pub session_id: Uuid,
265 pub profile_public_key: ProfilePublicKey,
267}
268
269impl RemoteChatSession {
270 #[must_use]
272 pub const fn new(session_id: Uuid, profile_public_key: ProfilePublicKey) -> Self {
273 Self {
274 session_id,
275 profile_public_key,
276 }
277 }
278
279 #[must_use]
281 pub fn has_expired(&self) -> bool {
282 self.profile_public_key.data().has_expired()
283 }
284
285 #[must_use]
287 pub fn as_data(&self) -> RemoteChatSessionData {
288 RemoteChatSessionData {
289 session_id: self.session_id,
290 profile_public_key: self.profile_public_key.data().clone(),
291 }
292 }
293}
294
295#[derive(Clone, Debug)]
299pub struct RemoteChatSessionData {
300 pub session_id: Uuid,
302 pub profile_public_key: ProfilePublicKeyData,
304}
305
306impl RemoteChatSessionData {
307 pub fn validate(
312 self,
313 profile_id: Uuid,
314 validator: &dyn SignatureValidator,
315 ) -> Result<RemoteChatSession, ValidationError> {
316 let public_key =
317 ProfilePublicKey::create_validated(profile_id, self.profile_public_key, validator)?;
318 Ok(RemoteChatSession::new(self.session_id, public_key))
319 }
320
321 pub fn to_protocol_data(&self) -> Result<ProtocolRemoteChatSessionData, ValidationError> {
326 let key_bytes = public_key_to_bytes(&self.profile_public_key.key)?;
327
328 Ok(ProtocolRemoteChatSessionData {
329 session_id: self.session_id,
330 expires_at_millis: system_time_to_millis(self.profile_public_key.expires_at),
331 public_key_bytes: key_bytes,
332 key_signature: self.profile_public_key.key_signature.clone(),
333 })
334 }
335}
336
337pub(super) fn system_time_from_millis(millis: i64) -> SystemTime {
338 if millis >= 0 {
339 UNIX_EPOCH + Duration::from_millis(millis as u64)
340 } else {
341 UNIX_EPOCH - Duration::from_millis(millis.unsigned_abs())
342 }
343}
344
345fn system_time_to_millis(time: SystemTime) -> i64 {
346 match time.duration_since(UNIX_EPOCH) {
347 Ok(duration) => i64::try_from(duration.as_millis()).unwrap_or(i64::MAX),
348 Err(error) => match i64::try_from(error.duration().as_millis()) {
349 Ok(millis) => millis.saturating_neg(),
350 Err(_) => i64::MIN,
351 },
352 }
353}
354
355mod signature_helpers {
357 use steel_crypto::{
358 CryptError,
359 signature::{SignatureOutput, SignatureUpdater},
360 };
361
362 pub struct ByteSliceUpdater<'a>(pub &'a [u8]);
363
364 impl SignatureUpdater for ByteSliceUpdater<'_> {
365 fn update(&self, output: &mut dyn SignatureOutput) -> Result<(), CryptError> {
366 output.update(self.0)
367 }
368 }
369}
370
371use signature_helpers::ByteSliceUpdater;
372
373#[cfg(test)]
374mod tests {
375 use rsa::{
376 pkcs1v15::SigningKey,
377 signature::{SignatureEncoding as _, Signer as _},
378 };
379 use sha1::Sha1;
380 use steel_crypto::{generate_key_pair, public_key_to_bytes, signature::ProfileKeyValidator};
381 use uuid::Uuid;
382
383 use super::{ProfilePublicKeyData, ValidationError, system_time_from_millis};
384
385 fn signed_profile_key(
386 profile_id: Uuid,
387 expires_at_millis: i64,
388 ) -> (ProfilePublicKeyData, ProfileKeyValidator) {
389 let (service_private_key, service_public_key) =
390 generate_key_pair().expect("test service key should generate");
391 let (_, player_public_key) = generate_key_pair().expect("test player key should generate");
392 let player_key_der =
393 public_key_to_bytes(&player_public_key).expect("test player key should encode");
394 let mut payload = Vec::with_capacity(24 + player_key_der.len());
395 payload.extend_from_slice(&profile_id.as_u128().to_be_bytes());
396 payload.extend_from_slice(&expires_at_millis.to_be_bytes());
397 payload.extend_from_slice(&player_key_der);
398 let signature = SigningKey::<Sha1>::new(service_private_key)
399 .sign(&payload)
400 .to_bytes()
401 .to_vec();
402
403 (
404 ProfilePublicKeyData::new(
405 system_time_from_millis(expires_at_millis),
406 player_public_key,
407 signature,
408 ),
409 ProfileKeyValidator::new(vec![service_public_key])
410 .expect("test service key should create a validator"),
411 )
412 }
413
414 #[test]
415 fn profile_key_signature_binds_uuid_expiry_and_public_key() {
416 let profile_id = Uuid::from_u128(1);
417 let (data, validator) = signed_profile_key(profile_id, 1_234);
418
419 data.validate_signature(profile_id, &validator)
420 .expect("untampered profile key should validate");
421 assert!(matches!(
422 data.validate_signature(Uuid::from_u128(2), &validator),
423 Err(ValidationError::InvalidSignature)
424 ));
425
426 let mut changed_expiry = data.clone();
427 changed_expiry.expires_at = system_time_from_millis(1_235);
428 assert!(matches!(
429 changed_expiry.validate_signature(profile_id, &validator),
430 Err(ValidationError::InvalidSignature)
431 ));
432
433 let mut changed_key = data.clone();
434 changed_key.key = generate_key_pair()
435 .expect("replacement player key should generate")
436 .1;
437 assert!(matches!(
438 changed_key.validate_signature(profile_id, &validator),
439 Err(ValidationError::InvalidSignature)
440 ));
441 }
442
443 #[test]
444 fn profile_key_signature_rejects_tampered_signature() {
445 let profile_id = Uuid::from_u128(1);
446 let (mut data, validator) = signed_profile_key(profile_id, 1_234);
447 data.key_signature[0] ^= 1;
448
449 assert!(matches!(
450 data.validate_signature(profile_id, &validator),
451 Err(ValidationError::InvalidSignature)
452 ));
453 }
454
455 #[test]
456 fn signed_payload_preserves_negative_expiry_millis() {
457 let profile_id = Uuid::from_u128(1);
458 let (data, validator) = signed_profile_key(profile_id, -1);
459
460 data.validate_signature(profile_id, &validator)
461 .expect("pre-epoch expiry should retain its signed timestamp");
462 }
463}