deepslate 0.3.1

A high-performance Minecraft server proxy written in Rust.
Documentation
//! Velocity modern player forwarding protocol.
//!
//! When connecting to a backend server that supports Velocity forwarding, the
//! backend sends a `LoginPluginMessage` on channel `velocity:player_info`.
//! We respond with an HMAC-SHA256-signed payload containing the player's
//! IP address, UUID, name, and profile properties.

use bytes::BufMut;
use hmac::{Hmac, Mac};
use sha2::Sha256;

use deepslate_protocol::types::{self, GameProfile};
use deepslate_protocol::varint;

/// The Velocity player info forwarding channel identifier.
pub const VELOCITY_FORWARDING_CHANNEL: &str = "velocity:player_info";

/// Velocity modern forwarding version constants.
pub mod version {
    /// Default forwarding: IP, UUID, name, properties.
    pub const MODERN_DEFAULT: i32 = 1;
    /// Adds player key (1.19+, `GENERIC_V1` keys).
    pub const MODERN_WITH_KEY: i32 = 2;
    /// Adds signature holder UUID (1.19.1+, `LINKED_V2` keys).
    pub const MODERN_WITH_KEY_V2: i32 = 3;
    /// Lazy session: no explicit key forwarding (1.19.3+).
    pub const MODERN_LAZY_SESSION: i32 = 4;
    /// Maximum supported forwarding version.
    pub const MODERN_MAX: i32 = MODERN_LAZY_SESSION;
}

/// Create the signed forwarding data payload for Velocity modern forwarding.
///
/// The format is:
/// ```text
/// [HMAC-SHA256 signature (32 bytes)][forwarding data]
/// ```
///
/// The forwarding data contains:
/// ```text
/// [VarInt version][String address][UUID uuid][String name][Properties[]]
/// ```
///
/// Since we only support 1.21+, we always use `MODERN_LAZY_SESSION` (version 4)
/// which does not include player key data.
type HmacSha256 = Hmac<Sha256>;

/// # Panics
///
/// This function will not panic under normal usage. The HMAC construction
/// uses `expect` but `HMAC-SHA256` accepts keys of any size.
#[must_use]
pub fn create_forwarding_data(
    secret: &[u8],
    player_address: &str,
    profile: &GameProfile,
    requested_version: i32,
) -> Vec<u8> {
    // Determine the actual forwarding version.
    // Since we only support 1.21+ (>= 1.19.3), we use MODERN_LAZY_SESSION
    // unless the backend requests a lower version.
    let actual_version = if requested_version >= version::MODERN_LAZY_SESSION {
        version::MODERN_LAZY_SESSION
    } else {
        // For older backends that only understand v1, fall back gracefully.
        version::MODERN_DEFAULT
    };

    // Build the forwarding data (without the HMAC prefix)
    let mut data = Vec::with_capacity(256);
    varint::write_var_int(&mut data, actual_version);
    types::write_string(&mut data, player_address);
    types::write_uuid(&mut data, profile.id);
    types::write_string(&mut data, &profile.name);
    types::write_properties(&mut data, &profile.properties);

    // Compute HMAC-SHA256
    let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
    mac.update(&data);
    let signature = mac.finalize().into_bytes();

    // Prepend signature to data
    let mut result = Vec::with_capacity(32 + data.len());
    result.put_slice(&signature);
    result.extend_from_slice(&data);
    result
}

#[cfg(test)]
mod tests {
    use uuid::Uuid;

    use deepslate_protocol::types::ProfileProperty;

    use super::*;

    #[test]
    fn test_create_forwarding_data_structure() {
        let secret = b"test-secret";
        let profile = GameProfile {
            id: Uuid::parse_str("069a79f4-44e9-4726-a5be-fca90e38aaf5").unwrap(),
            name: "Notch".to_string(),
            properties: vec![ProfileProperty {
                name: "textures".to_string(),
                value: "base64data".to_string(),
                signature: None,
            }],
        };

        let data = create_forwarding_data(secret, "127.0.0.1", &profile, version::MODERN_MAX);

        // First 32 bytes are HMAC-SHA256 signature
        assert!(data.len() > 32);

        // Verify the data after the signature starts with the version VarInt
        let forwarding_data = &data[32..];
        let mut cursor = forwarding_data;
        let ver = deepslate_protocol::varint::read_var_int(&mut cursor).unwrap();
        assert_eq!(ver, version::MODERN_LAZY_SESSION);
    }

    #[test]
    fn test_hmac_is_deterministic() {
        let secret = b"my-secret";
        let profile = GameProfile {
            id: Uuid::nil(),
            name: "Steve".to_string(),
            properties: vec![],
        };

        let d1 = create_forwarding_data(secret, "10.0.0.1", &profile, version::MODERN_MAX);
        let d2 = create_forwarding_data(secret, "10.0.0.1", &profile, version::MODERN_MAX);
        assert_eq!(d1, d2);
    }

    #[test]
    fn test_different_secret_produces_different_hmac() {
        let profile = GameProfile {
            id: Uuid::nil(),
            name: "Steve".to_string(),
            properties: vec![],
        };

        let d1 = create_forwarding_data(b"secret-a", "10.0.0.1", &profile, version::MODERN_MAX);
        let d2 = create_forwarding_data(b"secret-b", "10.0.0.1", &profile, version::MODERN_MAX);
        assert_ne!(d1[..32], d2[..32]); // Different HMAC
        assert_eq!(d1[32..], d2[32..]); // Same forwarding data
    }
}