steam-crypto-rs 0.1.2

Steam encryption and cryptographic utilities (AES-256-CBC, RSA-OAEP, HMAC-IV) for the Steam protocol.
Documentation
//! RSA encryption for Steam session key exchange.
//!
//! This module implements the RSA encryption used during the TCP ChannelEncrypt
//! handshake. The client generates a 32-byte session key, encrypts it with
//! Steam's public RSA key, and sends it to the server.

use rand::RngCore;
use rsa::{Oaep, RsaPublicKey};
use sha1::Sha1;

use crate::{error::CryptoError, session_key::SessionKey};

/// Steam's public RSA key for session key encryption.
/// This is the same key used by all Steam clients for the encryption handshake.
const STEAM_PUBLIC_KEY_MODULUS: &[u8] = &[
    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,
    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,
];

const STEAM_PUBLIC_KEY_EXPONENT: u32 = 0x11; // 17

/// Result of generating a session key for the encryption handshake.
pub struct SessionKeyPair {
    /// The plain 32-byte session key for symmetric encryption.
    pub plain: SessionKey,
    /// The RSA-encrypted session key to send to the server.
    pub encrypted: Vec<u8>,
}

/// Generate a new session key and encrypt it with Steam's public key.
///
/// This is used during the TCP ChannelEncrypt handshake:
/// 1. Server sends ChannelEncryptRequest with a 16-byte nonce
/// 2. Client generates 32-byte session key, XORs first 16 bytes with nonce
/// 3. Client encrypts session key with Steam's RSA public key
/// 4. Client sends encrypted key in ChannelEncryptResponse
///
/// # Arguments
/// * `nonce` - The 16-byte nonce received from the server
///
/// # Returns
/// A [`SessionKeyPair`] containing both the plain and encrypted keys.
pub fn generate_session_key(nonce: &[u8; 16]) -> Result<SessionKeyPair, CryptoError> {
    // Generate a random 32-byte session key
    let mut key_bytes = [0u8; 32];
    rand::rng().fill_bytes(&mut key_bytes);

    // XOR the first 16 bytes with the server's nonce
    for i in 0..16 {
        key_bytes[i] ^= nonce[i];
    }

    // Build the RSA public key
    let n = rsa::BigUint::from_bytes_be(STEAM_PUBLIC_KEY_MODULUS);
    let e = rsa::BigUint::from(STEAM_PUBLIC_KEY_EXPONENT);
    let public_key = RsaPublicKey::new(n, e).map_err(|e| CryptoError::EncryptionFailed(format!("Invalid RSA key: {}", e)))?;

    // Encrypt with RSA-OAEP using SHA1
    let padding = Oaep::new::<Sha1>();

    // Compatibility wrapper for rand_core 0.6 required by rsa 0.9
    struct RandCore6Wrapper;
    impl rsa::rand_core::RngCore for RandCore6Wrapper {
        fn next_u32(&mut self) -> u32 {
            rand::random()
        }
        fn next_u64(&mut self) -> u64 {
            rand::random()
        }
        fn fill_bytes(&mut self, dest: &mut [u8]) {
            rand::rng().fill_bytes(dest)
        }
        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::rand_core::Error> {
            rand::rng().fill_bytes(dest);
            Ok(())
        }
    }
    impl rsa::rand_core::CryptoRng for RandCore6Wrapper {}

    let mut rng = RandCore6Wrapper;
    let encrypted = public_key.encrypt(&mut rng, padding, &key_bytes).map_err(|e| CryptoError::EncryptionFailed(format!("RSA encryption failed: {}", e)))?;

    // Create the plain session key (without nonce XOR for actual use)
    // The server will XOR with nonce on its side
    let plain = SessionKey::from_bytes(&key_bytes).ok_or_else(|| CryptoError::EncryptionFailed("Invalid session key length".into()))?;

    Ok(SessionKeyPair { plain, encrypted })
}

/// Calculate CRC32 checksum of the encrypted session key.
///
/// This is sent as part of ChannelEncryptResponse for verification.
pub fn calculate_key_crc(encrypted_key: &[u8]) -> u32 {
    crc32fast::hash(encrypted_key)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_session_key() {
        let nonce = [0u8; 16];
        let result = generate_session_key(&nonce).unwrap();

        // Encrypted key should be 128 bytes (1024-bit RSA)
        assert_eq!(result.encrypted.len(), 128);

        // Plain key should be 32 bytes
        assert_eq!(result.plain.as_bytes().len(), 32);
    }

    #[test]
    fn test_key_crc() {
        let data = b"test data";
        let crc = calculate_key_crc(data);
        assert!(crc != 0);
    }
}