using System; using System.IO; using System.Security.Cryptography; using System.Text; using UnityEngine; public static class AllyHeroDeployLedgerStorage { private const string SecretSeed = "ban_total.ally_hero_deploy_ledger.v1"; private const string VaultDirectoryName = ".cache_bridge"; private const string MainFileName = ".ahd.dat"; private const string BackupFileName = ".ahd.bak"; private const string TempFileName = ".ahd.tmp"; private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName); private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName); private static string BackupFilePath => Path.Combine(VaultDirectoryPath, BackupFileName); private static string TempFilePath => Path.Combine(VaultDirectoryPath, TempFileName); public static bool HasExistingSaveFile() { return File.Exists(MainFilePath) || File.Exists(BackupFilePath); } public static bool TryLoad(out AllyHeroDeployLedgerPayload payload) { payload = CreateDefaultPayload(); if (TryReadPayload(MainFilePath, out payload)) { return true; } if (TryReadPayload(BackupFilePath, out payload)) { TrySave(payload); return true; } return false; } public static bool TrySave(AllyHeroDeployLedgerPayload payload) { try { Directory.CreateDirectory(VaultDirectoryPath); TryHidePath(VaultDirectoryPath); string envelopeJson = BuildEnvelopeJson(payload); File.WriteAllText(TempFilePath, envelopeJson, Encoding.UTF8); TryHidePath(TempFilePath); if (File.Exists(MainFilePath)) { File.Copy(MainFilePath, BackupFilePath, true); TryHidePath(BackupFilePath); } File.Copy(TempFilePath, MainFilePath, true); TryHidePath(MainFilePath); File.Delete(TempFilePath); return true; } catch (Exception ex) { Debug.LogWarning("[AllyHeroDeployLedgerStorage] Save failed: " + ex.Message); return false; } } public static AllyHeroDeployLedgerPayload CreateDefaultPayload() { return new AllyHeroDeployLedgerPayload { version = 1, lastUpdatedUtcTicks = DateTime.UtcNow.Ticks, entries = new System.Collections.Generic.List() }; } private static bool TryReadPayload(string path, out AllyHeroDeployLedgerPayload payload) { payload = CreateDefaultPayload(); if (!File.Exists(path)) { return false; } try { string envelopeJson = File.ReadAllText(path, Encoding.UTF8); AllyHeroDeployLedgerEnvelope envelope = JsonUtility.FromJson(envelopeJson); if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature)) { return false; } string expectedSignature = ComputeSignature(envelope.payload); if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal)) { Debug.LogWarning("[AllyHeroDeployLedgerStorage] Save signature mismatch. Possible tampering detected."); return false; } byte[] encryptedBytes = Convert.FromBase64String(envelope.payload); byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes()); string payloadJson = Encoding.UTF8.GetString(plainBytes); AllyHeroDeployLedgerPayload loadedPayload = JsonUtility.FromJson(payloadJson); if (loadedPayload == null) { return false; } payload = loadedPayload; return true; } catch (Exception ex) { Debug.LogWarning("[AllyHeroDeployLedgerStorage] Load failed from '" + path + "': " + ex.Message); return false; } } private static string BuildEnvelopeJson(AllyHeroDeployLedgerPayload payload) { payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks; string payloadJson = JsonUtility.ToJson(payload, false); byte[] plainBytes = Encoding.UTF8.GetBytes(payloadJson); byte[] encryptedBytes = XorTransform(plainBytes, BuildKeyBytes()); string payloadBase64 = Convert.ToBase64String(encryptedBytes); AllyHeroDeployLedgerEnvelope envelope = new AllyHeroDeployLedgerEnvelope { payload = payloadBase64, signature = ComputeSignature(payloadBase64) }; return JsonUtility.ToJson(envelope, false); } private static string ComputeSignature(string payloadBase64) { string signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier; using (SHA256 sha = SHA256.Create()) { byte[] bytes = Encoding.UTF8.GetBytes(signText); byte[] hash = sha.ComputeHash(bytes); return Convert.ToBase64String(hash); } } private static byte[] BuildKeyBytes() { using (SHA256 sha = SHA256.Create()) { string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed; return sha.ComputeHash(Encoding.UTF8.GetBytes(seed)); } } private static byte[] XorTransform(byte[] source, byte[] key) { byte[] result = new byte[source.Length]; for (int i = 0; i < source.Length; i++) { result[i] = (byte)(source[i] ^ key[i % key.Length]); } return result; } private static void TryHidePath(string path) { try { if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer) { return; } FileAttributes attributes = File.GetAttributes(path); if ((attributes & FileAttributes.Hidden) == 0) { File.SetAttributes(path, attributes | FileAttributes.Hidden); } } catch { } } }