Skip to main content

steamkit/networking/steam3/
envelope_encrypted_connection.rs

1//! Steam's channel encryption, layered on top of another connection.
2//!
3//! It answers the server's `ChannelEncryptRequest` with a fresh 32-byte session
4//! key, RSA-OAEP-SHA1 encrypted with the universe's public key. After that every
5//! message is encrypted with AES-256-CBC and authenticated with HMAC-SHA1 (see
6//! [`NetFilterEncryptionWithHMAC`]).
7//!
8//! # Handshake wire format
9//!
10//! The request and the result use the plain `MsgHdr` header (20 bytes), the
11//! response additionally carries a `MsgChannelEncryptResponse` body:
12//!
13//! ```text
14//! offset  size  field
15//! 0       4     EMsg (i32, little endian)
16//! 4       8     target job id (u64, u64::MAX)
17//! 12      8     source job id (u64, u64::MAX)
18//! 20      4     protocol version (u32, 1)
19//! 24      4     universe (i32)          -- request only
20//! 28      4     key size (u32, 128)     -- response only
21//! 32      128   RSA-OAEP-SHA1 blob     -- response only
22//! 160     4     CRC32 of the blob      -- response only
23//! 164     4     zero                   -- response only
24//! ```
25
26use std::net::{IpAddr, SocketAddr};
27use std::sync::atomic::{AtomicBool, Ordering};
28use std::sync::{Arc, Mutex, MutexGuard};
29use std::time::Duration;
30
31use async_trait::async_trait;
32use bytes::Bytes;
33use rand::RngCore;
34use rsa::pkcs8::DecodePublicKey;
35use rsa::{Oaep, RsaPublicKey};
36use sha1::Sha1;
37use tokio::sync::{broadcast, mpsc};
38
39use crate::base::generated::steam_language::{EMsg, EResult, EUniverse};
40use crate::error::SteamError;
41use crate::util::key_dictionary::KeyDictionary;
42
43use super::disconnected_event_args::DisconnectedEventArgs;
44use super::i_connection::{
45    Connection, ConnectionEvent, ConnectionEventSenders, ConnectionEvents, IConnection,
46};
47use super::net_filter_encryption_with_hmac::NetFilterEncryptionWithHMAC;
48use super::net_msg_event_args::NetMsgEventArgs;
49use super::protocol_type::ProtocolTypes;
50
51/// The size of the plain `MsgHdr` header.
52const MSG_HDR_LENGTH: usize = 20;
53
54/// The bit that marks a message as protobuf encoded.
55const PROTO_MASK: u32 = 0x8000_0000;
56
57/// The protocol version of the channel encryption handshake.
58const PROTOCOL_VERSION: u32 = 1;
59
60/// The size of the RSA blob in the channel encryption response.
61const KEY_SIZE: u32 = 128;
62
63/// The length of the session key.
64const SESSION_KEY_LENGTH: usize = 32;
65
66/// The minimum length of the random challenge sent by the server.
67const MIN_CHALLENGE_LENGTH: usize = 16;
68
69/// The state of the channel encryption handshake.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71enum EncryptionState {
72    Disconnected,
73    Connected,
74    Challenged,
75    Encrypted,
76}
77
78/// Gets to see the unencrypted handshake messages.
79pub use crate::util::debug_network_listener::IDebugNetworkListener as DebugNetworkListener;
80
81struct EnvelopeShared {
82    inner: Arc<dyn Connection>,
83    events: ConnectionEventSenders,
84    universe: EUniverse,
85    state: Mutex<EncryptionState>,
86    encryption: Mutex<Option<Arc<NetFilterEncryptionWithHMAC>>>,
87    debug_network_listener: Option<Arc<dyn DebugNetworkListener>>,
88    forwarders_started: AtomicBool,
89}
90
91/// A connection that runs Steam's channel encryption handshake on top of
92/// another connection.
93pub struct EnvelopeEncryptedConnection {
94    shared: Arc<EnvelopeShared>,
95}
96
97impl std::fmt::Debug for EnvelopeEncryptedConnection {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("EnvelopeEncryptedConnection").finish_non_exhaustive()
100    }
101}
102
103impl EnvelopeEncryptedConnection {
104    /// Creates a new envelope connection wrapping `inner`.
105    pub fn new(inner: Arc<dyn Connection>, universe: EUniverse) -> Self {
106        Self::with_debug_network_listener(inner, universe, None)
107    }
108
109    /// Creates a new envelope connection that reports the handshake messages to
110    /// `debug_network_listener`.
111    pub fn with_debug_network_listener(
112        inner: Arc<dyn Connection>,
113        universe: EUniverse,
114        debug_network_listener: Option<Arc<dyn DebugNetworkListener>>,
115    ) -> Self {
116        let shared = Arc::new(EnvelopeShared {
117            inner,
118            events: ConnectionEventSenders::with_default_capacity(),
119            universe,
120            state: Mutex::new(EncryptionState::Disconnected),
121            encryption: Mutex::new(None),
122            debug_network_listener,
123            forwarders_started: AtomicBool::new(false),
124        });
125
126        Self { shared }
127    }
128
129    /// Starts the tasks that forward the inner connection's events. Done on the
130    /// first `connect` rather than in `new`, so creating a connection doesn't
131    /// need a tokio runtime.
132    fn spawn_forwarders(&self) {
133        if self
134            .shared
135            .forwarders_started
136            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
137            .is_err()
138        {
139            return;
140        }
141
142        spawn_forwarders(self.shared.clone(), self.shared.inner.clone());
143    }
144
145    /// Subscribes to the `NetMsgReceived` event.
146    pub fn subscribe_net_msg_received(&self) -> broadcast::Receiver<NetMsgEventArgs> {
147        self.shared.events.subscribe_net_msg_received()
148    }
149
150    /// Subscribes to the `Connected` event.
151    pub fn subscribe_connected(&self) -> broadcast::Receiver<()> {
152        self.shared.events.subscribe_connected()
153    }
154
155    /// Subscribes to the `Disconnected` event.
156    pub fn subscribe_disconnected(&self) -> broadcast::Receiver<DisconnectedEventArgs> {
157        self.shared.events.subscribe_disconnected()
158    }
159}
160
161#[async_trait]
162impl IConnection for EnvelopeEncryptedConnection {
163    fn current_end_point(&self) -> Option<SocketAddr> {
164        self.shared.inner.current_end_point()
165    }
166
167    fn protocol_types(&self) -> ProtocolTypes {
168        self.shared.inner.protocol_types()
169    }
170
171    async fn connect(&self, end_point: SocketAddr, timeout: Duration) -> Result<(), SteamError> {
172        self.spawn_forwarders();
173
174        self.shared.inner.connect(end_point, timeout).await
175    }
176
177    async fn disconnect(&self, user_initiated: bool) {
178        self.shared.inner.disconnect(user_initiated).await;
179    }
180
181    async fn send(&self, data: Bytes) -> Result<(), SteamError> {
182        let state = lock(&self.shared.state).clone();
183
184        if state != EncryptionState::Encrypted {
185            return self.shared.inner.send(data).await;
186        }
187
188        let encryption = lock(&self.shared.encryption).clone();
189
190        let Some(encryption) = encryption else {
191            tracing::debug!(
192                "EnvelopeEncryptedConnection: Attempting to send client data when not connected."
193            );
194            return Ok(());
195        };
196
197        let encrypted = encryption.process_outgoing(&data)?;
198        self.shared.inner.send(Bytes::from(encrypted)).await
199    }
200
201    fn local_ip(&self) -> Option<IpAddr> {
202        self.shared.inner.local_ip()
203    }
204}
205
206impl ConnectionEvents for EnvelopeEncryptedConnection {
207    fn events(&self) -> mpsc::UnboundedReceiver<ConnectionEvent> {
208        self.shared.events.subscribe_events()
209    }
210
211    fn net_msg_received(&self) -> broadcast::Receiver<NetMsgEventArgs> {
212        self.shared.events.subscribe_net_msg_received()
213    }
214
215    fn connected(&self) -> broadcast::Receiver<()> {
216        self.shared.events.subscribe_connected()
217    }
218
219    fn disconnected(&self) -> broadcast::Receiver<DisconnectedEventArgs> {
220        self.shared.events.subscribe_disconnected()
221    }
222}
223
224/// Forwards the inner connection's events.
225///
226/// One task handles all three events, in order. That matters: the server sends
227/// `ChannelEncryptRequest` as soon as the socket opens, and it's only accepted
228/// once `Connected` has moved the state out of `Disconnected`.
229///
230/// The task only holds a weak reference to the connection. A strong one would
231/// make a cycle (connection -> inner connection -> event sender) and leak it.
232fn spawn_forwarders(shared: Arc<EnvelopeShared>, inner: Arc<dyn Connection>) {
233    // Subscribe before `connect` is called so no event is missed.
234    let mut events = inner.events();
235    let weak = Arc::downgrade(&shared);
236    drop(shared);
237
238    tokio::spawn(async move {
239        while let Some(event) = events.recv().await {
240            let Some(shared) = weak.upgrade() else {
241                break;
242            };
243
244            match event {
245                ConnectionEvent::NetMsgReceived(args) => {
246                    handle_net_msg_received(&shared, args).await;
247                }
248                ConnectionEvent::Connected => {
249                    *lock(&shared.state) = EncryptionState::Connected;
250                }
251                ConnectionEvent::Disconnected(args) => {
252                    *lock(&shared.state) = EncryptionState::Disconnected;
253                    *lock(&shared.encryption) = None;
254                    shared
255                        .events
256                        .emit_disconnected(DisconnectedEventArgs::new(args.user_initiated));
257                }
258            }
259        }
260    });
261}
262
263async fn handle_net_msg_received(shared: &Arc<EnvelopeShared>, args: NetMsgEventArgs) {
264    let state = lock(&shared.state).clone();
265
266    if state == EncryptionState::Encrypted {
267        let encryption = lock(&shared.encryption).clone();
268
269        let Some(encryption) = encryption else {
270            return;
271        };
272
273        match encryption.process_incoming(&args.data) {
274            Ok(plain_text) => {
275                shared.events.emit_net_msg_received(args.with_data(plain_text));
276            }
277            Err(err) => {
278                tracing::debug!(
279                    "EnvelopeEncryptedConnection: Unable to decrypt incoming packet: {err}"
280                );
281            }
282        }
283
284        return;
285    }
286
287    let Some(msg_type) = parse_msg_type(&args.data) else {
288        tracing::debug!(
289            "EnvelopeEncryptedConnection: Failed to parse message during channel setup, shutting down connection"
290        );
291        shared.inner.disconnect(false).await;
292        return;
293    };
294
295    if let Some(listener) = &shared.debug_network_listener {
296        listener.on_incoming_network_message(msg_type, &args.data);
297    }
298
299    if !is_expected_emsg(state, msg_type) {
300        tracing::debug!("EnvelopeEncryptedConnection: Rejected EMsg during channel setup");
301        return;
302    }
303
304    match msg_type {
305        EMsg::ChannelEncryptRequest => handle_encrypt_request(shared, &args.data).await,
306        EMsg::ChannelEncryptResult => handle_encrypt_result(shared, &args.data).await,
307        _ => {}
308    }
309}
310
311async fn handle_encrypt_request(shared: &Arc<EnvelopeShared>, data: &[u8]) {
312    if data.len() < MSG_HDR_LENGTH + 8 {
313        tracing::debug!(
314            "EnvelopeEncryptedConnection: Encryption handshake has a payload that is less than 16 bytes"
315        );
316        shared.inner.disconnect(false).await;
317        return;
318    }
319
320    let protocol_version = read_u32(data, MSG_HDR_LENGTH);
321    let universe =
322        EUniverse::from_value(read_i32(data, MSG_HDR_LENGTH + 4)).unwrap_or(EUniverse::Invalid);
323
324    tracing::debug!(
325        "EnvelopeEncryptedConnection: Got encryption request. Universe: {universe:?} Protocol ver: {protocol_version}"
326    );
327
328    // Only logged (SteamKit2 does the same); a mismatch doesn't abort the handshake.
329    if protocol_version != PROTOCOL_VERSION {
330        tracing::debug!(
331            "EnvelopeEncryptedConnection: Encryption handshake protocol version mismatch!"
332        );
333    }
334
335    if universe != shared.universe {
336        tracing::debug!(
337            "EnvelopeEncryptedConnection: Expected universe {:?} but server reported universe {universe:?}",
338            shared.universe
339        );
340    }
341
342    let random_challenge = &data[MSG_HDR_LENGTH + 8..];
343    if random_challenge.len() < MIN_CHALLENGE_LENGTH {
344        tracing::debug!(
345            "EnvelopeEncryptedConnection: Encryption handshake has a payload that is less than 16 bytes"
346        );
347        shared.inner.disconnect(false).await;
348        return;
349    }
350
351    let Some(public_key) = public_key_for_universe(universe) else {
352        tracing::debug!(
353            "EnvelopeEncryptedConnection: HandleEncryptRequest got request for invalid universe!"
354        );
355        shared.inner.disconnect(false).await;
356        return;
357    };
358
359    let mut temp_session_key = [0u8; SESSION_KEY_LENGTH];
360    rand::thread_rng().fill_bytes(&mut temp_session_key);
361
362    let mut blob_to_encrypt = Vec::with_capacity(SESSION_KEY_LENGTH + random_challenge.len());
363    blob_to_encrypt.extend_from_slice(&temp_session_key);
364    blob_to_encrypt.extend_from_slice(random_challenge);
365
366    let encrypted_handshake_blob = match rsa_encrypt_oaep_sha1(public_key, &blob_to_encrypt) {
367        Ok(blob) => blob,
368        Err(err) => {
369            tracing::debug!("EnvelopeEncryptedConnection: {err}");
370            shared.inner.disconnect(false).await;
371            return;
372        }
373    };
374
375    let key_crc = crc32fast::hash(&encrypted_handshake_blob);
376
377    let encryption = match NetFilterEncryptionWithHMAC::new(&temp_session_key) {
378        Ok(encryption) => encryption,
379        Err(err) => {
380            tracing::debug!("EnvelopeEncryptedConnection: {err}");
381            shared.inner.disconnect(false).await;
382            return;
383        }
384    };
385
386    let serialized = build_channel_encrypt_response(&encrypted_handshake_blob, key_crc);
387
388    if let Some(listener) = &shared.debug_network_listener {
389        listener.on_outgoing_network_message(EMsg::ChannelEncryptResponse, &serialized);
390    }
391
392    *lock(&shared.encryption) = Some(Arc::new(encryption));
393    *lock(&shared.state) = EncryptionState::Challenged;
394
395    let _ = shared.inner.send(Bytes::from(serialized)).await;
396}
397
398async fn handle_encrypt_result(shared: &Arc<EnvelopeShared>, data: &[u8]) {
399    if data.len() < MSG_HDR_LENGTH + 4 {
400        shared.inner.disconnect(false).await;
401        return;
402    }
403
404    let result = EResult::from_value(read_i32(data, MSG_HDR_LENGTH)).unwrap_or(EResult::Invalid);
405    tracing::debug!("EnvelopeEncryptedConnection: Encryption result: {result:?}");
406
407    let has_encryption = lock(&shared.encryption).is_some();
408
409    if result == EResult::OK && has_encryption {
410        *lock(&shared.state) = EncryptionState::Encrypted;
411        shared.events.emit_connected();
412    } else {
413        tracing::debug!("EnvelopeEncryptedConnection: Encryption channel setup failed");
414        shared.inner.disconnect(false).await;
415    }
416}
417
418fn is_expected_emsg(state: EncryptionState, msg: EMsg) -> bool {
419    match state {
420        EncryptionState::Disconnected => false,
421        EncryptionState::Connected => msg == EMsg::ChannelEncryptRequest,
422        EncryptionState::Challenged => msg == EMsg::ChannelEncryptResult,
423        EncryptionState::Encrypted => true,
424    }
425}
426
427fn parse_msg_type(data: &[u8]) -> Option<EMsg> {
428    if data.len() < 4 {
429        return None;
430    }
431
432    let raw = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
433    EMsg::from_value((raw & !PROTO_MASK) as i32)
434}
435
436fn build_channel_encrypt_response(encrypted_handshake_blob: &[u8], key_crc: u32) -> Vec<u8> {
437    let mut serialized = Vec::with_capacity(MSG_HDR_LENGTH + 8 + encrypted_handshake_blob.len() + 8);
438    serialized.extend_from_slice(&(EMsg::ChannelEncryptResponse as i32).to_le_bytes());
439    serialized.extend_from_slice(&u64::MAX.to_le_bytes());
440    serialized.extend_from_slice(&u64::MAX.to_le_bytes());
441    serialized.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
442    serialized.extend_from_slice(&KEY_SIZE.to_le_bytes());
443    serialized.extend_from_slice(encrypted_handshake_blob);
444    serialized.extend_from_slice(&key_crc.to_le_bytes());
445    serialized.extend_from_slice(&0u32.to_le_bytes());
446    serialized
447}
448
449fn rsa_encrypt_oaep_sha1(
450    public_key: &[u8],
451    blob_to_encrypt: &[u8],
452) -> Result<Vec<u8>, SteamError> {
453    let key = RsaPublicKey::from_public_key_der(public_key)
454        .map_err(|err| SteamError::Crypto(format!("invalid RSA public key: {err}")))?;
455
456    let padding = Oaep::new::<Sha1>();
457    let mut rng = rand::thread_rng();
458
459    key.encrypt(&mut rng, padding, blob_to_encrypt)
460        .map_err(|err| SteamError::Crypto(format!("RSA encryption failed: {err}")))
461}
462
463fn read_u32(data: &[u8], offset: usize) -> u32 {
464    u32::from_le_bytes([
465        data[offset],
466        data[offset + 1],
467        data[offset + 2],
468        data[offset + 3],
469    ])
470}
471
472fn read_i32(data: &[u8], offset: usize) -> i32 {
473    i32::from_le_bytes([
474        data[offset],
475        data[offset + 1],
476        data[offset + 2],
477        data[offset + 3],
478    ])
479}
480
481fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
482    match mutex.lock() {
483        Ok(guard) => guard,
484        Err(poisoned) => poisoned.into_inner(),
485    }
486}
487
488/// The Steam public key for `universe`, from [`KeyDictionary`].
489pub fn public_key_for_universe(universe: EUniverse) -> Option<&'static [u8]> {
490    KeyDictionary::get_public_key(universe)
491}