fix: Fixes encryption support for pre-6.0.5 clients (#2365)

### Summary

Fixes encryption detection for clients pre-6.0.5.0. To limit the amount of brute-force key checking we are only checking 4.0.11 to 6.0.4.
This commit is contained in:
Kamron Batman 2026-03-12 21:59:01 -07:00 committed by GitHub
parent 76395b77ec
commit f150458578
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 23 deletions

View file

@ -23,8 +23,6 @@ namespace Server;
public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion>, IEquatable<ClientVersion> public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion>, IEquatable<ClientVersion>
{ {
public static readonly ClientVersion Version400a = new("4.0.0a");
public static readonly ClientVersion Version407a = new("4.0.7a");
public static readonly ClientVersion Version500a = new("5.0.0a"); public static readonly ClientVersion Version500a = new("5.0.0a");
public static readonly ClientVersion Version502b = new("5.0.2b"); public static readonly ClientVersion Version502b = new("5.0.2b");
public static readonly ClientVersion Version6000 = new("6.0.0.0"); public static readonly ClientVersion Version6000 = new("6.0.0.0");

View file

@ -41,6 +41,7 @@ public sealed class LoginEncryption : IClientEncryption
/// <summary> /// <summary>
/// Attempts to initialize login encryption and validate the packet. /// Attempts to initialize login encryption and validate the packet.
/// Returns true if the packet appears to be validly encrypted with this scheme. /// Returns true if the packet appears to be validly encrypted with this scheme.
/// When version is null (pre-6.0.5 clients that don't send 0xEF), tries all known legacy keys.
/// </summary> /// </summary>
public static bool TryDecrypt( public static bool TryDecrypt(
ClientVersion version, ClientVersion version,
@ -52,17 +53,41 @@ public sealed class LoginEncryption : IClientEncryption
encryption = null; encryption = null;
var keys = LoginKeys.GetKeys(version);
if (keys is { Key1: 0, Key2: 0 })
{
return false;
}
if (encryptedPacket.Length < LoginPacketSize) if (encryptedPacket.Length < LoginPacketSize)
{ {
return false; return false;
} }
if (version != null)
{
var keys = LoginKeys.GetKeys(version);
return keys is not { Key1: 0, Key2: 0 } && TryDecryptWithKeys(keys, seed, encryptedPacket, out encryption);
}
// No version available (pre-6.0.5 client) - try all known legacy keys
for (var i = 0; i < LoginKeys.LegacyKeys.Length; i++)
{
var legacyKeys = LoginKeys.LegacyKeys[i];
if (TryDecryptWithKeys(legacyKeys, seed, encryptedPacket, out encryption))
{
return true;
}
}
return false;
}
/// <summary>
/// Attempts decryption with a specific key pair and validates the result.
/// </summary>
private static bool TryDecryptWithKeys(
LoginKeys keys,
uint seed,
ReadOnlySpan<byte> encryptedPacket,
out LoginEncryption encryption)
{
const int LoginPacketSize = 62;
// Copy and decrypt // Copy and decrypt
Span<byte> decrypted = stackalloc byte[LoginPacketSize]; Span<byte> decrypted = stackalloc byte[LoginPacketSize];
encryptedPacket[..LoginPacketSize].CopyTo(decrypted); encryptedPacket[..LoginPacketSize].CopyTo(decrypted);
@ -75,6 +100,7 @@ public sealed class LoginEncryption : IClientEncryption
// - Byte 60 must be 0x00 (null terminator for password) // - Byte 60 must be 0x00 (null terminator for password)
if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00) if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00)
{ {
encryption = null;
return false; return false;
} }

View file

@ -13,6 +13,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * * along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/ *************************************************************************/
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -28,6 +29,8 @@ public readonly struct LoginKeys
private static readonly Dictionary<ClientVersion, LoginKeys> _cache = []; private static readonly Dictionary<ClientVersion, LoginKeys> _cache = [];
private static LoginKeys[] _legacyKeys;
public uint Key1 { get; } public uint Key1 { get; }
public uint Key2 { get; } public uint Key2 { get; }
@ -37,6 +40,12 @@ public readonly struct LoginKeys
Key2 = key2; Key2 = key2;
} }
/// <summary>
/// Gets pre-computed login keys for all known client versions before 6.0.5
/// that do not send the 0xEF packet (and thus have no version available at login time).
/// </summary>
public static ReadOnlySpan<LoginKeys> LegacyKeys => _legacyKeys ??= BuildLegacyKeys();
/// <summary> /// <summary>
/// Gets or computes encryption keys for the specified client version. /// Gets or computes encryption keys for the specified client version.
/// Results are cached for performance. /// Results are cached for performance.
@ -54,20 +63,16 @@ public readonly struct LoginKeys
return keys; return keys;
} }
keys = ComputeKeys(version); keys = ComputeKeys((uint)version.Major, (uint)version.Minor, (uint)version.Revision);
_cache[version] = keys; _cache[version] = keys;
return keys; return keys;
} }
/// <summary> /// <summary>
/// Computes encryption keys from client version using the UO key derivation algorithm. /// Computes encryption keys from version components using the UO key derivation algorithm.
/// </summary> /// </summary>
private static LoginKeys ComputeKeys(ClientVersion version) private static LoginKeys ComputeKeys(uint major, uint minor, uint revision)
{ {
uint major = (uint)version.Major;
uint minor = (uint)version.Minor;
uint revision = (uint)version.Revision;
// Key1 derivation // Key1 derivation
uint key1 = (major << 23) | (minor << 14) | (revision << 4); uint key1 = (major << 23) | (minor << 14) | (revision << 4);
key1 ^= (revision * revision) << 9; key1 ^= (revision * revision) << 9;
@ -86,4 +91,28 @@ public readonly struct LoginKeys
return new LoginKeys(key1, key2); return new LoginKeys(key1, key2);
} }
/// <summary>
/// Builds the static array of pre-computed keys for pre-6.0.5 client versions.
/// These versions don't send 0xEF, so login encryption must be detected by trying known keys.
/// </summary>
private static LoginKeys[] BuildLegacyKeys()
{
// All unique (Major, Minor, Revision) tuples from 4.0.11 through 6.0.4
ReadOnlySpan<(uint Major, uint Minor, uint Revision)> versions =
[
(4, 0, 11),
(5, 0, 0), (5, 0, 1), (5, 0, 2), (5, 0, 3), (5, 0, 4),
(5, 0, 5), (5, 0, 6), (5, 0, 7), (5, 0, 8), (5, 0, 9),
(6, 0, 0), (6, 0, 1), (6, 0, 2), (6, 0, 3), (6, 0, 4),
];
var keys = new LoginKeys[versions.Length];
for (var i = 0; i < versions.Length; i++)
{
keys[i] = ComputeKeys(versions[i].Major, versions[i].Minor, versions[i].Revision);
}
return keys;
}
} }

View file

@ -63,9 +63,6 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
// Managed socket with buffers (handles lifecycle automatically) // Managed socket with buffers (handles lifecycle automatically)
internal RingSocket _socket; internal RingSocket _socket;
// General packet throttle state (used for other throttled packets)
internal bool _isThrottled;
private IAccount _account; private IAccount _account;
internal enum ParserState internal enum ParserState
@ -664,8 +661,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
_encryption = loginEncryption; _encryption = loginEncryption;
// Decrypt the buffer in place for processing // Decrypt the buffer in place for processing
var mutableBuffer = _socket.RecvBuffer.GetReadSpan(); loginEncryption.ClientDecrypt(buffer[..62]);
loginEncryption.ClientDecrypt(mutableBuffer[..62]);
} }
// Now process as normal (first byte should now be 0x80) // Now process as normal (first byte should now be 0x80)
@ -714,7 +710,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
case ProtocolState.GameServer_AwaitingGameServerLogin: case ProtocolState.GameServer_AwaitingGameServerLogin:
{ {
// Some clients send 0x80 on game server connection // Some clients send 0x80 on game server connection
if (packetId == 0x80) if (packetId == 0x80 || length == 62)
{ {
goto case ProtocolState.LoginServer_AwaitingLogin; goto case ProtocolState.LoginServer_AwaitingLogin;
} }
@ -767,8 +763,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
_encryption = gameEncryption; _encryption = gameEncryption;
// Decrypt the buffer in place for processing // Decrypt the buffer in place for processing
var mutableBuffer = _socket.RecvBuffer.GetReadSpan(); gameEncryption.ClientDecrypt(buffer[..65]);
gameEncryption.ClientDecrypt(mutableBuffer[..65]);
} }
// Now process as normal (first byte should now be 0x91) // Now process as normal (first byte should now be 0x91)