diff --git a/Projects/Server/Client/ClientVersion.cs b/Projects/Server/Client/ClientVersion.cs index cbe518ad2..798fd1353 100644 --- a/Projects/Server/Client/ClientVersion.cs +++ b/Projects/Server/Client/ClientVersion.cs @@ -23,8 +23,6 @@ namespace Server; public class ClientVersion : IComparable, IComparer, IEquatable { - 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 Version502b = new("5.0.2b"); public static readonly ClientVersion Version6000 = new("6.0.0.0"); diff --git a/Projects/Server/Network/Encryption/LoginEncryption.cs b/Projects/Server/Network/Encryption/LoginEncryption.cs index 7ae85d55b..e1310cfa5 100644 --- a/Projects/Server/Network/Encryption/LoginEncryption.cs +++ b/Projects/Server/Network/Encryption/LoginEncryption.cs @@ -41,6 +41,7 @@ public sealed class LoginEncryption : IClientEncryption /// /// Attempts to initialize login encryption and validate the packet. /// 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. /// public static bool TryDecrypt( ClientVersion version, @@ -52,17 +53,41 @@ public sealed class LoginEncryption : IClientEncryption encryption = null; - var keys = LoginKeys.GetKeys(version); - if (keys is { Key1: 0, Key2: 0 }) - { - return false; - } - if (encryptedPacket.Length < LoginPacketSize) { 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; + } + + /// + /// Attempts decryption with a specific key pair and validates the result. + /// + private static bool TryDecryptWithKeys( + LoginKeys keys, + uint seed, + ReadOnlySpan encryptedPacket, + out LoginEncryption encryption) + { + const int LoginPacketSize = 62; + // Copy and decrypt Span decrypted = stackalloc byte[LoginPacketSize]; encryptedPacket[..LoginPacketSize].CopyTo(decrypted); @@ -75,6 +100,7 @@ public sealed class LoginEncryption : IClientEncryption // - Byte 60 must be 0x00 (null terminator for password) if (decrypted[0] != 0x80 || decrypted[30] != 0x00 || decrypted[60] != 0x00) { + encryption = null; return false; } diff --git a/Projects/Server/Network/Encryption/LoginKeys.cs b/Projects/Server/Network/Encryption/LoginKeys.cs index 6acad3c2f..6b04a5bb7 100644 --- a/Projects/Server/Network/Encryption/LoginKeys.cs +++ b/Projects/Server/Network/Encryption/LoginKeys.cs @@ -13,6 +13,7 @@ * along with this program. If not, see . * *************************************************************************/ +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -28,6 +29,8 @@ public readonly struct LoginKeys private static readonly Dictionary _cache = []; + private static LoginKeys[] _legacyKeys; + public uint Key1 { get; } public uint Key2 { get; } @@ -37,6 +40,12 @@ public readonly struct LoginKeys Key2 = key2; } + /// + /// 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). + /// + public static ReadOnlySpan LegacyKeys => _legacyKeys ??= BuildLegacyKeys(); + /// /// Gets or computes encryption keys for the specified client version. /// Results are cached for performance. @@ -54,20 +63,16 @@ public readonly struct LoginKeys return keys; } - keys = ComputeKeys(version); + keys = ComputeKeys((uint)version.Major, (uint)version.Minor, (uint)version.Revision); _cache[version] = keys; return keys; } /// - /// Computes encryption keys from client version using the UO key derivation algorithm. + /// Computes encryption keys from version components using the UO key derivation algorithm. /// - 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 uint key1 = (major << 23) | (minor << 14) | (revision << 4); key1 ^= (revision * revision) << 9; @@ -86,4 +91,28 @@ public readonly struct LoginKeys return new LoginKeys(key1, key2); } + + /// + /// 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. + /// + 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; + } } diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index c57be287c..b64e45e18 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -63,9 +63,6 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode