steam-crypto-rs 0.1.2

Steam encryption and cryptographic utilities (AES-256-CBC, RSA-OAEP, HMAC-IV) for the Steam protocol.
Documentation
//! Symmetric encryption for Steam messages.

use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use rand::RngCore;

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

type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;

/// Encrypt a message using the session key.
///
/// The encrypted format is: IV (16 bytes) + encrypted_data
pub fn encrypt_message(session_key: &SessionKey, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
    // Generate random IV
    let mut iv = [0u8; 16];
    rand::rng().fill_bytes(&mut iv);

    // Calculate padded length and create buffer
    let block_size = 16;
    let padded_len = ((plaintext.len() / block_size) + 1) * block_size;
    let mut buffer = vec![0u8; padded_len];
    buffer[..plaintext.len()].copy_from_slice(plaintext);

    // Create cipher and encrypt with PKCS7 padding
    let cipher = Aes256CbcEnc::new(session_key.as_bytes().into(), &iv.into());
    let ciphertext = cipher.encrypt_padded_mut::<Pkcs7>(&mut buffer, plaintext.len()).map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;

    // Prepend IV
    let mut result = iv.to_vec();
    result.extend_from_slice(ciphertext);
    Ok(result)
}

/// Decrypt a message using the session key.
///
/// Expects format: IV (16 bytes) + encrypted_data
pub fn decrypt_message(session_key: &SessionKey, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
    if ciphertext.len() < 16 {
        return Err(CryptoError::DecryptionFailed("Ciphertext too short".into()));
    }

    // Extract IV
    let iv: [u8; 16] = ciphertext[..16].try_into().unwrap();
    let mut encrypted = ciphertext[16..].to_vec();

    // Decrypt
    let cipher = Aes256CbcDec::new(session_key.as_bytes().into(), &iv.into());
    let plaintext = cipher.decrypt_padded_mut::<Pkcs7>(&mut encrypted).map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;

    Ok(plaintext.to_vec())
}

/// Encrypt a message using HMAC-IV encryption.
///
/// This is the encryption method used for TCP connections after the handshake.
/// The IV is derived from HMAC-SHA1 of the plaintext using the HMAC portion of
/// the session key.
///
/// Format: IV (16 bytes) + encrypted_data
pub fn encrypt_with_hmac_iv(session_key: &SessionKey, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
    use hmac::{Hmac, Mac};
    use sha1::Sha1;

    // Generate IV from HMAC-SHA1 of plaintext
    let mut mac = Hmac::<Sha1>::new_from_slice(session_key.hmac_key()).map_err(|e| CryptoError::EncryptionFailed(format!("HMAC init failed: {}", e)))?;
    mac.update(plaintext);
    let hmac_result = mac.finalize().into_bytes();

    // Use first 13 bytes of HMAC + 3 random bytes as IV
    let mut iv = [0u8; 16];
    iv[..13].copy_from_slice(&hmac_result[..13]);
    rand::rng().fill_bytes(&mut iv[13..]);

    // Encrypt with AES-256-CBC
    let block_size = 16;
    let padded_len = ((plaintext.len() / block_size) + 1) * block_size;
    let mut buffer = vec![0u8; padded_len];
    buffer[..plaintext.len()].copy_from_slice(plaintext);

    let cipher = Aes256CbcEnc::new(session_key.as_bytes().into(), &iv.into());
    let ciphertext = cipher.encrypt_padded_mut::<Pkcs7>(&mut buffer, plaintext.len()).map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;

    // Prepend IV
    let mut result = iv.to_vec();
    result.extend_from_slice(ciphertext);
    Ok(result)
}

/// Decrypt a message with HMAC-IV verification.
///
/// This verifies message integrity by checking that the IV matches the HMAC of
/// the decrypted content. Used for TCP connections.
///
/// Expects format: IV (16 bytes) + encrypted_data
pub fn decrypt_with_hmac_iv(session_key: &SessionKey, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
    use hmac::{Hmac, Mac};
    use sha1::Sha1;

    if ciphertext.len() < 16 {
        return Err(CryptoError::DecryptionFailed("Ciphertext too short".into()));
    }

    // Extract IV
    let iv: [u8; 16] = ciphertext[..16].try_into().unwrap();
    let mut encrypted = ciphertext[16..].to_vec();

    // Decrypt
    let cipher = Aes256CbcDec::new(session_key.as_bytes().into(), &iv.into());
    let plaintext = cipher.decrypt_padded_mut::<Pkcs7>(&mut encrypted).map_err(|e| CryptoError::DecryptionFailed(e.to_string()))?;

    // Verify HMAC (first 13 bytes of IV should match HMAC of plaintext)
    let mut mac = Hmac::<Sha1>::new_from_slice(session_key.hmac_key()).map_err(|e| CryptoError::DecryptionFailed(format!("HMAC init failed: {}", e)))?;
    mac.update(plaintext);
    let hmac_result = mac.finalize().into_bytes();

    if iv[..13] != hmac_result[..13] {
        return Err(CryptoError::DecryptionFailed("HMAC verification failed".into()));
    }

    Ok(plaintext.to_vec())
}

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

    #[test]
    fn test_encrypt_decrypt_roundtrip() {
        let key = SessionKey::generate();
        let plaintext = b"Hello, Steam!";

        let encrypted = encrypt_message(&key, plaintext).unwrap();
        let decrypted = decrypt_message(&key, &encrypted).unwrap();

        assert_eq!(decrypted, plaintext);
    }
}