Security implementation

This commit is contained in:
Sardelka
2022-06-24 10:33:36 +08:00
parent 82e7cdd785
commit 82ab9237f5
16 changed files with 486 additions and 354 deletions

View File

@ -6,8 +6,10 @@ using Lidgren.Network;
using RageCoop.Core; using RageCoop.Core;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Security.Cryptography;
using GTA.Math; using GTA.Math;
using GTA.Native; using GTA.Native;
using System.Net;
namespace RageCoop.Client namespace RageCoop.Client
{ {
@ -16,9 +18,10 @@ namespace RageCoop.Client
public static NetClient Client; public static NetClient Client;
public static float Latency = 0; public static float Latency = 0;
public static bool ShowNetworkInfo = false; public static bool ShowNetworkInfo = false;
public static Security Security;
static Networking() static Networking()
{ {
Security=new Security(Main.Logger);
Task.Run(() => Task.Run(() =>
{ {
while (true) while (true)
@ -36,7 +39,7 @@ namespace RageCoop.Client
}); });
} }
public static void ToggleConnection(string address) public static void ToggleConnection(string address,string password=null)
{ {
if (IsOnServer) if (IsOnServer)
{ {
@ -44,6 +47,7 @@ namespace RageCoop.Client
} }
else else
{ {
password = password ?? Main.Settings.Password;
// 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP // 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP
NetPeerConfiguration config = new NetPeerConfiguration("623c92c287cc392406e7aaaac1c0f3b0") NetPeerConfiguration config = new NetPeerConfiguration("623c92c287cc392406e7aaaac1c0f3b0")
{ {
@ -51,6 +55,7 @@ namespace RageCoop.Client
}; };
config.EnableMessageType(NetIncomingMessageType.ConnectionLatencyUpdated); config.EnableMessageType(NetIncomingMessageType.ConnectionLatencyUpdated);
config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
Client = new NetClient(config); Client = new NetClient(config);
Client.Start(); Client.Start();
@ -70,17 +75,27 @@ namespace RageCoop.Client
} }
EntityPool.AddPlayer(); EntityPool.AddPlayer();
Task.Run(() =>
// Send HandshakePacket
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
new Packets.Handshake()
{ {
PedID = Main.LocalPlayerID, GetServerPublicKey(address);
Username = Main.Settings.Username,
ModVersion = Main.CurrentVersion,
}.Pack(outgoingMessage);
Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage);
// Send HandshakePacket
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
var handshake=new Packets.Handshake()
{
PedID = Main.LocalPlayerID,
Username = Main.Settings.Username,
ModVersion = Main.CurrentVersion,
PassHashEncrypted=Security.Encrypt(password.GetHash())
};
Security.GetSymmetricKeysCrypted(out handshake.AesKeyCrypted,out handshake.AesIVCrypted);
handshake.Pack(outgoingMessage);
Client.Connect(ip[0], short.Parse(ip[1]), outgoingMessage);
Main.QueueAction(() => { GTA.UI.Notification.Show($"~y~Trying to connect..."); });
});
} }
} }
public static bool IsOnServer public static bool IsOnServer
@ -111,39 +126,18 @@ namespace RageCoop.Client
} }
/*
private static void DecodeNativeCallWithResponse(Packets.NativeResponse packet)
{
object result = DecodeNativeCall(packet.Hash, packet.Args, true, packet.ResultType);
if (Main.CheckNativeHash.ContainsKey(packet.Hash))
{
foreach (KeyValuePair<ulong, byte> hash in Main.CheckNativeHash)
{
if (hash.Key == packet.Hash)
{
lock (Main.ServerItems)
{
Main.ServerItems.Add((int)result, hash.Value);
}
break;
}
}
}
NetOutgoingMessage outgoingMessage = Client.CreateMessage();
new Packets.NativeResponse()
{
Hash = 0,
Args = new List<object>() { result },
ID = packet.ID
}.Pack(outgoingMessage);
Client.SendMessage(outgoingMessage, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
Client.FlushSendQueue();
}
*/
#endregion // -- PLAYER -- #endregion // -- PLAYER --
private static void GetServerPublicKey(string address,int timeout=10000)
{
var msg=Client.CreateMessage();
new Packets.PublicKeyRequest().Pack(msg);
var adds =address.Split(':');
Client.SendUnconnectedMessage(msg,adds[0],int.Parse(adds[1]));
PublicKeyReceived.WaitOne(timeout);
}
#endregion #endregion
} }

View File

@ -6,13 +6,16 @@ using Lidgren.Network;
using RageCoop.Core; using RageCoop.Core;
using GTA; using GTA;
using RageCoop.Client.Menus; using RageCoop.Client.Menus;
using System.Threading;
using GTA.Math; using GTA.Math;
using GTA.Native; using GTA.Native;
using System.Net;
namespace RageCoop.Client namespace RageCoop.Client
{ {
internal static partial class Networking internal static partial class Networking
{ {
private static AutoResetEvent PublicKeyReceived=new AutoResetEvent(false);
public static void ProcessMessage(NetIncomingMessage message) public static void ProcessMessage(NetIncomingMessage message)
{ {
if(message == null) { return; } if(message == null) { return; }
@ -30,34 +33,16 @@ namespace RageCoop.Client
#if !NON_INTERACTIVE #if !NON_INTERACTIVE
CoopMenu.InitiateConnectionMenuSetting(); CoopMenu.InitiateConnectionMenuSetting();
#endif #endif
Main.QueueAction(() => { GTA.UI.Notification.Show("~y~Trying to connect..."); return true; });
break; break;
case NetConnectionStatus.Connected: case NetConnectionStatus.Connected:
if (message.SenderConnection.RemoteHailMessage.ReadByte() != (byte)PacketTypes.Handshake) Main.QueueAction(() => {
{ CoopMenu.ConnectedMenuSetting();
Client.Disconnect("Wrong packet!"); Main.MainChat.Init();
} PlayerList.Cleanup();
else GTA.UI.Notification.Show("~g~Connected!");
{ });
int len = message.SenderConnection.RemoteHailMessage.ReadInt32();
byte[] data = message.SenderConnection.RemoteHailMessage.ReadBytes(len);
Packets.Handshake handshakePacket = new Packets.Handshake(); Main.Logger.Info(">> Connected <<");
handshakePacket.Unpack(data);
#if !NON_INTERACTIVE
#endif
Main.QueueAction(() => {
CoopMenu.ConnectedMenuSetting();
Main.MainChat.Init();
PlayerList.Cleanup();
GTA.UI.Notification.Show("~g~Connected!");
});
Main.Logger.Info(">> Connected <<");
}
break; break;
case NetConnectionStatus.Disconnected: case NetConnectionStatus.Disconnected:
DownloadManager.Cleanup(); DownloadManager.Cleanup();
@ -89,161 +74,173 @@ namespace RageCoop.Client
} }
break; break;
case NetIncomingMessageType.Data: case NetIncomingMessageType.Data:
if (message.LengthBytes==0) { break; }
var packetType = (PacketTypes)message.ReadByte();
try
{ {
int len = message.ReadInt32(); if (message.LengthBytes==0) { break; }
byte[] data = message.ReadBytes(len); var packetType = PacketTypes.Unknown;
switch (packetType) try
{ {
case PacketTypes.CleanUpWorld: packetType = (PacketTypes)message.ReadByte();
{ int len = message.ReadInt32();
Main.QueueAction(() => { Main.CleanUpWorld(); return true; }); byte[] data = message.ReadBytes(len);
} switch (packetType)
break; {
case PacketTypes.PlayerConnect: case PacketTypes.PlayerConnect:
{ {
Packets.PlayerConnect packet = new Packets.PlayerConnect(); Packets.PlayerConnect packet = new Packets.PlayerConnect();
packet.Unpack(data); packet.Unpack(data);
Main.QueueAction(() => PlayerConnect(packet)); Main.QueueAction(() => PlayerConnect(packet));
} }
break;
case PacketTypes.PlayerDisconnect:
{
Packets.PlayerDisconnect packet = new Packets.PlayerDisconnect();
packet.Unpack(data);
Main.QueueAction(() => PlayerDisconnect(packet));
}
break;
case PacketTypes.PlayerInfoUpdate:
{
var packet = new Packets.PlayerInfoUpdate();
packet.Unpack(data);
PlayerList.UpdatePlayer(packet);
break; break;
} case PacketTypes.PlayerDisconnect:
#region ENTITY SYNC {
case PacketTypes.VehicleSync:
{
Packets.VehicleSync packet = new Packets.VehicleSync(); Packets.PlayerDisconnect packet = new Packets.PlayerDisconnect();
packet.Unpack(data); packet.Unpack(data);
VehicleSync(packet); Main.QueueAction(() => PlayerDisconnect(packet));
} }
break;
case PacketTypes.PedSync:
{
Packets.PedSync packet = new Packets.PedSync();
packet.Unpack(data);
PedSync(packet);
}
break;
case PacketTypes.VehicleStateSync:
{
Packets.VehicleStateSync packet = new Packets.VehicleStateSync();
packet.Unpack(data);
VehicleStateSync(packet);
}
break;
case PacketTypes.PedStateSync:
{
Packets.PedStateSync packet = new Packets.PedStateSync();
packet.Unpack(data);
PedStateSync(packet);
}
break;
case PacketTypes.ProjectileSync:
{
Packets.ProjectileSync packet = new Packets.ProjectileSync();
packet.Unpack(data);
ProjectileSync(packet);
break; break;
} case PacketTypes.PlayerInfoUpdate:
#endregion {
case PacketTypes.ChatMessage: var packet = new Packets.PlayerInfoUpdate();
{ packet.Unpack(data);
PlayerList.UpdatePlayer(packet);
break;
}
#region ENTITY SYNC
case PacketTypes.VehicleSync:
{
Packets.ChatMessage packet = new Packets.ChatMessage(); Packets.VehicleSync packet = new Packets.VehicleSync();
packet.Unpack(data); packet.Unpack(data);
VehicleSync(packet);
Main.QueueAction(() => { Main.MainChat.AddMessage(packet.Username, packet.Message); return true; }); }
break;
case PacketTypes.PedSync:
{
Packets.PedSync packet = new Packets.PedSync();
packet.Unpack(data);
PedSync(packet);
}
break;
case PacketTypes.VehicleStateSync:
{
Packets.VehicleStateSync packet = new Packets.VehicleStateSync();
packet.Unpack(data);
VehicleStateSync(packet);
}
break;
case PacketTypes.PedStateSync:
{
} Packets.PedStateSync packet = new Packets.PedStateSync();
break; packet.Unpack(data);
case PacketTypes.CustomEvent: PedStateSync(packet);
{
Packets.CustomEvent packet = new Packets.CustomEvent();
packet.Unpack(data);
Scripting.API.Events.InvokeCustomEventReceived(packet);
}
break;
case PacketTypes.FileTransferChunk:
{
Packets.FileTransferChunk packet = new Packets.FileTransferChunk();
packet.Unpack(data);
DownloadManager.Write(packet.ID, packet.FileChunk); }
break;
case PacketTypes.ProjectileSync:
{
Packets.ProjectileSync packet = new Packets.ProjectileSync();
packet.Unpack(data);
ProjectileSync(packet);
break;
}
#endregion
case PacketTypes.ChatMessage:
{
} Packets.ChatMessage packet = new Packets.ChatMessage();
break; packet.Unpack(data);
case PacketTypes.FileTransferRequest:
{
Packets.FileTransferRequest packet = new Packets.FileTransferRequest();
packet.Unpack(data);
DownloadManager.AddFile(packet.ID, packet.Name, packet.FileLength); Main.QueueAction(() => { Main.MainChat.AddMessage(packet.Username, packet.Message); return true; });
}
break;
case PacketTypes.FileTransferComplete:
{
Packets.FileTransferComplete packet = new Packets.FileTransferComplete();
packet.Unpack(data);
Main.Logger.Debug($"Finalizing download:{packet.ID}"); }
DownloadManager.Complete(packet.ID); break;
case PacketTypes.CustomEvent:
{
Packets.CustomEvent packet = new Packets.CustomEvent();
packet.Unpack(data);
Scripting.API.Events.InvokeCustomEventReceived(packet);
}
break;
case PacketTypes.FileTransferChunk:
{
Packets.FileTransferChunk packet = new Packets.FileTransferChunk();
packet.Unpack(data);
} DownloadManager.Write(packet.ID, packet.FileChunk);
break;
default: }
if (packetType.IsSyncEvent()) break;
{ case PacketTypes.FileTransferRequest:
// Dispatch to main thread {
Main.QueueAction(() => { SyncEvents.HandleEvent(packetType, data); return true; }); Packets.FileTransferRequest packet = new Packets.FileTransferRequest();
} packet.Unpack(data);
break;
DownloadManager.AddFile(packet.ID, packet.Name, packet.FileLength);
}
break;
case PacketTypes.FileTransferComplete:
{
Packets.FileTransferComplete packet = new Packets.FileTransferComplete();
packet.Unpack(data);
Main.Logger.Debug($"Finalizing download:{packet.ID}");
DownloadManager.Complete(packet.ID);
}
break;
default:
if (packetType.IsSyncEvent())
{
// Dispatch to main thread
Main.QueueAction(() => { SyncEvents.HandleEvent(packetType, data); return true; });
}
break;
}
} }
catch (Exception ex)
{
Main.QueueAction(() => {
GTA.UI.Notification.Show("~r~~h~Packet Error");
return true;
});
Main.Logger.Error($"[{packetType}] {ex.Message}");
Main.Logger.Error(ex);
Client.Disconnect($"Packet Error [{packetType}]");
}
break;
} }
catch (Exception ex)
{
Main.QueueAction(() => {
GTA.UI.Notification.Show("~r~~h~Packet Error");
return true;
});
Main.Logger.Error($"[{packetType}] {ex.Message}");
Main.Logger.Error(ex);
Client.Disconnect($"Packet Error [{packetType}]");
}
break;
case NetIncomingMessageType.ConnectionLatencyUpdated: case NetIncomingMessageType.ConnectionLatencyUpdated:
Latency = message.ReadFloat(); Latency = message.ReadFloat();
break; break;
case NetIncomingMessageType.UnconnectedData:
{
var packetType = (PacketTypes)message.ReadByte();
int len = message.ReadInt32();
byte[] data = message.ReadBytes(len);
if (packetType==PacketTypes.PublicKeyResponse)
{
var packet=new Packets.PublicKeyResponse();
packet.Unpack(data);
Security.SetServerPublicKey(packet.Modulus,packet.Exponent);
PublicKeyReceived.Set();
}
break;
}
case NetIncomingMessageType.DebugMessage: case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.ErrorMessage: case NetIncomingMessageType.ErrorMessage:
case NetIncomingMessageType.WarningMessage: case NetIncomingMessageType.WarningMessage:

View File

@ -16,6 +16,7 @@ namespace RageCoop.Client.Scripting
{ {
API.RegisterCustomEventHandler(CustomEvents.SetAutoRespawn,SetAutoRespawn); API.RegisterCustomEventHandler(CustomEvents.SetAutoRespawn,SetAutoRespawn);
API.RegisterCustomEventHandler(CustomEvents.NativeCall,NativeCall); API.RegisterCustomEventHandler(CustomEvents.NativeCall,NativeCall);
API.RegisterCustomEventHandler(CustomEvents.CleanUpWorld, (s) => Main.QueueAction(() => Main.CleanUpWorld()));
} }
public override void OnStop() public override void OnStop()

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.IO;
using RageCoop.Core;
namespace RageCoop.Client
{
internal class Security
{
public RSA ServerRSA { get; set; }
public Aes ClientAes { get; set; }=Aes.Create();
private Logger Logger;
public Security(Logger logger)
{
Logger = logger;
ClientAes.GenerateKey();
ClientAes.GenerateIV();
}
public void GetSymmetricKeysCrypted(out byte[] cryptedKey,out byte[] cryptedIV)
{
// Logger?.Debug($"Aes.Key:{ClientAes.Key.Dump()}, Aes.IV:{ClientAes.IV.Dump()}");
cryptedKey =ServerRSA.Encrypt(ClientAes.Key, RSAEncryptionPadding.Pkcs1);
cryptedIV =ServerRSA.Encrypt(ClientAes.IV,RSAEncryptionPadding.Pkcs1);
}
public byte[] Encrypt(byte[] data)
{
return new CryptoStream(new MemoryStream(data), ClientAes.CreateEncryptor(), CryptoStreamMode.Read).ReadToEnd();
}
public void SetServerPublicKey(byte[] modulus,byte[] exponent)
{
var para = new RSAParameters();
para.Modulus = modulus;
para.Exponent = exponent;
ServerRSA=RSA.Create(para);
}
}
}

View File

@ -11,6 +11,8 @@ namespace RageCoop.Client
/// Don't use it! /// Don't use it!
/// </summary> /// </summary>
public string Username { get; set; } = "Player"; public string Username { get; set; } = "Player";
public string Password { get; set; } = "";
/// <summary> /// <summary>
/// Don't use it! /// Don't use it!
/// </summary> /// </summary>

View File

@ -9,6 +9,7 @@ using GTA.Native;
using System.IO; using System.IO;
using System.Xml.Serialization; using System.Xml.Serialization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace RageCoop.Client namespace RageCoop.Client
{ {
@ -197,6 +198,11 @@ namespace RageCoop.Client
{ {
Function.Call(Hash.SET_RADIO_TO_STATION_INDEX, index); Function.Call(Hash.SET_RADIO_TO_STATION_INDEX, index);
} }
public static byte[] GetHash(this string inputString)
{
using (HashAlgorithm algorithm = SHA256.Create())
return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
}
} }
} }

View File

@ -54,7 +54,10 @@ namespace RageCoop.Core
CurrentIndex += length; CurrentIndex += length;
return value; return value;
} }
public byte[] ReadByteArray()
{
return ReadByteArray(ReadInt());
}
public short ReadShort() public short ReadShort()
{ {
short value = BitConverter.ToInt16(ResultArray, CurrentIndex); short value = BitConverter.ToInt16(ResultArray, CurrentIndex);

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using GTA.Math; using GTA.Math;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Net; using System.Net;
using System.IO;
namespace RageCoop.Core namespace RageCoop.Core
{ {
public class CoreUtils public class CoreUtils
@ -82,6 +83,11 @@ namespace RageCoop.Core
bytes.AddInt(sb.Length); bytes.AddInt(sb.Length);
bytes.AddRange(sb); bytes.AddRange(sb);
} }
public static void AddArray(this List<byte> bytes, byte[] toadd)
{
bytes.AddInt(toadd.Length);
bytes.AddRange(toadd);
}
public static int GetHash(string s) public static int GetHash(string s)
{ {
@ -202,5 +208,28 @@ namespace RageCoop.Core
action(obj); action(obj);
} }
} }
public static byte[] ReadToEnd(this Stream stream)
{
if (stream is MemoryStream)
return ((MemoryStream)stream).ToArray();
using (var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
public static byte[] Join(this List<byte[]> arrays)
{
if (arrays.Count==1) { return arrays[0]; }
var output = new byte[arrays.Sum(arr => arr.Length)];
int writeIdx = 0;
foreach (var byteArr in arrays)
{
byteArr.CopyTo(output, writeIdx);
writeIdx += byteArr.Length;
}
return output;
}
} }
} }

View File

@ -13,12 +13,14 @@ namespace RageCoop.Core
PlayerConnect=1, PlayerConnect=1,
PlayerDisconnect=2, PlayerDisconnect=2,
PlayerInfoUpdate=3, PlayerInfoUpdate=3,
PublicKeyRequest=4,
PublicKeyResponse=5,
ChatMessage=10, ChatMessage=10,
// NativeCall=11, // NativeCall=11,
// NativeResponse=12, // NativeResponse=12,
//Mod=13, // Mod=13,
CleanUpWorld=14, // CleanUpWorld=14,
FileTransferChunk=15, FileTransferChunk=15,
FileTransferRequest=16, FileTransferRequest=16,
@ -49,6 +51,7 @@ namespace RageCoop.Core
#endregion #endregion
#endregion #endregion
Unknown=255
} }
public static class PacketExtensions public static class PacketExtensions
{ {

View File

@ -16,6 +16,21 @@ namespace RageCoop.Core
public string ModVersion { get; set; } public string ModVersion { get; set; }
/// <summary>
/// The asymetrically crypted Aes key
/// </summary>
public byte[] AesKeyCrypted;
/// <summary>
/// The asymetrically crypted Aes IV
/// </summary>
public byte[] AesIVCrypted;
/// <summary>
/// The password hash with client Aes
/// </summary>
public byte[] PassHashEncrypted { get; set; }
public override void Pack(NetOutgoingMessage message) public override void Pack(NetOutgoingMessage message)
{ {
#region PacketToNetOutGoingMessage #region PacketToNetOutGoingMessage
@ -36,6 +51,16 @@ namespace RageCoop.Core
byteArray.AddRange(BitConverter.GetBytes(modVersionBytes.Length)); byteArray.AddRange(BitConverter.GetBytes(modVersionBytes.Length));
byteArray.AddRange(modVersionBytes); byteArray.AddRange(modVersionBytes);
// Write AesKeyCrypted
byteArray.AddArray(AesKeyCrypted);
// Write AesIVCrypted
byteArray.AddArray(AesIVCrypted);
// Write PassHash
byteArray.AddArray(PassHashEncrypted);
byte[] result = byteArray.ToArray(); byte[] result = byteArray.ToArray();
message.Write(result.Length); message.Write(result.Length);
@ -52,12 +77,17 @@ namespace RageCoop.Core
PedID = reader.ReadInt(); PedID = reader.ReadInt();
// Read Username // Read Username
int usernameLength = reader.ReadInt(); Username = reader.ReadString(reader.ReadInt());
Username = reader.ReadString(usernameLength);
// Read ModVersion // Read ModVersion
int modVersionLength = reader.ReadInt(); ModVersion = reader.ReadString(reader.ReadInt());
ModVersion = reader.ReadString(modVersionLength);
AesKeyCrypted=reader.ReadByteArray();
AesIVCrypted=reader.ReadByteArray();
PassHashEncrypted=reader.ReadByteArray();
#endregion #endregion
} }
} }
@ -202,5 +232,52 @@ namespace RageCoop.Core
BlipColor=(GTA.BlipColor)reader.ReadByte(); BlipColor=(GTA.BlipColor)reader.ReadByte();
} }
} }
public class PublicKeyResponse : Packet
{
public byte[] Modulus;
public byte[] Exponent;
public override void Pack(NetOutgoingMessage message)
{
#region PacketToNetOutGoingMessage
message.Write((byte)PacketTypes.PublicKeyResponse);
List<byte> byteArray = new List<byte>();
byteArray.AddArray(Modulus);
byteArray.AddArray(Exponent);
byte[] result = byteArray.ToArray();
message.Write(result.Length);
message.Write(result);
#endregion
}
public override void Unpack(byte[] array)
{
#region NetIncomingMessageToPacket
var reader=new BitReader(array);
Modulus=reader.ReadByteArray();
Exponent=reader.ReadByteArray();
#endregion
}
}
public class PublicKeyRequest : Packet
{
public override void Pack(NetOutgoingMessage message)
{
#region PacketToNetOutGoingMessage
message.Write((byte)PacketTypes.PublicKeyRequest);
#endregion
}
public override void Unpack(byte[] array)
{
}
}
} }
} }

View File

@ -16,8 +16,8 @@ namespace RageCoop.Core.Scripting
public static readonly int OnPlayerDied = Hash("RageCoop.OnPlayerDied"); public static readonly int OnPlayerDied = Hash("RageCoop.OnPlayerDied");
public static readonly int SetAutoRespawn = Hash("RageCoop.SetAutoRespawn"); public static readonly int SetAutoRespawn = Hash("RageCoop.SetAutoRespawn");
public static readonly int NativeCall = Hash("RageCoop.NativeCall"); public static readonly int NativeCall = Hash("RageCoop.NativeCall");
// public static readonly int NativeCallNoResponse = Hash("RageCoop.NativeCallNoResponse");
public static readonly int NativeResponse = Hash("RageCoop.NativeResponse"); public static readonly int NativeResponse = Hash("RageCoop.NativeResponse");
public static readonly int CleanUpWorld = Hash("RageCoop.CleanUpWorld");
/// <summary> /// <summary>
/// Get a Int32 hash of a string. /// Get a Int32 hash of a string.
/// </summary> /// </summary>

View File

@ -10,6 +10,7 @@ namespace RageCoop.Server
{ {
public class ServerPed public class ServerPed
{ {
public int ID { get;internal set; }
/// <summary> /// <summary>
/// The ID of the ped's last vehicle. /// The ID of the ped's last vehicle.
/// </summary> /// </summary>
@ -56,6 +57,7 @@ namespace RageCoop.Server
public PlayerConfig Config { get { return _config; }set { _config=value;Server.SendPlayerInfos(); } } public PlayerConfig Config { get { return _config; }set { _config=value;Server.SendPlayerInfos(); } }
internal readonly Dictionary<int, Action<object>> Callbacks = new(); internal readonly Dictionary<int, Action<object>> Callbacks = new();
internal byte[] PublicKey { get; set; }
public bool IsReady { get; internal set; }=false; public bool IsReady { get; internal set; }=false;
public string Username { get;internal set; } = "N/A"; public string Username { get;internal set; } = "N/A";
#region CUSTOMDATA FUNCTIONS #region CUSTOMDATA FUNCTIONS
@ -119,114 +121,14 @@ namespace RageCoop.Server
Server.Logger?.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<"); Server.Logger?.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
} }
} }
/*
/// <summary> /// <summary>
/// Send a native call to client and ignore its return value. /// Send a CleanUpWorld message to this client.
/// </summary> /// </summary>
/// <param name="hash">The function's hash</param> /// <param name="clients"></param>
/// <param name="args">Arguments</param> public void SendCleanUpWorld(List<Client> clients = null)
public void SendNativeCall(ulong hash, params object[] args)
{ {
try SendCustomEvent(CustomEvents.CleanUpWorld, null);
{
NetConnection userConnection = Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID);
if (userConnection == null)
{
Server.Logger?.Error($"[Client->SendNativeCall(ulong hash, params object[] args)]: Connection \"{NetID}\" not found!");
return;
}
if (args != null && args.Length == 0)
{
Server.Logger?.Error($"[Client->SendNativeCall(ulong hash, Dictionary<string, object> args)]: Missing arguments!");
return;
}
Packets.NativeCall packet = new()
{
Hash = hash,
Args = new List<object>(args) ?? new List<object>(),
};
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, userConnection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
}
catch (Exception e)
{
Server.Logger?.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
} }
/// <summary>
/// Send a native call to client and do a callback when the response is received.
/// </summary>
/// <param name="callback">The callback to be invoked when the response is received.</param>
/// <param name="hash">The function's hash</param>
/// <param name="returnType">The return type of the response</param>
/// <param name="args">Arguments</param>
public void SendNativeCallWithResponse(Action<object> callback, GTA.Native.Hash hash, Type returnType, params object[] args)
{
try
{
NetConnection userConnection = Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID);
if (userConnection == null)
{
Server.Logger?.Error($"[Client->SendNativeResponse(Action<object> callback, ulong hash, Type type, params object[] args)]: Connection \"{NetID}\" not found!");
return;
}
if (args != null && args.Length == 0)
{
Server.Logger?.Error($"[Client->SendNativeCall(ulong hash, Dictionary<string, object> args)]: Missing arguments!");
return;
}
long id = ++_callbacksCount;
Callbacks.Add(id, callback);
byte returnTypeValue = 0x00;
if (returnType == typeof(int))
{
// NOTHING BECAUSE VALUE IS 0x00
}
else if (returnType == typeof(bool))
{
returnTypeValue = 0x01;
}
else if (returnType == typeof(float))
{
returnTypeValue = 0x02;
}
else if (returnType == typeof(string))
{
returnTypeValue = 0x03;
}
else if (returnType == typeof(Vector3))
{
returnTypeValue = 0x04;
}
else
{
Server.Logger?.Error($"[Client->SendNativeCall(ulong hash, Dictionary<string, object> args)]: Missing return type!");
return;
}
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
new Packets.NativeResponse()
{
Hash = (ulong)hash,
Args = new List<object>(args) ?? new List<object>(),
ResultType = returnTypeValue,
ID = id
}.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, userConnection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
}
catch (Exception e)
{
Server.Logger?.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
*/
/// <summary> /// <summary>
/// Send a native call to client and do a callback when the response received. /// Send a native call to client and do a callback when the response received.
@ -273,18 +175,6 @@ namespace RageCoop.Server
} }
return ID; return ID;
} }
public void SendCleanUpWorld()
{
NetConnection userConnection = Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID);
if (userConnection == null)
{
Server.Logger?.Error($"[Client->SendCleanUpWorld()]: Connection \"{NetID}\" not found!");
return;
}
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
outgoingMessage.Write((byte)PacketTypes.CleanUpWorld);
Server.MainNetServer.SendMessage(outgoingMessage, userConnection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default);
}
public void SendCustomEvent(int id,List<object> args) public void SendCustomEvent(int id,List<object> args)
{ {

View File

@ -196,20 +196,7 @@ namespace RageCoop.Server.Scripting
/// </summary> /// </summary>
public void SendCleanUpWorldToAll(List<Client> clients = null) public void SendCleanUpWorldToAll(List<Client> clients = null)
{ {
if (Server.MainNetServer.ConnectionsCount == 0) SendCustomEvent(CustomEvents.CleanUpWorld,null,clients);
{
return;
}
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
outgoingMessage.Write((byte)PacketTypes.CleanUpWorld);
if (clients == null)
{
Server.MainNetServer.SendToAll(outgoingMessage, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default);
}
else
{
clients.ForEach(client => { Server.MainNetServer.SendMessage(outgoingMessage,client.Connection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default); });
}
} }
/// <summary> /// <summary>
@ -248,7 +235,7 @@ namespace RageCoop.Server.Scripting
/// <param name="eventHash">An unique identifier of the event</param> /// <param name="eventHash">An unique identifier of the event</param>
/// <param name="args">The objects conataing your data, supported types: byte, short, ushort, int, uint, long, ulong, float, bool, string.</param> /// <param name="args">The objects conataing your data, supported types: byte, short, ushort, int, uint, long, ulong, float, bool, string.</param>
/// <param name="targets">The target clients to send. Leave it null to send to all clients</param> /// <param name="targets">The target clients to send. Leave it null to send to all clients</param>
public void SendCustomEvent(int eventHash,List<object> args,List<Client> targets=null) public void SendCustomEvent(int eventHash,List<object> args=null,List<Client> targets=null)
{ {
targets ??= new(Server.Clients.Values); targets ??= new(Server.Clients.Values);
var p = new Packets.CustomEvent() var p = new Packets.CustomEvent()

View File

@ -32,6 +32,11 @@ namespace RageCoop.Server.Scripting
{ {
public int ID { get; set; } public int ID { get; set; }
public string Username { get; set; } public string Username { get; set; }
/// <summary>
/// The hashed value of client password, sent with RSA asymmetric encryption.
/// </summary>
public string PasswordHash { get; set; }
public IPEndPoint EndPoint { get; set; } public IPEndPoint EndPoint { get; set; }
public void Deny(string reason) public void Deny(string reason)
{ {

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO;
using RageCoop.Core;
using System.Runtime.Serialization;
using System.Security.Cryptography;
namespace RageCoop.Server
{
internal class Security
{
private readonly Logger Logger;
public Security(Logger logger)
{
Logger= logger;
}
public RSA RSA=RSA.Create(2048);
private Dictionary<IPEndPoint, Aes> SecuredConnections = new Dictionary<IPEndPoint, Aes>();
public bool HasSecuredConnection(IPEndPoint target)
{
return SecuredConnections.ContainsKey(target);
}
public byte[] Encrypt(byte[] data, IPEndPoint target)
{
var ms=new MemoryStream();
using (var cs = new CryptoStream(ms, SecuredConnections[target].CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
}
return ms.ToArray();
}
public byte[] Decrypt(byte[] data, IPEndPoint target)
{
return new CryptoStream(new MemoryStream(data), SecuredConnections[target].CreateDecryptor(), CryptoStreamMode.Read).ReadToEnd();
}
public void AddConnection(IPEndPoint endpoint, byte[] cryptedKey,byte[] cryptedIV)
{
var key = RSA.Decrypt(cryptedKey, RSAEncryptionPadding.Pkcs1);
var iv = RSA.Decrypt(cryptedIV, RSAEncryptionPadding.Pkcs1);
// Logger?.Debug($"key:{key.Dump()}, iv:{iv.Dump()}");
var conAes = Aes.Create();
conAes.Key = key;
conAes.IV = iv;
if (!SecuredConnections.ContainsKey(endpoint))
{
SecuredConnections.Add(endpoint,conAes);
}
else
{
SecuredConnections[endpoint] = conAes;
}
}
public void RemoveConnection(IPEndPoint ep)
{
if (SecuredConnections.ContainsKey(ep))
{
SecuredConnections.Remove(ep);
}
}
public void GetPublicKey(out byte[] modulus, out byte[] exponent)
{
var key = RSA.ExportParameters(false);
modulus = key.Modulus;
exponent = key.Exponent;
}
public void ClearConnections()
{
SecuredConnections.Clear();
}
}
}

View File

@ -39,11 +39,13 @@ namespace RageCoop.Server
private Resources Resources; private Resources Resources;
public API API; public API API;
public Logger Logger; public Logger Logger;
private Security Security;
public Server(Logger logger=null) public Server(Logger logger=null)
{ {
Logger=logger; Logger=logger;
API=new API(this); API=new API(this);
Resources=new Resources(this); Resources=new Resources(this);
Security=new Security(Logger);
Logger?.Info("================"); Logger?.Info("================");
Logger?.Info($"Server bound to: 0.0.0.0:{MainSettings.Port}"); Logger?.Info($"Server bound to: 0.0.0.0:{MainSettings.Port}");
Logger?.Info($"Server version: {Assembly.GetCallingAssembly().GetName().Version}"); Logger?.Info($"Server version: {Assembly.GetCallingAssembly().GetName().Version}");
@ -57,10 +59,12 @@ namespace RageCoop.Server
MaximumConnections = MainSettings.MaxPlayers, MaximumConnections = MainSettings.MaxPlayers,
EnableUPnP = false, EnableUPnP = false,
AutoFlushSendQueue = true, AutoFlushSendQueue = true,
MaximumTransmissionUnit=2000, // PublicKeyResponse
}; };
config.EnableMessageType(NetIncomingMessageType.ConnectionApproval); config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
config.EnableMessageType(NetIncomingMessageType.ConnectionLatencyUpdated); config.EnableMessageType(NetIncomingMessageType.ConnectionLatencyUpdated);
config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
MainNetServer = new NetServer(config); MainNetServer = new NetServer(config);
MainNetServer.Start(); MainNetServer.Start();
@ -170,8 +174,6 @@ namespace RageCoop.Server
{ {
Logger?.Info("Listening for clients"); Logger?.Info("Listening for clients");
Logger?.Info("Please use CTRL + C if you want to stop the server!"); Logger?.Info("Please use CTRL + C if you want to stop the server!");
while (!Program.ReadyToStop) while (!Program.ReadyToStop)
{ {
try try
@ -191,10 +193,10 @@ namespace RageCoop.Server
Resources.StopAll(); Resources.StopAll();
} }
Client sender;
private void ProcessMessage(NetIncomingMessage message) private void ProcessMessage(NetIncomingMessage message)
{ {
if(message == null) { return; } Client sender;
if (message == null) { return; }
switch (message.MessageType) switch (message.MessageType)
{ {
case NetIncomingMessageType.ConnectionApproval: case NetIncomingMessageType.ConnectionApproval:
@ -214,12 +216,12 @@ namespace RageCoop.Server
Packets.Handshake packet = new(); Packets.Handshake packet = new();
packet.Unpack(data); packet.Unpack(data);
GetHandshake(message.SenderConnection, packet); GetHandshake(message.SenderConnection, packet);
} }
catch (Exception e) catch (Exception e)
{ {
Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: {e.Message}"); Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: {e.Message}");
Logger?.Error(e);
message.SenderConnection.Deny(e.Message); message.SenderConnection.Deny(e.Message);
} }
} }
@ -413,6 +415,19 @@ namespace RageCoop.Server
case NetIncomingMessageType.VerboseDebugMessage: case NetIncomingMessageType.VerboseDebugMessage:
Logger?.Debug(message.ReadString()); Logger?.Debug(message.ReadString());
break; break;
case NetIncomingMessageType.UnconnectedData:
{
if (message.ReadByte()==(byte)PacketTypes.PublicKeyRequest)
{
var msg = MainNetServer.CreateMessage();
var p=new Packets.PublicKeyResponse();
Security.GetPublicKey(out p.Modulus,out p.Exponent);
p.Pack(msg);
Logger?.Debug($"Sending public key to {message.SenderEndPoint}, length:{msg.LengthBytes}");
MainNetServer.SendUnconnectedMessage(msg, message.SenderEndPoint);
}
}
break;
default: default:
Logger?.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType, message.LengthBytes, message.DeliveryMethod, message.SequenceChannel)); Logger?.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType, message.LengthBytes, message.DeliveryMethod, message.SequenceChannel));
break; break;
@ -480,12 +495,25 @@ namespace RageCoop.Server
connection.Deny("Username is already taken!"); connection.Deny("Username is already taken!");
return; return;
} }
string passhash;
try
{
Security.AddConnection(connection.RemoteEndPoint, packet.AesKeyCrypted,packet.AesIVCrypted);
passhash=Security.Decrypt(packet.PassHashEncrypted,connection.RemoteEndPoint).GetString();
}
catch (Exception ex)
{
Logger?.Error($"Cannot process handshake packet from {connection.RemoteEndPoint}");
Logger?.Error(ex);
connection.Deny("Malformed handshak packet!");
return;
}
var args = new HandshakeEventArgs() var args = new HandshakeEventArgs()
{ {
EndPoint=connection.RemoteEndPoint, EndPoint=connection.RemoteEndPoint,
ID=packet.PedID, ID=packet.PedID,
Username=packet.Username Username=packet.Username,
PasswordHash=passhash,
}; };
API.Events.InvokePlayerHandshake(args); API.Events.InvokePlayerHandshake(args);
if (args.Cancel) if (args.Cancel)
@ -495,6 +523,7 @@ namespace RageCoop.Server
} }
connection.Approve();
Client tmpClient; Client tmpClient;
@ -510,23 +539,14 @@ namespace RageCoop.Server
ID=packet.PedID, ID=packet.PedID,
Player = new() Player = new()
{ {
ID= packet.PedID,
} }
} }
);; );;
} }
Logger?.Debug($"Handshake sucess, Player:{packet.Username} PedID:{packet.PedID}"); Logger?.Debug($"Handshake sucess, Player:{packet.Username} PedID:{packet.PedID}");
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
// Create a new handshake packet
new Packets.Handshake()
{
PedID = packet.PedID,
Username = string.Empty,
ModVersion = string.Empty,
}.Pack(outgoingMessage);
// Accept the connection and send back a new handshake packet with the connection ID
connection.Approve(outgoingMessage);
} }
// The connection has been approved, now we need to send all other players to the new player and the new player to all players // The connection has been approved, now we need to send all other players to the new player and the new player to all players