#![warn(missing_docs)]
#![deny(unsafe_code)]
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit};
use rsa::Pkcs1v15Encrypt;
use rsa::pkcs8::EncodePublicKey;
use rsa::{RsaPrivateKey, RsaPublicKey};
use sha1::{Digest, Sha1};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CryptoError {
#[error("RSA key generation failed: {0}")]
KeyGeneration(String),
#[error("RSA decryption failed: {0}")]
Decryption(String),
#[error("invalid shared secret length: expected 16, got {0}")]
InvalidSecretLength(usize),
#[error("public key encoding failed: {0}")]
PublicKeyEncoding(String),
}
pub struct ServerKeyPair {
private_key: RsaPrivateKey,
public_key: RsaPublicKey,
public_key_der: Vec<u8>,
}
impl ServerKeyPair {
pub fn generate() -> Result<Self, CryptoError> {
let mut rng = rsa::rand_core::OsRng;
let private_key = RsaPrivateKey::new(&mut rng, 1024)
.map_err(|e| CryptoError::KeyGeneration(e.to_string()))?;
let public_key = RsaPublicKey::from(&private_key);
let public_key_der = public_key
.to_public_key_der()
.map_err(|e| CryptoError::PublicKeyEncoding(e.to_string()))?
.to_vec();
Ok(Self {
private_key,
public_key,
public_key_der,
})
}
pub fn public_key_der(&self) -> &[u8] {
&self.public_key_der
}
pub fn public_key(&self) -> &RsaPublicKey {
&self.public_key
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
self.private_key
.decrypt(Pkcs1v15Encrypt, ciphertext)
.map_err(|e| CryptoError::Decryption(e.to_string()))
}
pub fn decrypt_shared_secret(&self, encrypted_secret: &[u8]) -> Result<[u8; 16], CryptoError> {
let decrypted = self.decrypt(encrypted_secret)?;
decrypted
.try_into()
.map_err(|v: Vec<u8>| CryptoError::InvalidSecretLength(v.len()))
}
}
impl std::fmt::Debug for ServerKeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerKeyPair")
.field("public_key_der_len", &self.public_key_der.len())
.finish()
}
}
pub struct CipherState {
cipher: Aes128,
enc_iv: [u8; 16],
dec_iv: [u8; 16],
}
impl CipherState {
pub fn new(shared_secret: &[u8; 16]) -> Self {
Self {
cipher: Aes128::new(shared_secret.into()),
enc_iv: *shared_secret,
dec_iv: *shared_secret,
}
}
pub fn decrypt(&mut self, data: &mut [u8]) {
for byte in data.iter_mut() {
let mut block = self.dec_iv.into();
self.cipher.encrypt_block(&mut block);
let ciphertext_byte = *byte;
*byte ^= block[0];
self.dec_iv.copy_within(1.., 0);
self.dec_iv[15] = ciphertext_byte;
}
}
pub fn encrypt(&mut self, data: &mut [u8]) {
for byte in data.iter_mut() {
let mut block = self.enc_iv.into();
self.cipher.encrypt_block(&mut block);
*byte ^= block[0];
self.enc_iv.copy_within(1.., 0);
self.enc_iv[15] = *byte;
}
}
pub fn split(self) -> (DecryptCipher, EncryptCipher) {
(
DecryptCipher {
cipher: self.cipher.clone(),
iv: self.dec_iv,
},
EncryptCipher {
cipher: self.cipher,
iv: self.enc_iv,
},
)
}
}
impl std::fmt::Debug for CipherState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CipherState").finish()
}
}
pub struct DecryptCipher {
cipher: Aes128,
iv: [u8; 16],
}
impl DecryptCipher {
pub fn decrypt(&mut self, data: &mut [u8]) {
for byte in data.iter_mut() {
let mut block = self.iv.into();
self.cipher.encrypt_block(&mut block);
let ciphertext_byte = *byte;
*byte ^= block[0];
self.iv.copy_within(1.., 0);
self.iv[15] = ciphertext_byte;
}
}
}
impl std::fmt::Debug for DecryptCipher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DecryptCipher").finish()
}
}
pub struct EncryptCipher {
cipher: Aes128,
iv: [u8; 16],
}
impl EncryptCipher {
pub fn encrypt(&mut self, data: &mut [u8]) {
for byte in data.iter_mut() {
let mut block = self.iv.into();
self.cipher.encrypt_block(&mut block);
*byte ^= block[0];
self.iv.copy_within(1.., 0);
self.iv[15] = *byte;
}
}
}
impl std::fmt::Debug for EncryptCipher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EncryptCipher").finish()
}
}
pub fn minecraft_digest(server_id: &str, shared_secret: &[u8], public_key_der: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(server_id.as_bytes());
hasher.update(shared_secret);
hasher.update(public_key_der);
let hash = hasher.finalize();
let negative = hash[0] & 0x80 != 0;
if negative {
let mut bytes = hash.to_vec();
let mut carry = true;
for byte in bytes.iter_mut().rev() {
*byte = !*byte;
if carry {
let (result, overflow) = byte.overflowing_add(1);
*byte = result;
carry = overflow;
}
}
let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
let trimmed = hex.trim_start_matches('0');
format!("-{trimmed}")
} else {
let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
let trimmed = hex.trim_start_matches('0');
if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
}
}
}
pub fn offline_uuid(name: &str) -> uuid::Uuid {
use md5::{Digest as Md5Digest, Md5};
let input = format!("OfflinePlayer:{name}");
let hash = Md5::digest(input.as_bytes());
let mut bytes: [u8; 16] = hash.into();
bytes[6] = (bytes[6] & 0x0f) | 0x30;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
uuid::Uuid::from_bytes(bytes)
}
pub fn generate_challenge() -> [u8; 4] {
use rand::RngExt;
let mut buf = [0u8; 4];
rand::rng().fill(&mut buf[..]);
buf
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use rsa::rand_core::OsRng;
#[test]
fn test_cipher_encrypt_decrypt_roundtrip() {
let secret = [0x42u8; 16];
let mut cipher_enc = CipherState::new(&secret);
let mut cipher_dec = CipherState::new(&secret);
let original = b"Hello, Minecraft!".to_vec();
let mut data = original.clone();
cipher_enc.encrypt(&mut data);
assert_ne!(data, original, "encrypted data should differ from original");
cipher_dec.decrypt(&mut data);
assert_eq!(data, original, "decrypted data should match original");
}
#[test]
fn test_cipher_is_stateful() {
let secret = [0xAB; 16];
let mut cipher = CipherState::new(&secret);
let mut data1 = b"test".to_vec();
cipher.encrypt(&mut data1);
let mut data2 = b"test".to_vec();
cipher.encrypt(&mut data2);
assert_ne!(
data1, data2,
"same plaintext encrypted twice should differ (stateful cipher)"
);
}
#[test]
fn test_cipher_multi_chunk_roundtrip() {
let secret = [0x13; 16];
let mut enc = CipherState::new(&secret);
let mut dec = CipherState::new(&secret);
let chunk1 = b"first chunk ".to_vec();
let chunk2 = b"second chunk".to_vec();
let mut enc1 = chunk1.clone();
enc.encrypt(&mut enc1);
let mut enc2 = chunk2.clone();
enc.encrypt(&mut enc2);
dec.decrypt(&mut enc1);
dec.decrypt(&mut enc2);
assert_eq!(enc1, chunk1);
assert_eq!(enc2, chunk2);
}
#[test]
fn test_cipher_empty_data() {
let secret = [0x00; 16];
let mut cipher = CipherState::new(&secret);
let mut data = Vec::new();
cipher.encrypt(&mut data); cipher.decrypt(&mut data); }
#[test]
fn test_rsa_keygen_and_der() {
let keypair = ServerKeyPair::generate().expect("key generation");
assert!(
keypair.public_key_der().len() > 100,
"public key DER should be > 100 bytes"
);
assert!(
keypair.public_key_der().len() < 300,
"public key DER should be < 300 bytes"
);
}
#[test]
fn test_rsa_encrypt_decrypt_roundtrip() {
let keypair = ServerKeyPair::generate().expect("key generation");
let plaintext = b"shared_secret!!!";
let ciphertext = keypair
.public_key
.encrypt(&mut OsRng, Pkcs1v15Encrypt, plaintext)
.expect("encryption");
let decrypted = keypair.decrypt(&ciphertext).expect("decryption");
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_decrypt_shared_secret_correct_length() {
let keypair = ServerKeyPair::generate().expect("key generation");
let secret = [0x42u8; 16];
let encrypted = keypair
.public_key
.encrypt(&mut OsRng, Pkcs1v15Encrypt, &secret)
.expect("encryption");
let decrypted = keypair
.decrypt_shared_secret(&encrypted)
.expect("decryption");
assert_eq!(decrypted, secret);
}
#[test]
fn test_decrypt_shared_secret_wrong_length() {
let keypair = ServerKeyPair::generate().expect("key generation");
let wrong = [0x42u8; 8];
let encrypted = keypair
.public_key
.encrypt(&mut OsRng, Pkcs1v15Encrypt, &wrong)
.expect("encryption");
let err = keypair.decrypt_shared_secret(&encrypted).unwrap_err();
assert!(matches!(err, CryptoError::InvalidSecretLength(8)));
}
#[test]
fn test_minecraft_digest_notch() {
let result = minecraft_digest("Notch", &[], &[]);
assert_eq!(result, "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48");
}
#[test]
fn test_minecraft_digest_jeb() {
let result = minecraft_digest("jeb_", &[], &[]);
assert_eq!(result, "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1");
}
#[test]
fn test_minecraft_digest_simon() {
let result = minecraft_digest("simon", &[], &[]);
assert_eq!(result, "88e16a1019277b15d58faf0541e11910eb756f6");
}
#[test]
fn test_offline_uuid_deterministic() {
let uuid1 = offline_uuid("TestPlayer");
let uuid2 = offline_uuid("TestPlayer");
assert_eq!(uuid1, uuid2, "same name should produce same UUID");
}
#[test]
fn test_offline_uuid_different_names() {
let uuid1 = offline_uuid("Alice");
let uuid2 = offline_uuid("Bob");
assert_ne!(
uuid1, uuid2,
"different names should produce different UUIDs"
);
}
#[test]
fn test_offline_uuid_is_v3() {
let uuid = offline_uuid("Steve");
assert_eq!(
uuid.get_version(),
Some(uuid::Version::Md5),
"offline UUID should be version 3 (MD5)"
);
}
#[test]
fn test_offline_uuid_matches_java() {
let uuid = offline_uuid("Notch");
assert_eq!(
uuid.to_string(),
"b50ad385-829d-3141-a216-7e7d7539ba7f",
"offline UUID should match Java's UUID.nameUUIDFromBytes"
);
}
#[test]
fn test_generate_challenge_length() {
let challenge = generate_challenge();
assert_eq!(challenge.len(), 4);
}
#[test]
fn test_generate_challenge_random() {
let c1 = generate_challenge();
let c2 = generate_challenge();
assert_ne!(c1, c2, "two challenges should almost certainly differ");
}
#[test]
fn test_cipher_split_roundtrip() {
let secret = [0x42u8; 16];
let cipher = CipherState::new(&secret);
let (mut decrypt, mut encrypt) = cipher.split();
let original = b"Hello, split cipher!".to_vec();
let mut data = original.clone();
encrypt.encrypt(&mut data);
assert_ne!(data, original);
decrypt.decrypt(&mut data);
assert_eq!(data, original);
}
#[test]
fn test_cipher_split_matches_unsplit() {
let secret = [0x13u8; 16];
let mut unsplit = CipherState::new(&secret);
let original = b"consistency check".to_vec();
let mut data_unsplit = original.clone();
unsplit.encrypt(&mut data_unsplit);
let cipher = CipherState::new(&secret);
let (_dec, mut enc) = cipher.split();
let mut data_split = original;
enc.encrypt(&mut data_split);
assert_eq!(
data_unsplit, data_split,
"split cipher should produce identical ciphertext"
);
}
#[test]
fn test_cipher_split_multi_chunk() {
let secret = [0xAB; 16];
let cipher = CipherState::new(&secret);
let (mut decrypt, mut encrypt) = cipher.split();
let chunk1 = b"first chunk".to_vec();
let chunk2 = b"second chunk".to_vec();
let chunk3 = b"third chunk".to_vec();
let mut enc1 = chunk1.clone();
let mut enc2 = chunk2.clone();
let mut enc3 = chunk3.clone();
encrypt.encrypt(&mut enc1);
encrypt.encrypt(&mut enc2);
encrypt.encrypt(&mut enc3);
decrypt.decrypt(&mut enc1);
decrypt.decrypt(&mut enc2);
decrypt.decrypt(&mut enc3);
assert_eq!(enc1, chunk1);
assert_eq!(enc2, chunk2);
assert_eq!(enc3, chunk3);
}
}