using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using UnityEngine; using UnityEngine.SceneManagement; /// /// 文件完整性检查器 /// 在游戏启动时检查exe目录下的所有exe、dll等文件的MD5值 /// public class FileIntegrityChecker { // 需要检查的文件扩展名 private static readonly string[] TargetExtensions = { ".exe", ".dll", ".so", ".dylib" }; // 加密存储的文件名 private static readonly string ChecksumFileName = "file_checksums.enc"; // GUI显示相关的静态变量 public static string CheckStatusMessage = ""; // 检查状态消息 public static List MismatchedFilesList = new List(); // 不匹配的文件列表 public static bool IsFirstRun = false; // 是否是首次运行 public static int TotalFilesCount = 0; // 总文件数 // 加密密钥(基于应用程序路径生成,确保一致性) private static byte[] GetEncryptionKey() { // 使用应用程序路径生成固定密钥 string appPath = Application.dataPath; using (SHA256 sha256 = SHA256.Create()) { return sha256.ComputeHash(Encoding.UTF8.GetBytes(appPath + "FileIntegrityKey2024")); } } /// /// 游戏启动时自动执行文件完整性检查 /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void OnGameStart() { try { // 获取exe所在目录 string exeDirectory = GetExeDirectory(); if (string.IsNullOrEmpty(exeDirectory)) { Debug.LogError("[文件完整性检查] 无法获取exe目录路径"); return; } // 获取streamingAssetsPath目录(用于保存校验文件) string streamingAssetsPath = Application.streamingAssetsPath; // 如果streamingAssetsPath目录不存在,创建它 if (!Directory.Exists(streamingAssetsPath)) { Directory.CreateDirectory(streamingAssetsPath); } // 校验文件保存在streamingAssetsPath目录下 string checksumFilePath = Path.Combine(streamingAssetsPath, ChecksumFileName); // 判断是首次运行还是校验模式 if (!File.Exists(checksumFilePath)) { // 首次运行:计算并保存所有文件的MD5(排除streamingAssetsPath目录) Debug.Log("[文件完整性检查] 首次运行,正在计算文件MD5值..."); Dictionary fileChecksums = CalculateAllFileChecksums(exeDirectory, streamingAssetsPath); SaveChecksumsEncrypted(checksumFilePath, fileChecksums); // 更新GUI显示信息 IsFirstRun = true; TotalFilesCount = fileChecksums.Count; CheckStatusMessage = $"首次运行完成!共保存 {fileChecksums.Count} 个文件的MD5值"; MismatchedFilesList.Clear(); Debug.Log($"[文件完整性检查] 已完成!共保存 {fileChecksums.Count} 个文件的MD5值"); } else { // 后续运行:校验文件MD5(排除streamingAssetsPath目录) Debug.Log("[文件完整性检查] 正在校验文件完整性..."); Dictionary savedChecksums = LoadChecksumsDecrypted(checksumFilePath); List mismatchedFiles = VerifyFileChecksums(exeDirectory, savedChecksums, streamingAssetsPath); // 更新GUI显示信息 IsFirstRun = false; TotalFilesCount = savedChecksums.Count; MismatchedFilesList = mismatchedFiles; if (mismatchedFiles.Count > 0) { // 打印所有不匹配的文件 CheckStatusMessage = $"发现 {mismatchedFiles.Count} 个文件MD5不匹配!"; Debug.LogError($"[文件完整性检查] 发现 {mismatchedFiles.Count} 个文件MD5不匹配!"); foreach (string file in mismatchedFiles) { Debug.LogError($"[文件完整性检查] MD5不匹配: {file}"); } } else { CheckStatusMessage = "所有文件完整性校验通过!"; Debug.Log("[文件完整性检查] 所有文件完整性校验通过!"); SceneManager.LoadScene("GameLauncher"); } } // // 创建GUI显示对象 // CreateGUIDisplay(); } catch (Exception ex) { Debug.LogError($"[文件完整性检查] 执行过程中发生错误: {ex.Message}\n{ex.StackTrace}"); } } /// /// 获取exe所在目录 /// private static string GetExeDirectory() { // 在Unity编辑器中,Application.dataPath指向Assets文件夹 // 在构建后的游戏中,Application.dataPath指向"游戏名_Data"文件夹 string dataPath = Application.dataPath; // 获取exe所在目录(dataPath的父目录) string exeDirectory = Directory.GetParent(dataPath)?.FullName; // 输出调试信息 Debug.Log($"[文件完整性检查] Application.dataPath: {dataPath}"); Debug.Log($"[文件完整性检查] Exe目录: {exeDirectory}"); return exeDirectory; } /// /// 计算指定目录下所有目标文件的MD5值(递归搜索所有子目录,排除指定目录) /// private static Dictionary CalculateAllFileChecksums(string directory, string excludeDirectory) { Dictionary checksums = new Dictionary(); if (!Directory.Exists(directory)) { Debug.LogWarning($"[文件完整性检查] 目录不存在: {directory}"); return checksums; } // 规范化路径,用于比较 string normalizedExcludeDir = Path.GetFullPath(excludeDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); Debug.Log($"[文件完整性检查] 开始扫描目录: {directory} (递归搜索所有子目录,排除: {normalizedExcludeDir})"); // 递归遍历目录下的所有文件(包括子目录) string[] allFiles = Directory.GetFiles(directory, "*", SearchOption.AllDirectories); Debug.Log($"[文件完整性检查] 找到 {allFiles.Length} 个文件,正在筛选目标文件..."); foreach (string filePath in allFiles) { // 排除streamingAssetsPath目录及其子目录中的文件 string normalizedFilePath = Path.GetFullPath(filePath); string normalizedFileDir = Path.GetDirectoryName(normalizedFilePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); // 检查文件是否在排除目录中 if (normalizedFileDir.StartsWith(normalizedExcludeDir, StringComparison.OrdinalIgnoreCase)) { continue; // 跳过排除目录中的文件 } string extension = Path.GetExtension(filePath).ToLower(); // 只处理目标扩展名的文件 if (TargetExtensions.Contains(extension)) { try { // 获取相对于exe目录的相对路径,用于保存和后续验证 string relativePath = Path.GetRelativePath(directory, filePath); string md5 = CalculateFileMD5(filePath); checksums[relativePath] = md5; Debug.Log($"[文件完整性检查] 已计算: {relativePath} -> {md5}"); } catch (Exception ex) { Debug.LogWarning($"[文件完整性检查] 计算文件MD5失败: {filePath}, 错误: {ex.Message}"); } } } Debug.Log($"[文件完整性检查] 扫描完成,共找到 {checksums.Count} 个目标文件"); return checksums; } /// /// 计算单个文件的MD5值 /// private static string CalculateFileMD5(string filePath) { using (MD5 md5 = MD5.Create()) { using (FileStream stream = File.OpenRead(filePath)) { byte[] hashBytes = md5.ComputeHash(stream); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } } } /// /// 加密保存MD5校验和字典 /// private static void SaveChecksumsEncrypted(string filePath, Dictionary checksums) { try { // 将字典序列化为JSON格式的字符串 StringBuilder jsonBuilder = new StringBuilder(); jsonBuilder.Append("{"); bool first = true; foreach (var kvp in checksums) { if (!first) jsonBuilder.Append(","); jsonBuilder.Append($"\"{kvp.Key}\":\"{kvp.Value}\""); first = false; } jsonBuilder.Append("}"); string jsonData = jsonBuilder.ToString(); byte[] dataBytes = Encoding.UTF8.GetBytes(jsonData); // 使用AES加密 byte[] key = GetEncryptionKey(); byte[] encryptedData = EncryptAES(dataBytes, key); // 保存加密后的数据 File.WriteAllBytes(filePath, encryptedData); Debug.Log($"[文件完整性检查] MD5数据已加密保存到: {filePath}"); } catch (Exception ex) { Debug.LogError($"[文件完整性检查] 保存加密数据失败: {ex.Message}"); throw; } } /// /// 解密并加载MD5校验和字典 /// private static Dictionary LoadChecksumsDecrypted(string filePath) { try { // 读取加密数据 byte[] encryptedData = File.ReadAllBytes(filePath); // 解密 byte[] key = GetEncryptionKey(); byte[] decryptedData = DecryptAES(encryptedData, key); // 解析JSON string jsonData = Encoding.UTF8.GetString(decryptedData); Dictionary checksums = new Dictionary(); // 简单的JSON解析(因为格式简单,可以手动解析) jsonData = jsonData.Trim('{', '}'); if (!string.IsNullOrEmpty(jsonData)) { string[] pairs = jsonData.Split(','); foreach (string pair in pairs) { string[] keyValue = pair.Split(':'); if (keyValue.Length == 2) { string fileName = keyValue[0].Trim('"'); string value = keyValue[1].Trim('"'); checksums[fileName] = value; } } } return checksums; } catch (Exception ex) { Debug.LogError($"[文件完整性检查] 加载解密数据失败: {ex.Message}"); return new Dictionary(); } } /// /// 校验文件MD5值(递归搜索所有子目录,排除指定目录) /// private static List VerifyFileChecksums(string directory, Dictionary savedChecksums, string excludeDirectory) { List mismatchedFiles = new List(); // 规范化路径,用于比较 string normalizedExcludeDir = Path.GetFullPath(excludeDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); Debug.Log($"[文件完整性检查] 开始校验文件,已保存 {savedChecksums.Count} 个文件的MD5值(排除目录: {normalizedExcludeDir})"); // 检查已保存的文件(使用相对路径) foreach (var kvp in savedChecksums) { string relativePath = kvp.Key; // 相对路径(如 "MonoBleedingEdge/EmbedRuntime/mono.dll") string savedMD5 = kvp.Value; string filePath = Path.Combine(directory, relativePath); // 检查文件是否在排除目录中 string normalizedFilePath = Path.GetFullPath(filePath); string normalizedFileDir = Path.GetDirectoryName(normalizedFilePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (normalizedFileDir.StartsWith(normalizedExcludeDir, StringComparison.OrdinalIgnoreCase)) { continue; // 跳过排除目录中的文件 } if (!File.Exists(filePath)) { // 文件不存在 mismatchedFiles.Add($"{relativePath} (文件已删除)"); Debug.LogWarning($"[文件完整性检查] 文件不存在: {relativePath}"); continue; } try { string currentMD5 = CalculateFileMD5(filePath); if (currentMD5 != savedMD5) { // MD5不匹配 mismatchedFiles.Add(relativePath); Debug.LogWarning($"[文件完整性检查] MD5不匹配 - 文件: {relativePath}, 保存的MD5: {savedMD5}, 当前的MD5: {currentMD5}"); } } catch (Exception ex) { Debug.LogWarning($"[文件完整性检查] 校验文件失败: {relativePath}, 错误: {ex.Message}"); mismatchedFiles.Add($"{relativePath} (校验失败)"); } } // 检查是否有新文件(不在保存列表中的目标文件,递归搜索所有子目录,排除指定目录) Debug.Log($"[文件完整性检查] 检查是否有新增文件..."); string[] currentFiles = Directory.GetFiles(directory, "*", SearchOption.AllDirectories); foreach (string filePath in currentFiles) { // 排除streamingAssetsPath目录及其子目录中的文件 string normalizedFilePath = Path.GetFullPath(filePath); string normalizedFileDir = Path.GetDirectoryName(normalizedFilePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); // 检查文件是否在排除目录中 if (normalizedFileDir.StartsWith(normalizedExcludeDir, StringComparison.OrdinalIgnoreCase)) { continue; // 跳过排除目录中的文件 } string extension = Path.GetExtension(filePath).ToLower(); if (TargetExtensions.Contains(extension)) { // 获取相对路径 string relativePath = Path.GetRelativePath(directory, filePath); if (!savedChecksums.ContainsKey(relativePath)) { mismatchedFiles.Add($"{relativePath} (新增文件,未在记录中)"); Debug.LogWarning($"[文件完整性检查] 发现新文件: {relativePath}"); } } } Debug.Log($"[文件完整性检查] 校验完成,发现 {mismatchedFiles.Count} 个不匹配的文件"); return mismatchedFiles; } /// /// AES加密 /// private static byte[] EncryptAES(byte[] data, byte[] key) { using (Aes aes = Aes.Create()) { // 使用密钥的前32字节作为AES密钥 byte[] aesKey = new byte[32]; Array.Copy(key, 0, aesKey, 0, Math.Min(key.Length, 32)); aes.Key = aesKey; // 使用密钥的后16字节作为IV byte[] iv = new byte[16]; if (key.Length >= 48) { Array.Copy(key, 32, iv, 0, 16); } else { // 如果密钥不够长,使用前16字节的哈希 using (MD5 md5 = MD5.Create()) { iv = md5.ComputeHash(key).Take(16).ToArray(); } } aes.IV = iv; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; using (ICryptoTransform encryptor = aes.CreateEncryptor()) { using (MemoryStream ms = new MemoryStream()) { using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { cs.Write(data, 0, data.Length); cs.FlushFinalBlock(); return ms.ToArray(); } } } } } /// /// AES解密 /// private static byte[] DecryptAES(byte[] encryptedData, byte[] key) { using (Aes aes = Aes.Create()) { // 使用密钥的前32字节作为AES密钥 byte[] aesKey = new byte[32]; Array.Copy(key, 0, aesKey, 0, Math.Min(key.Length, 32)); aes.Key = aesKey; // 使用密钥的后16字节作为IV byte[] iv = new byte[16]; if (key.Length >= 48) { Array.Copy(key, 32, iv, 0, 16); } else { // 如果密钥不够长,使用前16字节的哈希 using (MD5 md5 = MD5.Create()) { iv = md5.ComputeHash(key).Take(16).ToArray(); } } aes.IV = iv; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; using (ICryptoTransform decryptor = aes.CreateDecryptor()) { using (MemoryStream ms = new MemoryStream(encryptedData)) { using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) { using (MemoryStream result = new MemoryStream()) { cs.CopyTo(result); return result.ToArray(); } } } } } } // /// // /// 创建GUI显示对象 // /// // private static void CreateGUIDisplay() // { // // 检查是否已存在GUI显示对象 // GameObject existingGUI = GameObject.Find("FileIntegrityCheckerGUI"); // if (existingGUI == null) // { // // 创建新的GameObject用于显示GUI // GameObject guiObject = new GameObject("FileIntegrityCheckerGUI"); // guiObject.AddComponent(); // // 确保对象不会被销毁 // UnityEngine.Object.DontDestroyOnLoad(guiObject); // } // } } // // /// // /// 文件完整性检查器GUI显示组件 // /// 用于在屏幕上显示检查结果 // /// // public class FileIntegrityCheckerGUI : MonoBehaviour // { // // GUI显示位置和大小 // private Rect windowRect = new Rect(10, 10, 500, 300); // private bool showWindow = true; // private Vector2 scrollPosition = Vector2.zero; // // // void OnGUI() // // { // // // 绘制窗口 // // windowRect = GUILayout.Window(1001, windowRect, DrawWindow, "文件完整性检查"); // // } // // // /// // /// 绘制窗口内容 // /// // void DrawWindow(int windowID) // { // GUILayout.BeginVertical(); // // // 显示检查状态 // GUIStyle statusStyle = new GUIStyle(GUI.skin.label); // statusStyle.fontSize = 16; // statusStyle.fontStyle = FontStyle.Bold; // // if (FileIntegrityChecker.IsFirstRun) // { // statusStyle.normal.textColor = Color.cyan; // GUILayout.Label($"状态: {FileIntegrityChecker.CheckStatusMessage}", statusStyle); // GUILayout.Label($"总文件数: {FileIntegrityChecker.TotalFilesCount}", GUI.skin.label); // } // else // { // if (FileIntegrityChecker.MismatchedFilesList.Count > 0) // { // statusStyle.normal.textColor = Color.red; // GUILayout.Label($"状态: {FileIntegrityChecker.CheckStatusMessage}", statusStyle); // } // else // { // statusStyle.normal.textColor = Color.green; // GUILayout.Label($"状态: {FileIntegrityChecker.CheckStatusMessage}", statusStyle); // } // GUILayout.Label($"检查文件数: {FileIntegrityChecker.TotalFilesCount}", GUI.skin.label); // } // // GUILayout.Space(10); // // // 显示不匹配的文件列表 // if (FileIntegrityChecker.MismatchedFilesList.Count > 0) // { // GUILayout.Label("不匹配的文件列表:", GUI.skin.label); // // // 滚动视图 // scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150)); // // GUIStyle errorStyle = new GUIStyle(GUI.skin.label); // errorStyle.normal.textColor = Color.red; // errorStyle.wordWrap = true; // // foreach (string file in FileIntegrityChecker.MismatchedFilesList) // { // GUILayout.Label($"❌ {file}", errorStyle); // } // // GUILayout.EndScrollView(); // } // else if (!FileIntegrityChecker.IsFirstRun) // { // GUIStyle successStyle = new GUIStyle(GUI.skin.label); // successStyle.normal.textColor = Color.green; // successStyle.fontSize = 14; // GUILayout.Label("所有文件完整性正常!", successStyle); // } // // GUILayout.Space(10); // // // 关闭按钮 // if (GUILayout.Button("关闭窗口")) // { // showWindow = false; // Destroy(gameObject); // } // // GUILayout.EndVertical(); // // // 允许拖动窗口 // GUI.DragWindow(); // } // }