Skip to main content

steam_crypto/
rsa_encryption.rs

1//! RSA encryption for Steam session key exchange.
2//!
3//! This module implements the RSA encryption used during the TCP ChannelEncrypt
4//! handshake. The client generates a 32-byte session key, encrypts it with
5//! Steam's public RSA key, and sends it to the server.
6
7use rand::RngCore;
8use rsa::{Oaep, RsaPublicKey};
9use sha1::Sha1;
10
11use crate::{error::CryptoError, session_key::SessionKey};
12
13/// Steam's public RSA key for session key encryption.
14/// This is the same key used by all Steam clients for the encryption handshake.
15const STEAM_PUBLIC_KEY_MODULUS: &[u8] = &[
16    0xDF, 0xEC, 0x1A, 0xD6, 0x2C, 0x10, 0x66, 0x2C, 0x17, 0x35, 0x3A, 0x14, 0xB0, 0x7C, 0x59, 0x11, 0x7F, 0x9D, 0xD3, 0xD8, 0x2B, 0x7A, 0xE3, 0xE0, 0x15, 0xCD, 0x19, 0x1E, 0x46, 0xE8, 0x7B, 0x87, 0x74, 0xA2, 0x18, 0x46, 0x31, 0xA9, 0x03, 0x14, 0x79, 0x82, 0x1F, 0x11, 0x13, 0xF4, 0xC0, 0xCE, 0x63, 0x1F, 0x73, 0x53, 0xD0, 0x5C, 0x82, 0xD5, 0x14, 0x9C, 0x1E, 0xB8, 0x67, 0xE9, 0x5B, 0xF7, 0x0F, 0xD5, 0x51, 0x40, 0x11, 0x4E, 0xF9, 0x75, 0x6D, 0x29, 0x00, 0x10, 0xB4, 0xF6, 0x0E, 0x7F, 0x79, 0xE5, 0x67, 0xE7, 0x62, 0x25, 0x9E, 0xC7, 0x3B, 0xAB, 0x19, 0x7C, 0xD2, 0xF9, 0x18, 0x51, 0xBF, 0x68, 0x6E,
17    0xA5, 0x30, 0x6B, 0x00, 0x63, 0x1A, 0x5A, 0x1E, 0x1C, 0x11, 0x75, 0xC4, 0x15, 0xD9, 0x3C, 0xE0, 0xF5, 0x97, 0xF6, 0xE6, 0x08, 0x27, 0xFE, 0xA6, 0xF4, 0x08, 0x8C, 0xD8, 0x59,
18];
19
20const STEAM_PUBLIC_KEY_EXPONENT: u32 = 0x11; // 17
21
22/// Result of generating a session key for the encryption handshake.
23pub struct SessionKeyPair {
24    /// The plain 32-byte session key for symmetric encryption.
25    pub plain: SessionKey,
26    /// The RSA-encrypted session key to send to the server.
27    pub encrypted: Vec<u8>,
28}
29
30/// Generate a new session key and encrypt it with Steam's public key.
31///
32/// This is used during the TCP ChannelEncrypt handshake:
33/// 1. Server sends ChannelEncryptRequest with a 16-byte nonce
34/// 2. Client generates 32-byte session key, XORs first 16 bytes with nonce
35/// 3. Client encrypts session key with Steam's RSA public key
36/// 4. Client sends encrypted key in ChannelEncryptResponse
37///
38/// # Arguments
39/// * `nonce` - The 16-byte nonce received from the server
40///
41/// # Returns
42/// A [`SessionKeyPair`] containing both the plain and encrypted keys.
43pub fn generate_session_key(nonce: &[u8; 16]) -> Result<SessionKeyPair, CryptoError> {
44    // Generate a random 32-byte session key
45    let mut key_bytes = [0u8; 32];
46    rand::rng().fill_bytes(&mut key_bytes);
47
48    // XOR the first 16 bytes with the server's nonce
49    for i in 0..16 {
50        key_bytes[i] ^= nonce[i];
51    }
52
53    // Build the RSA public key
54    let n = rsa::BigUint::from_bytes_be(STEAM_PUBLIC_KEY_MODULUS);
55    let e = rsa::BigUint::from(STEAM_PUBLIC_KEY_EXPONENT);
56    let public_key = RsaPublicKey::new(n, e).map_err(|e| CryptoError::EncryptionFailed(format!("Invalid RSA key: {}", e)))?;
57
58    // Encrypt with RSA-OAEP using SHA1
59    let padding = Oaep::new::<Sha1>();
60
61    // Compatibility wrapper for rand_core 0.6 required by rsa 0.9
62    struct RandCore6Wrapper;
63    impl rsa::rand_core::RngCore for RandCore6Wrapper {
64        fn next_u32(&mut self) -> u32 {
65            rand::random()
66        }
67        fn next_u64(&mut self) -> u64 {
68            rand::random()
69        }
70        fn fill_bytes(&mut self, dest: &mut [u8]) {
71            rand::rng().fill_bytes(dest)
72        }
73        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::rand_core::Error> {
74            rand::rng().fill_bytes(dest);
75            Ok(())
76        }
77    }
78    impl rsa::rand_core::CryptoRng for RandCore6Wrapper {}
79
80    let mut rng = RandCore6Wrapper;
81    let encrypted = public_key.encrypt(&mut rng, padding, &key_bytes).map_err(|e| CryptoError::EncryptionFailed(format!("RSA encryption failed: {}", e)))?;
82
83    // Create the plain session key (without nonce XOR for actual use)
84    // The server will XOR with nonce on its side
85    let plain = SessionKey::from_bytes(&key_bytes).ok_or_else(|| CryptoError::EncryptionFailed("Invalid session key length".into()))?;
86
87    Ok(SessionKeyPair { plain, encrypted })
88}
89
90/// Calculate CRC32 checksum of the encrypted session key.
91///
92/// This is sent as part of ChannelEncryptResponse for verification.
93pub fn calculate_key_crc(encrypted_key: &[u8]) -> u32 {
94    crc32fast::hash(encrypted_key)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_generate_session_key() {
103        let nonce = [0u8; 16];
104        let result = generate_session_key(&nonce).unwrap();
105
106        // Encrypted key should be 128 bytes (1024-bit RSA)
107        assert_eq!(result.encrypted.len(), 128);
108
109        // Plain key should be 32 bytes
110        assert_eq!(result.plain.as_bytes().len(), 32);
111    }
112
113    #[test]
114    fn test_key_crc() {
115        let data = b"test data";
116        let crc = calculate_key_crc(data);
117        assert!(crc != 0);
118    }
119}