diff --git a/protocol.md b/protocol.md index 7d36327..5892067 100644 --- a/protocol.md +++ b/protocol.md @@ -322,13 +322,32 @@ Communication between the master server and clients will be done over a WebSocke 1 - Succeeded - Boolean + Server Count (n) + Packed Unsigned Short - 2 - Message - String + r > 1 + Iterated over n (0 ≤ in - 1) + + + 2 + 4i + Server Id + Packed Unsigned Short + + + 3 + 4i + User Count + Packed Unsigned Short + + + 4 + 4i + IP Address + IPv4 String + + + 5 + 4i + Port + Packed Unsigned Short diff --git a/server/Configuration.cs b/server/Configuration.cs index b6ee8b2..d77831e 100644 --- a/server/Configuration.cs +++ b/server/Configuration.cs @@ -21,6 +21,7 @@ namespace SockScape { "Master Port", "Master Addr", "Master Secret", + "Master IV", "Max Users" } }, diff --git a/server/Encryption/BlockCipher.cs b/server/Encryption/BlockCipher.cs new file mode 100644 index 0000000..1efe02a --- /dev/null +++ b/server/Encryption/BlockCipher.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Glove; + +namespace SockScape.Encryption { + class BlockCipher { + public const int KeySize = 192; + public const int KeySizeBytes = KeySize / 8; + + private readonly byte[] Key = new byte[KeySizeBytes]; + private readonly byte[] IV; + + public BlockCipher(BigInteger key) { + Key = Key.Select(x => (byte)0).ToArray(); + Array.Copy(key.ToByteArray(), Key, Key.Length); + + IV = Configuration.General["Master IV"].Str.HexStringToBytes() + ?? new byte[] { 0x0b, 0xfc, 0xd7, 0x2d, 0x23, 0xb7, 0x83, 0xb2 }; + } + + public byte[] Encrypt(byte[] data) { + try { + var ms = new MemoryStream(); + var cs = new CryptoStream(ms, + new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), + CryptoStreamMode.Write); + + cs.Write(data, 0, data.Length); + cs.FlushFinalBlock(); + + byte[] ret = ms.ToArray(); + + cs.Close(); + ms.Close(); + + return ret; + } catch(Exception e) { + Console.WriteLine($"TDES ENCRYPT ERROR: {e.Message}"); + return null; + } + } + + public byte[] Decrypt(byte[] data) { + try { + var ms = new MemoryStream(data); + var cs = new CryptoStream(ms, + new TripleDESCryptoServiceProvider().CreateEncryptor(Key, IV), + CryptoStreamMode.Read); + + byte[] ret = new byte[data.Length]; + cs.Read(ret, 0, data.Length); + + cs.Close(); + ms.Close(); + + return ret; + } catch(Exception e) { + Console.WriteLine($"TDES DECRYPT ERROR: {e.Message}"); + return null; + } + } + } +} diff --git a/server/Encryption/KeyExchange.cs b/server/Encryption/KeyExchange.cs index 268da0c..5d02413 100644 --- a/server/Encryption/KeyExchange.cs +++ b/server/Encryption/KeyExchange.cs @@ -6,10 +6,14 @@ using System.Threading.Tasks; using System.Numerics; using Glove; using System.Globalization; +using System.Security.Cryptography; namespace SockScape.Encryption { class Key { - private static readonly BigInteger Secret = RNG.NextPrime(512 / 8); + public const int KeySize = 512; + public const int KeySizeBytes = KeySize / 8; + + private static readonly BigInteger Secret = RNG.NextPrime(KeySizeBytes); public BigInteger Generator { get; } = 2; public BigInteger Modulus { get; } public BigInteger PrivateKey { get; private set; } = BigInteger.Zero; @@ -17,7 +21,7 @@ namespace SockScape.Encryption { => !PrivateKey.IsZero; public Key() { - Modulus = RNG.NextPrime(512 / 8); + Modulus = RNG.NextPrime(KeySizeBytes); } public Packet GenerateRequestPacket() { diff --git a/server/Encryption/Cipher.cs b/server/Encryption/StreamCipher.cs similarity index 90% rename from server/Encryption/Cipher.cs rename to server/Encryption/StreamCipher.cs index d94d199..8c6a0c2 100644 --- a/server/Encryption/Cipher.cs +++ b/server/Encryption/StreamCipher.cs @@ -7,11 +7,11 @@ using System.Numerics; using Glove; namespace SockScape.Encryption { - class Cipher { - private readonly byte[] Key = new byte[512 / 8]; + class StreamCipher { + private readonly byte[] Key = new byte[Encryption.Key.KeySizeBytes]; private readonly byte[] State = new byte[256]; - public Cipher(BigInteger key) { + public StreamCipher(BigInteger key) { int i = 0, j = 0; State = State.Select(x => (byte)i++).ToArray(); Key = Key.Select(x => (byte)0).ToArray(); diff --git a/server/Entrypoint.cs b/server/Entrypoint.cs index 559861b..3ac68a4 100644 --- a/server/Entrypoint.cs +++ b/server/Entrypoint.cs @@ -14,10 +14,20 @@ using MySql.Data.Entity; namespace SockScape { static class ServerContext { - public static Dictionary> Servers { get; } - = new Dictionary>(); - public static Dictionary> Pools { get; } - = new Dictionary>(); + public static Dictionary> Servers { get; } + = new Dictionary>(); + public static Dictionary> Pools { get; } + = new Dictionary>(); + + public static Packet StatusUpdatePacket { + get { + var packet = new Packet(kIntraSlaveId.StatusUpdate, (byte)Servers.Count); + foreach(var pool in Pools) + packet.AddRegions(pool.Key.Pack(), ((ushort)pool.Value.ConnectionCount).Pack(), Servers[pool.Key].Port.Pack()); + + return packet; + } + } } class Entrypoint { @@ -33,8 +43,8 @@ namespace SockScape { var serverHandle = new Server((ushort)server["Port"], pool, server); - ServerContext.Pools.Add(server["Id"], pool); - ServerContext.Servers.Add(server["Id"], serverHandle); + ServerContext.Pools.Add((ushort)server["Id"], pool); + ServerContext.Servers.Add((ushort)server["Id"], serverHandle); serverHandle.Start(); } diff --git a/server/Libraries/Glove/INI/Value.cs b/server/Libraries/Glove/INI/Value.cs index d820785..5aaf319 100644 --- a/server/Libraries/Glove/INI/Value.cs +++ b/server/Libraries/Glove/INI/Value.cs @@ -12,24 +12,29 @@ namespace Glove.INI { Raw = raw; } - public static implicit operator string(Value value) { - return value.Raw; - } + public string Str + => this; - public static implicit operator bool(Value value) { - return Boolean.TryParse(value.Raw, out bool retval) && retval; - } + public Int32 Int + => this; - public static implicit operator Int32(Value value) { - return Int32.TryParse(value.Raw, out Int32 retval) + public double Dbl + => this; + + public static implicit operator string(Value value) + => value.Raw; + + public static implicit operator bool(Value value) + => Boolean.TryParse(value.Raw, out bool retval) && retval; + + public static implicit operator Int32(Value value) + => Int32.TryParse(value.Raw, out Int32 retval) ? retval : 0; - } - public static implicit operator double(Value value) { - return Double.TryParse(value.Raw, out double retval) + public static implicit operator double(Value value) + => Double.TryParse(value.Raw, out double retval) ? retval : 0; - } } } diff --git a/server/Libraries/Glove/NumericExtensions.cs b/server/Libraries/Glove/NumericExtensions.cs index 177edd9..d6a6845 100644 --- a/server/Libraries/Glove/NumericExtensions.cs +++ b/server/Libraries/Glove/NumericExtensions.cs @@ -60,5 +60,23 @@ namespace Glove { public static BigInteger HexStringToBigInt(this string value) => BigInteger.Parse(value, NumberStyles.HexNumber); + + public static byte[] HexStringToBytes(this string value) { + if(value.Length % 2 == 1) + return null; + + var bytes = new byte[value.Length / 2]; + for(var i = 0; i < bytes.Length; ++i) { + char upperNibble = value[2 * i], + lowerNibble = value[2 * i + 1]; + + if(!upperNibble.IsHex() || !lowerNibble.IsHex()) + return null; + + bytes[i] = (byte)((upperNibble.HexValue() << 4) | lowerNibble.HexValue()); + } + + return bytes; + } } } diff --git a/server/Libraries/Glove/StringExtensions.cs b/server/Libraries/Glove/StringExtensions.cs index 29bb093..228d4de 100644 --- a/server/Libraries/Glove/StringExtensions.cs +++ b/server/Libraries/Glove/StringExtensions.cs @@ -5,6 +5,18 @@ using System.Text; using System.Threading.Tasks; namespace Glove { + public static class CharExtensions { + public static bool IsHex(this char c) { + return (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'F') + || (c >= 'a' && c <= 'f'); + } + + public static byte HexValue(this char c) { + return (byte)(!c.IsHex() ? 0 : c - (c < 0x3A ? 0x30 : (c < 0x61 ? 0x37 : 0x57))); + } + } + public static class StringExtensions { public static byte[] GetBytes(this string str, bool isUtf8 = true) => isUtf8 ? Encoding.UTF8.GetBytes(str) diff --git a/server/Libraries/Kneesocks/Handshake.cs b/server/Libraries/Kneesocks/Handshake.cs index 8e634a2..4e5d343 100644 --- a/server/Libraries/Kneesocks/Handshake.cs +++ b/server/Libraries/Kneesocks/Handshake.cs @@ -29,7 +29,7 @@ namespace Kneesocks { Service_Unavailable = 503, Gateway_Timeout = 504 } - public kStatusCode? StatusCode { get; private set; } = null; + public kStatusCode? StatusCode { get; } = null; protected string StatusCodeText => Enum.GetName(typeof(kStatusCode), StatusCode).Replace('_', ' '); diff --git a/server/MasterServerList.cs b/server/MasterServerList.cs new file mode 100644 index 0000000..cd3980e --- /dev/null +++ b/server/MasterServerList.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Glove; + +namespace SockScape { + static class MasterServerList { + public static Dictionary Servers { get; } + = new Dictionary(); + + public static void Write(Server server) { + lock(Servers) { + if(HasId(server.Id) && !Servers[server.Id].Address.Equals(server.Address)) + Console.WriteLine($"{DateTime.Now.ToShortTimeString()} - Server {server.Id} has changed IP addresses."); + + Servers[server.Id] = server; + } + } + + public static Packet ReportPacket { + get { + lock(Servers) { + var packet = new Packet(kInterMasterId.ServerListing, ((ushort)Servers.Count).Pack()); + foreach(var server in Servers) + // TODO change this to support IPv6 + packet.AddRegions(server.Key.Pack(), server.Value.UserCount.Pack(), + server.Value.Address.MapToIPv4().ToString(), server.Value.Port.Pack()); + + return packet; + } + } + } + + public static bool HasId(UInt16 id) + => Servers.ContainsKey(id); + + public static void Clear() + => Servers.Clear(); + } + + class Server { + public ushort Id { get; set; } + public ushort UserCount { get; set; } + public IPAddress Address { get; set; } + public ushort Port { get; set; } + } +} diff --git a/server/ServerList.cs b/server/ServerList.cs deleted file mode 100644 index 0665bb1..0000000 --- a/server/ServerList.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace SockScape { - static class ServerList { - private static ServerDictionary Servers { get; } - = new ServerDictionary(); - - private static Dictionary List - => Servers.List; - - public static void Write(Server server) { - lock(Servers) { - if(HasId(server.Id) && !List[server.Id].Address.Equals(server.Address)) - Console.WriteLine($"{DateTime.Now.ToShortTimeString()} - Server {server.Id} has changed IP addresses."); - - List[server.Id] = server; - } - } - - public static Packet ReportPacket { - get { - var packet = new Packet(kInterMasterId.); - - return packet; - } - } - - public static bool HasId(UInt16 id) - => List.ContainsKey(id); - - public static void Clear() - => List.Clear(); - - public class ServerDictionary { - public Dictionary List { get; } - = new Dictionary(); - - public Server this[UInt16 i] { - get => List.ContainsKey(i) ? List[i] : null; - set => List[i] = value; - } - } - } - - class Server { - public ushort Id { get; set; } - public ushort UserCount { get; set; } - public IPAddress Address { get; set; } - public ushort Port { get; set; } - } -} diff --git a/server/SockScape.csproj b/server/SockScape.csproj index ab9fea1..a6bbe1a 100644 --- a/server/SockScape.csproj +++ b/server/SockScape.csproj @@ -94,17 +94,18 @@ + - + 201708251600325_Initial.cs - + - - + + diff --git a/server/Socks/MasterConnection.cs b/server/Socks/MasterConnection.cs index fb1d9bc..3453e80 100644 --- a/server/Socks/MasterConnection.cs +++ b/server/Socks/MasterConnection.cs @@ -10,7 +10,7 @@ using SockScape.Encryption; namespace SockScape { class MasterConnection : Connection { private Key Key; - public Cipher Encryptor { get; private set; } + public StreamCipher Encryptor { get; private set; } protected override void OnOpen() { Key = new Key(); @@ -39,7 +39,7 @@ namespace SockScape { return; } - Encryptor = new Cipher(Key.PrivateKey); + Encryptor = new StreamCipher(Key.PrivateKey); break; case kInterMasterId.LoginAttempt: diff --git a/server/Socks/MasterUdpClient.cs b/server/Socks/MasterIntraClient.cs similarity index 85% rename from server/Socks/MasterUdpClient.cs rename to server/Socks/MasterIntraClient.cs index 3e60a89..1e97c62 100644 --- a/server/Socks/MasterUdpClient.cs +++ b/server/Socks/MasterIntraClient.cs @@ -10,9 +10,8 @@ using Glove; using SockScape.Encryption; namespace SockScape { - static class MasterUdpClient { + static class MasterIntraClient { private static Key Key; - private static Cipher Encryptor; private static UdpClient Sock; private static Thread ListeningThread; @@ -55,10 +54,9 @@ namespace SockScape { switch((kIntraMasterId)packet.Id) { case kIntraMasterId.KeyExchange: var responsePacket = Key.ParseRequestPacket(packet); - Encryptor = new Cipher(Key.PrivateKey); + Encryptor = new StreamCipher(Key.PrivateKey); if(responsePacket != null) Send(responsePacket); - else LastMessageIn = new DateTime(0); break; @@ -72,6 +70,8 @@ namespace SockScape { break; case kIntraMasterId.EncryptionError: + NextSendId = NextRecvId = 0; + Buffer.Clear(); Key = new Key(); Encryptor = null; LastMessageIn = new DateTime(0); @@ -79,9 +79,9 @@ namespace SockScape { } } - if (LastMessageIn.Ticks != 0) { + if(LastMessageIn.Ticks != 0) { if(DeltaLastOut.TotalSeconds > 2) - Send(new Packet()); + Send(Encryptor.Parse(ServerContext.StatusUpdatePacket.GetBytes())); } else if(DeltaLastOut.TotalSeconds > 10) Send(new Packet(kIntraSlaveId.InitiationAttempt, Configuration.General["Master Secret"])); @@ -91,9 +91,14 @@ namespace SockScape { } public static void Send(Packet packet) { - var message = packet.GetBytes(); - Sock.Send(message, message.Length); + Send(packet.GetBytes()); + } + + public static void Send(byte[] bytes) { + Sock.Send(bytes, bytes.Length); LastMessageOut = DateTime.Now; + Buffer.Add(NextSendId, bytes); + ++NextSendId; } public static void Close() { diff --git a/server/Socks/MasterUdpServer.cs b/server/Socks/MasterIntraServer.cs similarity index 79% rename from server/Socks/MasterUdpServer.cs rename to server/Socks/MasterIntraServer.cs index 73fb09c..ee780f1 100644 --- a/server/Socks/MasterUdpServer.cs +++ b/server/Socks/MasterIntraServer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,7 +24,7 @@ namespace SockScape { if(IsOpen || ListeningThread != null) return; - ServerList.Clear(); + MasterServerList.Clear(); Prospects = new Dictionary(); Clients = new Dictionary(); @@ -49,8 +50,11 @@ namespace SockScape { encryptor == null ? Packet.FromBytes(data) : Packet.FromBytes(encryptor.Parse(data)); - if(packet == null) + if(packet == null) { + if(encryptor != null) + EncryptionError(endPoint); continue; + } Clients[client].LastReceive = DateTime.Now; switch((kIntraSlaveId)packet.Id) { @@ -76,7 +80,8 @@ namespace SockScape { var privateKey = Prospects[client].Key.ParseResponsePacket(packet); if(privateKey != -1) { - Prospects[client].Encryptor = new Cipher(privateKey); + Prospects[client].LastReceive = DateTime.Now; + Prospects[client].Encryptor = new StreamCipher(privateKey); Clients[client] = Prospects[client]; Prospects.Remove(client); } else @@ -88,13 +93,13 @@ namespace SockScape { break; if(packet.CheckRegions(0, 1)) { - NegativeAck(endPoint, kIntraSlaveId.StatusUpdate, "Server count is malformed."); + NegativeAck(endPoint, encryptor, kIntraSlaveId.StatusUpdate, "Server count is malformed."); break; } byte serverCount = packet[0].Raw[0]; if(packet.RegionCount != 1 + 3 * serverCount) { - NegativeAck(endPoint, kIntraSlaveId.StatusUpdate, "Region count does not match server count"); + NegativeAck(endPoint, encryptor, kIntraSlaveId.StatusUpdate, "Region count does not match server count"); break; } @@ -102,7 +107,7 @@ namespace SockScape { if(!packet.CheckRegions(2 + 3 * i, 2, 2, 2)) continue; - ServerList.Write(new Server { + MasterServerList.Write(new Server { Id = packet[2 + 3 * i].Raw.UnpackUInt16(), UserCount = packet[3 + 3 * i].Raw.UnpackUInt16(), Address = endPoint.Address, @@ -110,7 +115,7 @@ namespace SockScape { }); } - PositiveAck(endPoint, kIntraSlaveId.StatusUpdate); + PositiveAck(endPoint, encryptor, kIntraSlaveId.StatusUpdate); break; } } @@ -122,8 +127,11 @@ namespace SockScape { } private static void Send(Packet packet, IPEndPoint client) { - var message = packet.GetBytes(); - Sock.Send(message, message.Length, client); + Send(packet.GetBytes(), client); + } + + private static void Send(byte[] bytes, IPEndPoint client) { + Sock.Send(bytes, bytes.Length, client); } public static void Close() { @@ -141,12 +149,12 @@ namespace SockScape { return Clients.ContainsKey(client); } - private static void PositiveAck(IPEndPoint endPoint, kIntraSlaveId id) { - Send(new Packet(kIntraMasterId.PositiveAck, id), endPoint); + private static void PositiveAck(IPEndPoint endPoint, StreamCipher cipher, kIntraSlaveId id) { + Send(cipher.Parse(new Packet(kIntraMasterId.PositiveAck, id).GetBytes()), endPoint); } - private static void NegativeAck(IPEndPoint endPoint, kIntraSlaveId id, string message = "An error occurred while parsing a packet.") { - Send(new Packet(kIntraMasterId.NegativeAck, id, message), endPoint); + private static void NegativeAck(IPEndPoint endPoint, StreamCipher cipher, kIntraSlaveId id, string message = "An error occurred while parsing a packet.") { + Send(cipher.Parse(new Packet(kIntraMasterId.NegativeAck, id, message).GetBytes()), endPoint); } private static void EncryptionError(IPEndPoint endPoint, string message = "A general encryption error has occurred. Renegotiation required.") { @@ -157,7 +165,7 @@ namespace SockScape { public IPEndPoint Address { get; set; } public DateTime LastReceive { get; set; } public TimeSpan ReceiveDelta => DateTime.Now - LastReceive; - public Cipher Encryptor { get; set; } + public StreamCipher Encryptor { get; set; } public Key Key { get; set; } } } diff --git a/server/Socks/Protocols/Packet.cs b/server/Socks/Protocols/Packet.cs index a820d44..296cd2a 100644 --- a/server/Socks/Protocols/Packet.cs +++ b/server/Socks/Protocols/Packet.cs @@ -92,6 +92,13 @@ namespace SockScape { return this; } + public Packet AddRegions(params object[] regions) { + foreach(var region in regions) + AddRegion(region); + + return this; + } + public bool CheckRegions(int startIndex, params int[] lengths) { if(startIndex + lengths.Length > RegionCount) return false; @@ -150,4 +157,32 @@ namespace SockScape { => this; } } + + public class PacketBuffer { + private short MaxSize; + private Dictionary Buffer + = new Dictionary(); + + public PacketBuffer(short maxSize = 10) { + MaxSize = maxSize; + } + + public void Add(uint id, byte[] packet) { + Buffer[id] = packet; + + if(Buffer.Count > 10) + Buffer = + Buffer.Where(x => x.Key >= id - 10) + .ToDictionary(x => x.Key, x => x.Value); + } + + public byte[] this[uint i] + => Buffer[i]; + + public bool HasId(uint id) + => Buffer.ContainsKey(id); + + public void Clear() + => Buffer.Clear(); + } }