/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: GameEncryption.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Runtime.Intrinsics; using System.Security.Cryptography; using Server.Logging; namespace Server.Network; /// /// Implements game packet encryption/decryption using Twofish + MD5. /// Used for all game packets after login (0x91 and onwards). /// public sealed class GameEncryption : IClientEncryption { private static readonly ILogger logger = LogFactory.GetLogger(typeof(GameEncryption)); private const int CipherTableSize = 256; private const int BlockSize = 16; // Static identity table [0..255] for vectorized CopyTo initialization private static readonly byte[] IdentityTable = CreateIdentityTable(); private static byte[] CreateIdentityTable() { var table = new byte[CipherTableSize]; for (var i = 0; i < CipherTableSize; i++) { table[i] = (byte)i; } return table; } private readonly TwofishEngine _twofish; private readonly byte[] _cipherTable; private readonly byte[] _xorKey; private ushort _recvPos; private byte _sendPos; public GameEncryption(uint seed) { // Create 16-byte key from seed (repeated 4 times) Span key = stackalloc byte[16]; key[0] = key[4] = key[8] = key[12] = (byte)((seed >> 24) & 0xFF); key[1] = key[5] = key[9] = key[13] = (byte)((seed >> 16) & 0xFF); key[2] = key[6] = key[10] = key[14] = (byte)((seed >> 8) & 0xFF); key[3] = key[7] = key[11] = key[15] = (byte)(seed & 0xFF); _twofish = new TwofishEngine(key); // Initialize cipher table with identity [0..255] using vectorized copy _cipherTable = GC.AllocateUninitializedArray(CipherTableSize); IdentityTable.CopyTo(_cipherTable, 0); // Encrypt cipher table with Twofish RefreshCipherTable(); // Compute MD5 hash of cipher table for server->client XOR key _xorKey = MD5.HashData(_cipherTable); } /// /// Refreshes the cipher table by encrypting it with Twofish. /// Called every 256 bytes of received data. /// private void RefreshCipherTable() { // Encrypt cipher table in 16-byte blocks for (var i = 0; i < CipherTableSize; i += BlockSize) { _twofish.EncryptBlock(_cipherTable.AsSpan(i, BlockSize)); } _recvPos = 0; } /// /// Decrypts incoming data from the client (in-place). /// XORs with cipher table, refreshing every 256 bytes. /// public void ClientDecrypt(Span buffer) { for (var i = 0; i < buffer.Length; i++) { if (_recvPos >= CipherTableSize) { RefreshCipherTable(); } buffer[i] ^= _cipherTable[_recvPos++]; } } /// /// Encrypts outgoing data to the client (in-place). /// XORs with MD5 hash of cipher table (16-byte rotating key). /// Uses SIMD optimization for larger buffers. /// public void ServerEncrypt(Span buffer) { var i = 0; // SIMD path: process 16 bytes at a time when aligned with key if (_sendPos == 0 && buffer.Length >= 16 && Vector128.IsHardwareAccelerated) { var keyVec = Vector128.Create(_xorKey); for (; i + 16 <= buffer.Length; i += 16) { var chunk = Vector128.LoadUnsafe(ref buffer[i]); var result = Vector128.Xor(chunk, keyVec); result.StoreUnsafe(ref buffer[i]); } } // Scalar path for remainder or when not aligned for (; i < buffer.Length; i++) { buffer[i] ^= _xorKey[_sendPos++]; _sendPos &= 0x0F; // Wrap at 16 } } /// /// Attempts to decrypt a game login packet and validate it. /// Returns true if the packet appears to be validly encrypted. /// public static bool TryDecrypt(uint seed, ReadOnlySpan encryptedPacket, out GameEncryption encryption) { const int GameLoginPacketSize = 65; encryption = null; if (encryptedPacket.Length < GameLoginPacketSize) { if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: Invalid buffer length {Length}", encryptedPacket.Length); } return false; } if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: Seed=0x{Seed:X8}", seed); logger.Debug("GameEncryption.TryDecrypt: Encrypted[0..16]: {Bytes}", Convert.ToHexString(encryptedPacket[..16])); } // Copy and decrypt Span decrypted = stackalloc byte[GameLoginPacketSize]; encryptedPacket[..GameLoginPacketSize].CopyTo(decrypted); var enc = new GameEncryption(seed); if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: CipherTable[0..16]: {Bytes}", Convert.ToHexString(enc._cipherTable.AsSpan(0, 16))); logger.Debug("GameEncryption.TryDecrypt: XorKey: {Bytes}", Convert.ToHexString(enc._xorKey)); } enc.ClientDecrypt(decrypted); if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: Decrypted[0..16]: {Bytes}", Convert.ToHexString(decrypted[..16])); logger.Debug("GameEncryption.TryDecrypt: First byte=0x{Byte:X2} (expected 0x91)", decrypted[0]); } // Validate: first byte must be 0x91 (game server login packet ID) if (decrypted[0] != 0x91) { if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: Validation FAILED - first byte is not 0x91"); } return false; } if (EncryptionManager.Debug) { logger.Debug("GameEncryption.TryDecrypt: Validation PASSED"); } // Re-create encryption for actual use encryption = new GameEncryption(seed); return true; } }