Decompiled source of ModIntegrity v1.0.1

IronLabs.ModIntegrity.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using IronLabs.SharedLib;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("IronLabs.ModIntegrity")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("IronLabs.ModIntegrity")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("9B7B2B9F-38C2-4E87-A7BD-E29310479B42")]
[assembly: AssemblyFileVersion("1.0.1")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.1.40292")]
namespace IronLabs.ModIntegrity
{
	internal sealed class ConnectionState
	{
		internal string Nonce { get; }

		internal bool IsValidated { get; set; }

		internal ConnectionState(string nonce)
		{
			Nonce = nonce;
		}
	}
	internal static class ConnectionValidator
	{
		private static readonly Dictionary<ZRpc, ConnectionState> ServerStates = new Dictionary<ZRpc, ConnectionState>();

		private static readonly Dictionary<ZRpc, string> ClientNonces = new Dictionary<ZRpc, string>();

		internal static void Register(ZRpc rpc)
		{
			rpc.Register<ZPackage>("IronLabs.ModIntegrity.Challenge", (Action<ZRpc, ZPackage>)ReceiveChallenge);
			rpc.Register<ZPackage>("IronLabs.ModIntegrity.Response", (Action<ZRpc, ZPackage>)ReceiveResponse);
		}

		internal static void SendChallenge(ZRpc rpc)
		{
			string nonce = CreateNonce();
			ServerStates[rpc] = new ConnectionState(nonce);
			rpc.Invoke("IronLabs.ModIntegrity.Challenge", new object[1] { IntegrityProtocol.CreateChallenge(nonce) });
			ModIntegrityPlugin.Log.LogDebug("Sent DLL integrity challenge to " + Describe(rpc) + ".");
		}

		internal static void SendResponse(ZRpc rpc)
		{
			if (!ClientNonces.TryGetValue(rpc, out var value))
			{
				ModIntegrityPlugin.Log.LogError("Server did not send a DLL integrity challenge.");
				return;
			}
			IReadOnlyList<DllRecord> readOnlyList = DllInventory.ReadClientInventory();
			rpc.Invoke("IronLabs.ModIntegrity.Response", new object[1] { IntegrityProtocol.CreateResponse(value, readOnlyList) });
			ModIntegrityPlugin.Log.LogDebug($"Sent {readOnlyList.Count} DLL hashes to the server.");
		}

		internal static bool IsValidated(ZRpc rpc)
		{
			ConnectionState value;
			return ServerStates.TryGetValue(rpc, out value) && value.IsValidated;
		}

		internal static void Remove(ZRpc rpc)
		{
			ServerStates.Remove(rpc);
			ClientNonces.Remove(rpc);
		}

		internal static void Clear()
		{
			ServerStates.Clear();
			ClientNonces.Clear();
		}

		private static void ReceiveChallenge(ZRpc rpc, ZPackage package)
		{
			try
			{
				ClientNonces[rpc] = IntegrityProtocol.ReadChallenge(package);
				ModIntegrityPlugin.Log.LogDebug("Received DLL integrity challenge from " + Describe(rpc) + ".");
			}
			catch (Exception ex)
			{
				ModIntegrityPlugin.Log.LogError("Invalid DLL integrity challenge: " + ex.Message);
			}
		}

		private static void ReceiveResponse(ZRpc rpc, ZPackage package)
		{
			try
			{
				ValidateResponse(rpc, package);
			}
			catch (Exception ex)
			{
				Reject(rpc, "Invalid integrity response: " + ex.Message);
			}
		}

		private static void ValidateResponse(ZRpc rpc, ZPackage package)
		{
			string nonce;
			IReadOnlyList<DllRecord> readOnlyList = IntegrityProtocol.ReadResponse(package, out nonce);
			if (!ServerStates.TryGetValue(rpc, out var value) || value.Nonce != nonce)
			{
				Reject(rpc, "The challenge nonce is missing, expired, or incorrect.");
				return;
			}
			ServerDllPolicy policy = DllInventory.ReadServerPolicy();
			InventoryDifference inventoryDifference = InventoryComparer.Compare(policy, readOnlyList);
			if (!inventoryDifference.IsMatch)
			{
				LogDifference(rpc, inventoryDifference);
				RejectConnection(rpc);
			}
			else
			{
				value.IsValidated = true;
				ModIntegrityPlugin.Log.LogInfo($"DLL integrity validated for {Describe(rpc)}: {readOnlyList.Count} exact matches.");
			}
		}

		private static void LogDifference(ZRpc rpc, InventoryDifference difference)
		{
			ModIntegrityPlugin.Log.LogError("DLL integrity validation failed for " + Describe(rpc) + ".");
			LogItems("Missing", difference.Missing);
			LogItems("Unexpected", difference.Unexpected);
			LogItems("SHA-256 mismatch", difference.Different);
		}

		private static void LogItems(string category, IEnumerable<string> items)
		{
			foreach (string item in items)
			{
				ModIntegrityPlugin.Log.LogError("- " + category + ": " + item);
			}
		}

		private static void Reject(ZRpc rpc, string reason)
		{
			ModIntegrityPlugin.Log.LogError("DLL integrity validation failed for " + Describe(rpc) + ": " + reason);
			RejectConnection(rpc);
		}

		private static void RejectConnection(ZRpc rpc)
		{
			rpc.Invoke("Error", new object[1] { 3 });
		}

		private static string CreateNonce()
		{
			byte[] array = new byte[32];
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				randomNumberGenerator.GetBytes(array);
			}
			return Convert.ToBase64String(array);
		}

		private static string Describe(ZRpc rpc)
		{
			object obj;
			if (rpc == null)
			{
				obj = null;
			}
			else
			{
				ISocket socket = rpc.GetSocket();
				obj = ((socket != null) ? socket.GetEndPointString() : null);
			}
			if (obj == null)
			{
				obj = "unknown peer";
			}
			return (string)obj;
		}
	}
	internal static class DllInventory
	{
		internal const string WhitelistDirectoryName = "ModIntegrity_Whitelist";

		internal const string GreylistDirectoryName = "ModIntegrity_Greylist";

		private static ServerDllPolicy _serverPolicy;

		internal static IReadOnlyList<DllRecord> ReadClientInventory()
		{
			return ReadDirectory(Paths.PluginPath);
		}

		internal static ServerDllPolicy ReadServerPolicy()
		{
			if (_serverPolicy != null)
			{
				return _serverPolicy;
			}
			if (!ZNet.instance.IsDedicated())
			{
				IReadOnlyList<DllRecord> required = ReadDirectory(Paths.PluginPath);
				_serverPolicy = new ServerDllPolicy(required, new List<DllRecord>());
				return _serverPolicy;
			}
			_serverPolicy = ReadDedicatedPolicy();
			ModIntegrityPlugin.Log.LogInfo($"Loaded {_serverPolicy.Required.Count} required and " + $"{_serverPolicy.Optional.Count} optional DLL hashes.");
			return _serverPolicy;
		}

		internal static void ClearServerReference()
		{
			_serverPolicy = null;
		}

		private static ServerDllPolicy ReadDedicatedPolicy()
		{
			string text = Path.Combine(Paths.ConfigPath, "ModIntegrity_Whitelist");
			if (!Directory.Exists(text))
			{
				throw new DirectoryNotFoundException("Required DLL directory not found: " + text);
			}
			string text2 = Path.Combine(Paths.ConfigPath, "ModIntegrity_Greylist");
			IReadOnlyList<DllRecord> readOnlyList2;
			if (!Directory.Exists(text2))
			{
				IReadOnlyList<DllRecord> readOnlyList = new List<DllRecord>();
				readOnlyList2 = readOnlyList;
			}
			else
			{
				readOnlyList2 = ReadDirectory(text2);
			}
			IReadOnlyList<DllRecord> optional = readOnlyList2;
			return new ServerDllPolicy(ReadDirectory(text), optional);
		}

		private static IReadOnlyList<DllRecord> ReadDirectory(string root)
		{
			return (from path in Directory.GetFiles(root, "*.dll", SearchOption.AllDirectories)
				select CreateRecord(root, path)).OrderBy<DllRecord, string>((DllRecord record) => record.RelativePath, StringComparer.OrdinalIgnoreCase).ToList();
		}

		private static DllRecord CreateRecord(string root, string path)
		{
			string text = path.Substring(root.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			text = text.Replace(Path.DirectorySeparatorChar, '/');
			using SHA256 sHA = SHA256.Create();
			using FileStream inputStream = File.OpenRead(path);
			return new DllRecord(text, ToHex(sHA.ComputeHash(inputStream)));
		}

		private static string ToHex(byte[] bytes)
		{
			return BitConverter.ToString(bytes).Replace("-", string.Empty);
		}
	}
	internal sealed class DllRecord
	{
		internal string RelativePath { get; }

		internal string Sha256 { get; }

		internal DllRecord(string relativePath, string sha256)
		{
			RelativePath = relativePath;
			Sha256 = sha256;
		}
	}
	internal static class IntegrityProtocol
	{
		internal const int Version = 1;

		internal const int MaximumDllCount = 1024;

		internal const string ChallengeRpc = "IronLabs.ModIntegrity.Challenge";

		internal const string ResponseRpc = "IronLabs.ModIntegrity.Response";

		internal static ZPackage CreateChallenge(string nonce)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(1);
			val.Write(nonce);
			return val;
		}

		internal static ZPackage CreateResponse(string nonce, IReadOnlyList<DllRecord> inventory)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(1);
			val.Write(nonce);
			val.Write(inventory.Count);
			foreach (DllRecord item in inventory)
			{
				val.Write(item.RelativePath);
				val.Write(item.Sha256);
			}
			return val;
		}

		internal static string ReadChallenge(ZPackage package)
		{
			AssertProtocolVersion(package.ReadInt());
			return package.ReadString();
		}

		internal static IReadOnlyList<DllRecord> ReadResponse(ZPackage package, out string nonce)
		{
			AssertProtocolVersion(package.ReadInt());
			nonce = package.ReadString();
			int num = package.ReadInt();
			if (num < 0 || num > 1024)
			{
				throw new InvalidOperationException($"Invalid DLL count: {num}.");
			}
			List<DllRecord> list = new List<DllRecord>(num);
			for (int i = 0; i < num; i++)
			{
				list.Add(new DllRecord(package.ReadString(), package.ReadString()));
			}
			return list;
		}

		private static void AssertProtocolVersion(int version)
		{
			if (version != 1)
			{
				throw new InvalidOperationException($"Unsupported integrity protocol {version}; expected {1}.");
			}
		}
	}
	internal static class InventoryComparer
	{
		internal static InventoryDifference Compare(ServerDllPolicy policy, IReadOnlyList<DllRecord> actual)
		{
			Dictionary<string, DllRecord> dictionary = ToDictionary(policy.Required);
			Dictionary<string, DllRecord> optional = ToDictionary(policy.Optional);
			Dictionary<string, DllRecord> actual2 = ToDictionary(actual);
			EnsureDisjoint(dictionary, optional);
			InventoryDifference inventoryDifference = new InventoryDifference();
			CompareRequired(dictionary, actual2, inventoryDifference);
			CompareOptional(optional, actual2, inventoryDifference);
			FindUnexpected(dictionary, optional, actual2, inventoryDifference);
			return inventoryDifference;
		}

		private static Dictionary<string, DllRecord> ToDictionary(IReadOnlyList<DllRecord> records)
		{
			return records.ToDictionary<DllRecord, string>((DllRecord record) => record.RelativePath, StringComparer.OrdinalIgnoreCase);
		}

		private static void CompareRequired(Dictionary<string, DllRecord> expected, Dictionary<string, DllRecord> actual, InventoryDifference difference)
		{
			foreach (KeyValuePair<string, DllRecord> item in expected.OrderBy((KeyValuePair<string, DllRecord> pair) => pair.Key))
			{
				if (!actual.TryGetValue(item.Key, out var value))
				{
					difference.Missing.Add(item.Key);
				}
				else if (!string.Equals(item.Value.Sha256, value.Sha256, StringComparison.OrdinalIgnoreCase))
				{
					difference.Different.Add(item.Key);
				}
			}
		}

		private static void CompareOptional(Dictionary<string, DllRecord> optional, Dictionary<string, DllRecord> actual, InventoryDifference difference)
		{
			foreach (KeyValuePair<string, DllRecord> item in optional)
			{
				if (actual.TryGetValue(item.Key, out var value) && !string.Equals(item.Value.Sha256, value.Sha256, StringComparison.OrdinalIgnoreCase))
				{
					difference.Different.Add(item.Key);
				}
			}
		}

		private static void FindUnexpected(Dictionary<string, DllRecord> required, Dictionary<string, DllRecord> optional, Dictionary<string, DllRecord> actual, InventoryDifference difference)
		{
			foreach (string item in from path in actual.Keys
				where !required.ContainsKey(path) && !optional.ContainsKey(path)
				orderby path
				select path)
			{
				difference.Unexpected.Add(item);
			}
		}

		private static void EnsureDisjoint(Dictionary<string, DllRecord> required, Dictionary<string, DllRecord> optional)
		{
			string text = required.Keys.FirstOrDefault(optional.ContainsKey);
			if (text != null)
			{
				throw new InvalidOperationException("DLL is present in both whitelist and greylist: " + text);
			}
		}
	}
	internal sealed class InventoryDifference
	{
		internal List<string> Missing { get; } = new List<string>();

		internal List<string> Unexpected { get; } = new List<string>();

		internal List<string> Different { get; } = new List<string>();

		internal bool IsMatch => Missing.Count == 0 && Unexpected.Count == 0 && Different.Count == 0;
	}
	[BepInPlugin("IronLabs.ModIntegrity", "IronLabs.ModIntegrity", "1.0.1")]
	public sealed class ModIntegrityPlugin : IronLabsPlugin
	{
		internal const string PluginGuid = "IronLabs.ModIntegrity";

		internal const string PluginName = "IronLabs.ModIntegrity";

		internal const string PluginVersion = "1.0.1";

		internal static ModLog Log { get; private set; }

		private void Awake()
		{
			Log = InitializePlugin("IronLabs.ModIntegrity");
			Log.LogInfo("IronLabs.ModIntegrity 1.0.1 is enforcing exact DLL integrity.");
		}

		private void OnDestroy()
		{
			ConnectionValidator.Clear();
			DllInventory.ClearServerReference();
			ShutdownPlugin();
			Log = null;
		}
	}
	[HarmonyPatch]
	internal static class NetworkPatches
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
		private static void RegisterIntegrityRpcs(ZNetPeer peer)
		{
			ConnectionValidator.Register(peer.m_rpc);
		}

		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyPatch(typeof(ZNet), "RPC_ServerHandshake")]
		private static void SendServerChallenge(ZRpc rpc)
		{
			ConnectionValidator.SendChallenge(rpc);
		}

		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")]
		private static void SendClientInventory(ZRpc rpc)
		{
			ConnectionValidator.SendResponse(rpc);
		}

		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		private static bool RejectUnvalidatedPeer(ZNet __instance, ZRpc rpc)
		{
			if (!__instance.IsServer() || ConnectionValidator.IsValidated(rpc))
			{
				return true;
			}
			ModIntegrityPlugin.Log.LogError("Rejected unvalidated peer " + rpc.GetSocket().GetEndPointString() + " before PeerInfo.");
			rpc.Invoke("Error", new object[1] { 3 });
			return false;
		}

		[HarmonyPrefix]
		[HarmonyPatch(typeof(ZNet), "Disconnect")]
		private static void RemoveConnectionState(ZNetPeer peer)
		{
			if (peer != null)
			{
				ConnectionValidator.Remove(peer.m_rpc);
			}
		}
	}
	internal sealed class ServerDllPolicy
	{
		internal IReadOnlyList<DllRecord> Required { get; }

		internal IReadOnlyList<DllRecord> Optional { get; }

		internal ServerDllPolicy(IReadOnlyList<DllRecord> required, IReadOnlyList<DllRecord> optional)
		{
			Required = required;
			Optional = optional;
		}
	}
}
namespace IronLabs.SharedLib
{
	public abstract class IronLabsPlugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		private bool _patchesApplied;

		protected ModLog InitializePlugin(string pluginGuid)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Expected O, but got Unknown
			ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger);
			Version version = ((object)this).GetType().Assembly.GetName().Version;
			modLog.LogInfo($"AssemblyVersion: {version}.");
			_harmony = new Harmony(pluginGuid);
			PatchOwnNamespace(modLog);
			return modLog;
		}

		protected void PatchOwnNamespace(ModLog log)
		{
			if (_patchesApplied)
			{
				log.LogDebug("Harmony patches are already active; skipping registration.");
				return;
			}
			string text = ((object)this).GetType().Namespace;
			Type[] types = Assembly.GetExecutingAssembly().GetTypes();
			foreach (Type type in types)
			{
				if (type.Namespace == text)
				{
					_harmony.CreateClassProcessor(type).Patch();
				}
			}
			_patchesApplied = true;
			log.LogDebug("Harmony patches were applied for the plugin namespace.");
		}

		protected void ShutdownPlugin()
		{
			if (_patchesApplied)
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				_patchesApplied = false;
			}
		}
	}
	public sealed class ModLog
	{
		private readonly ManualLogSource _logger;

		public ModLog(ManualLogSource logger)
		{
			_logger = logger;
		}

		public void LogFatal(object message)
		{
			Write((LogLevel)1, message);
		}

		public void LogError(object message)
		{
			Write((LogLevel)2, message);
		}

		public void LogWarning(object message)
		{
			Write((LogLevel)4, message);
		}

		public void LogMessage(object message)
		{
			Write((LogLevel)8, message);
		}

		public void LogInfo(object message)
		{
			Write((LogLevel)16, message);
		}

		public void LogDebug(object message)
		{
			Write((LogLevel)32, message);
		}

		public void Log(LogLevel level, object message)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			Write(level, message);
		}

		private void Write(LogLevel level, object message)
		{
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
			_logger.Log(level, (object)$"[{arg}] {message}");
		}
	}
}