加入用户最近100场战绩记录并实现展示

This commit is contained in:
FloatGaming
2026-03-16 23:18:58 +08:00
parent ec42f2e34b
commit f5c6f143c0
106 changed files with 12120 additions and 642 deletions
@@ -0,0 +1,555 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Reflection;
using System.Text;
using UnityEngine;
[Serializable]
public class SecureSaveEnvelope
{
public int version;
public string payload;
public string signature;
public long savedUtcTicks;
}
public static class SecureSaveVault
{
private const string SecretSeed = "ban_total.secure_save_v2";
private const string VaultDirectoryName = ".cache_bridge";
private static bool s_dpapiInitialized;
private static bool s_dpapiSupported;
private static MethodInfo s_dpapiProtectMethod;
private static MethodInfo s_dpapiUnprotectMethod;
private static object s_dpapiCurrentUserScope;
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
public static bool SaveJson<T>(string category, string key, T data, string legacyPlainPath = null)
{
if (!typeof(T).IsValueType && (object)data == null)
{
return false;
}
string json = JsonUtility.ToJson(data, false);
return SaveRawJson(category, key, json, legacyPlainPath);
}
public static bool TryLoadJson<T>(string category, string key, out T data, string legacyPlainPath = null)
{
data = default(T);
string json;
if (!TryLoadRawJson(category, key, out json, legacyPlainPath))
{
return false;
}
if (string.IsNullOrEmpty(json))
{
return false;
}
try
{
data = JsonUtility.FromJson<T>(json);
if (typeof(T).IsValueType)
{
return true;
}
return (object)data != null;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] JSON parse failed: {ex.Message}");
data = default(T);
return false;
}
}
public static bool SaveRawJson(string category, string key, string json, string legacyPlainPath = null)
{
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key) || json == null)
{
return false;
}
string mainPath = GetFilePath(category, key, ".dat");
string backupPath = GetFilePath(category, key, ".bak");
string tempPath = GetFilePath(category, key, ".tmp");
try
{
string directory = Path.GetDirectoryName(mainPath);
Directory.CreateDirectory(directory);
TryHidePath(directory);
string envelopeJson = BuildEnvelopeJson(category, key, json);
File.WriteAllText(tempPath, envelopeJson, Encoding.UTF8);
TryHidePath(tempPath);
if (File.Exists(mainPath))
{
File.Copy(mainPath, backupPath, true);
TryHidePath(backupPath);
}
File.Copy(tempPath, mainPath, true);
TryHidePath(mainPath);
File.Delete(tempPath);
DeleteLegacyPlainFile(legacyPlainPath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Save failed ({category}/{key}): {ex.Message}");
return false;
}
}
public static bool TryLoadRawJson(string category, string key, out string json, string legacyPlainPath = null)
{
json = null;
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key))
{
return false;
}
string mainPath = GetFilePath(category, key, ".dat");
string backupPath = GetFilePath(category, key, ".bak");
if (TryReadEncryptedFile(category, key, mainPath, out json))
{
return true;
}
if (TryReadEncryptedFile(category, key, backupPath, out json))
{
SaveRawJson(category, key, json, legacyPlainPath);
return true;
}
if (!string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath))
{
try
{
json = File.ReadAllText(legacyPlainPath, Encoding.UTF8);
if (!string.IsNullOrEmpty(json))
{
SaveRawJson(category, key, json, legacyPlainPath);
return true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Legacy migration failed ({legacyPlainPath}): {ex.Message}");
}
}
return false;
}
public static bool Delete(string category, string key, string legacyPlainPath = null)
{
try
{
DeleteIfExists(GetFilePath(category, key, ".dat"));
DeleteIfExists(GetFilePath(category, key, ".bak"));
DeleteIfExists(GetFilePath(category, key, ".tmp"));
DeleteLegacyPlainFile(legacyPlainPath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Delete failed ({category}/{key}): {ex.Message}");
return false;
}
}
public static List<string> LoadAllRawJson(string category, string legacyDirectory = null, string legacySearchPattern = "*.json")
{
var result = new List<string>();
if (string.IsNullOrWhiteSpace(category))
{
return result;
}
string categoryDirectory = GetCategoryDirectory(category);
if (Directory.Exists(categoryDirectory))
{
string[] encryptedFiles = Directory.GetFiles(categoryDirectory, "*.dat", SearchOption.TopDirectoryOnly);
for (int i = 0; i < encryptedFiles.Length; i++)
{
string path = encryptedFiles[i];
string key = ExtractKeyFromFileName(path);
if (string.IsNullOrEmpty(key))
{
continue;
}
string json;
if (TryReadEncryptedFile(category, key, path, out json) && !string.IsNullOrEmpty(json))
{
result.Add(json);
}
}
}
if (!string.IsNullOrEmpty(legacyDirectory) && Directory.Exists(legacyDirectory))
{
string[] legacyFiles = Directory.GetFiles(legacyDirectory, legacySearchPattern, SearchOption.TopDirectoryOnly);
for (int i = 0; i < legacyFiles.Length; i++)
{
string legacyPath = legacyFiles[i];
try
{
string json = File.ReadAllText(legacyPath, Encoding.UTF8);
if (string.IsNullOrEmpty(json))
{
continue;
}
string legacyKey = Path.GetFileNameWithoutExtension(legacyPath);
SaveRawJson(category, legacyKey, json, legacyPath);
result.Add(json);
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Legacy bulk migration failed ({legacyPath}): {ex.Message}");
}
}
}
return result;
}
public static int CountEncryptedFiles(string category)
{
if (string.IsNullOrWhiteSpace(category))
{
return 0;
}
string categoryDirectory = GetCategoryDirectory(category);
if (!Directory.Exists(categoryDirectory))
{
return 0;
}
return Directory.GetFiles(categoryDirectory, "*.dat", SearchOption.TopDirectoryOnly).Length;
}
private static bool TryReadEncryptedFile(string category, string key, string filePath, out string json)
{
json = null;
if (!File.Exists(filePath))
{
return false;
}
try
{
string envelopeJson = File.ReadAllText(filePath, Encoding.UTF8);
var envelope = JsonUtility.FromJson<SecureSaveEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
string expectedSignature = ComputeSignature(category, envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
{
Debug.LogWarning($"[SecureSaveVault] Signature mismatch ({category}/{key}). Possible tampering detected.");
return false;
}
byte[] protectedBytes = Convert.FromBase64String(envelope.payload);
byte[] plainBytes;
if (!TryUnprotectBytes(category, key, protectedBytes, out plainBytes))
{
return false;
}
json = Encoding.UTF8.GetString(plainBytes);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Read failed ({category}/{key}): {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(string category, string key, string json)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(json);
byte[] protectedBytes = ProtectBytes(category, key, plainBytes);
string payloadBase64 = Convert.ToBase64String(protectedBytes);
var envelope = new SecureSaveEnvelope
{
version = 2,
payload = payloadBase64,
signature = ComputeSignature(category, payloadBase64),
savedUtcTicks = DateTime.UtcNow.Ticks
};
return JsonUtility.ToJson(envelope, false);
}
private static string GetCategoryDirectory(string category)
{
string safeCategory = ShortHash("cat|" + category);
return Path.Combine(VaultDirectoryPath, "." + safeCategory);
}
private static string GetFilePath(string category, string key, string extension)
{
string categoryDirectory = GetCategoryDirectory(category);
string safeKey = ShortHash("key|" + key);
return Path.Combine(categoryDirectory, "." + safeKey + extension);
}
private static string ExtractKeyFromFileName(string path)
{
string fileName = Path.GetFileNameWithoutExtension(path);
if (string.IsNullOrEmpty(fileName))
{
return null;
}
return fileName.StartsWith(".") ? fileName.Substring(1) : fileName;
}
private static string ComputeSignature(string category, string payloadBase64)
{
string signText = payloadBase64 + "|" + category + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
return Convert.ToBase64String(hash);
}
}
private static byte[] ProtectBytes(string category, string key, byte[] plainBytes)
{
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
byte[] dpapiBytes;
if (TryProtectWithDpapi(category, plainBytes, out dpapiBytes))
{
return dpapiBytes;
}
#endif
using (var aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key);
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
{
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
byte[] result = new byte[aes.IV.Length + cipherBytes.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length);
return result;
}
}
}
private static bool TryUnprotectBytes(string category, string key, byte[] protectedBytes, out byte[] plainBytes)
{
plainBytes = null;
try
{
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
if (TryUnprotectWithDpapi(category, protectedBytes, out plainBytes))
{
return plainBytes != null;
}
#endif
using (var aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
{
return false;
}
byte[] iv = new byte[ivLength];
byte[] cipher = new byte[protectedBytes.Length - ivLength];
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor())
{
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
return plainBytes != null;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Decrypt failed ({category}/{key}): {ex.Message}");
plainBytes = null;
return false;
}
}
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
private static bool TryProtectWithDpapi(string category, byte[] plainBytes, out byte[] protectedBytes)
{
protectedBytes = null;
if (!EnsureDpapi())
{
return false;
}
try
{
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category), s_dpapiCurrentUserScope }) as byte[];
return protectedBytes != null && protectedBytes.Length > 0;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] DPAPI protect failed, fallback to AES: {ex.Message}");
protectedBytes = null;
return false;
}
}
private static bool TryUnprotectWithDpapi(string category, byte[] protectedBytes, out byte[] plainBytes)
{
plainBytes = null;
if (!EnsureDpapi())
{
return false;
}
try
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category), s_dpapiCurrentUserScope }) as byte[];
return plainBytes != null && plainBytes.Length > 0;
}
catch
{
plainBytes = null;
return false;
}
}
private static bool EnsureDpapi()
{
if (s_dpapiInitialized)
{
return s_dpapiSupported;
}
s_dpapiInitialized = true;
try
{
Type protectedDataType =
Type.GetType("System.Security.Cryptography.ProtectedData, System.Security.Cryptography.ProtectedData") ??
Type.GetType("System.Security.Cryptography.ProtectedData, System.Security");
Type scopeType =
Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security.Cryptography.ProtectedData") ??
Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security");
if (protectedDataType == null || scopeType == null)
{
s_dpapiSupported = false;
return false;
}
s_dpapiProtectMethod = protectedDataType.GetMethod("Protect", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(byte[]), typeof(byte[]), scopeType }, null);
s_dpapiUnprotectMethod = protectedDataType.GetMethod("Unprotect", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(byte[]), typeof(byte[]), scopeType }, null);
if (s_dpapiProtectMethod == null || s_dpapiUnprotectMethod == null)
{
s_dpapiSupported = false;
return false;
}
s_dpapiCurrentUserScope = Enum.Parse(scopeType, "CurrentUser");
s_dpapiSupported = s_dpapiCurrentUserScope != null;
return s_dpapiSupported;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] DPAPI initialize failed, fallback to AES: {ex.Message}");
s_dpapiSupported = false;
return false;
}
}
#endif
private static byte[] BuildEntropy(string category)
{
string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
using (var sha = SHA256.Create())
{
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] BuildAesKey(string category, string key)
{
return BuildEntropy(category);
}
private static string ShortHash(string value)
{
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + Application.identifier + "|" + SecretSeed));
return BitConverter.ToString(hash, 0, 12).Replace("-", string.Empty).ToLowerInvariant();
}
}
private static void DeleteLegacyPlainFile(string legacyPlainPath)
{
if (string.IsNullOrEmpty(legacyPlainPath) || !File.Exists(legacyPlainPath))
{
return;
}
DeleteIfExists(legacyPlainPath);
}
private static void DeleteIfExists(string path)
{
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
File.Delete(path);
}
}
private static void TryHidePath(string path)
{
try
{
if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer)
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Hidden) == 0)
{
File.SetAttributes(path, attributes | FileAttributes.Hidden);
}
}
catch
{
}
}
}