deepslate 0.3.1

A high-performance Minecraft server proxy written in Rust.
Documentation
//! RSA key pair management and Minecraft's "Notchian" server ID hash.

use num_bigint::BigInt;
use rsa::pkcs8::EncodePublicKey;
use rsa::rand_core::OsRng;
use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use sha1::Digest;
use sha1::Sha1;

/// RSA key pair used for client authentication.
pub struct ServerKeyPair {
    /// The RSA private key.
    private_key: RsaPrivateKey,
    /// DER-encoded public key (cached for sending to clients).
    public_key_der: Vec<u8>,
}

impl ServerKeyPair {
    /// Generate a new 1024-bit RSA key pair.
    ///
    /// Minecraft uses 1024-bit RSA (mandated by the protocol).
    ///
    /// # Errors
    ///
    /// Returns an error if key generation fails.
    pub fn generate() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        let private_key = RsaPrivateKey::new(&mut OsRng, 1024)?;
        let public_key = RsaPublicKey::from(&private_key);
        let public_key_der = public_key.to_public_key_der()?.to_vec();

        Ok(Self {
            private_key,
            public_key_der,
        })
    }

    /// Get the DER-encoded public key bytes (X.509/SPKI format).
    #[must_use]
    pub fn public_key_der(&self) -> &[u8] {
        &self.public_key_der
    }

    /// Decrypt data encrypted with the public key (PKCS1v1.5 padding).
    ///
    /// Used to decrypt the shared secret and verify token from the client.
    ///
    /// # Errors
    ///
    /// Returns an error if decryption fails (wrong key, bad padding, etc.).
    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, rsa::Error> {
        self.private_key.decrypt(Pkcs1v15Encrypt, ciphertext)
    }
}

/// Compute the Minecraft "Notchian" server ID hash.
///
/// This is `SHA1(shared_secret + public_key_der)` rendered as a signed
/// two's-complement hexadecimal string (Java's `BigInteger.toString(16)`).
/// The hash may have a leading `-` for negative values and has no leading
/// zero padding.
#[must_use]
pub fn generate_server_id(shared_secret: &[u8], public_key_der: &[u8]) -> String {
    let mut hasher = Sha1::new();
    hasher.update(shared_secret);
    hasher.update(public_key_der);
    let hash = hasher.finalize();

    // Interpret as a signed big integer (two's complement) and format as hex.
    // This matches Java's `new BigInteger(digest).toString(16)`.
    let bigint = BigInt::from_signed_bytes_be(&hash);
    bigint.to_str_radix(16)
}

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

    #[test]
    fn test_generate_key_pair() {
        let kp = ServerKeyPair::generate().unwrap();
        assert!(!kp.public_key_der().is_empty());
        // DER-encoded 1024-bit RSA public key should be ~162 bytes
        assert!(kp.public_key_der().len() > 100);
    }

    #[test]
    fn test_encrypt_decrypt_roundtrip() {
        let kp = ServerKeyPair::generate().unwrap();
        let plaintext = b"Hello, Minecraft!";
        let public_key = RsaPublicKey::from(&kp.private_key);
        let ciphertext = public_key
            .encrypt(&mut OsRng, Pkcs1v15Encrypt, plaintext)
            .unwrap();
        let decrypted = kp.decrypt(&ciphertext).unwrap();
        assert_eq!(decrypted, plaintext);
    }

    #[test]
    fn test_server_id_hash_twos_complement() {
        // Verify the format is correct (may start with '-' for negative BigIntegers)
        let hash = generate_server_id(&[0xFF; 16], &[0xFF; 16]);
        assert!(!hash.is_empty());
        assert!(hash.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
    }
}