using System; using System.IO; using System.Security.Cryptography; using System.Text; using UnityEngine; public static class StoreOwnershipStorage { private const string SecretSeed = "ban_total.store_ownership.v1"; private const string VaultDirectoryName = ".cache_bridge"; private const string MainFileName = ".own.dat"; private const string BackupFileName = ".own.bak"; private const string TempFileName = ".own.tmp"; private const string RecoverySlotKey = "store_ownership_storage"; private static string VaultDirectoryPath { get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); } } private static string MainFilePath { get { return Path.Combine(VaultDirectoryPath, MainFileName); } } private static string BackupFilePath { get { return Path.Combine(VaultDirectoryPath, BackupFileName); } } private static string TempFilePath { get { return Path.Combine(VaultDirectoryPath, TempFileName); } } public static bool TryLoad(out StoreOwnershipPayload payload) { payload = CreateDefaultPayload(); if (TryReadPayloadFromVariants(MainFileName, out payload)) { return true; } if (TryReadPayloadFromVariants(BackupFileName, out payload)) { TrySave(payload); return true; } if (PlayerProgressBackupService.TryRestoreStoreOwnership(out payload)) { TrySave(payload); return true; } if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null) { TrySave(payload); return true; } return false; } public static bool HasAnyRecoverableState() { return HasAnyVaultFile(MainFileName) || HasAnyVaultFile(BackupFileName) || PlayerProgressBackupService.HasStoreOwnershipBackup() || LocalRecoveryMirror.HasSlotData(RecoverySlotKey); } public static bool TrySave(StoreOwnershipPayload payload) { try { Directory.CreateDirectory(VaultDirectoryPath); TryHidePath(VaultDirectoryPath); var 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); PlayerProgressBackupService.SaveStoreOwnership(payload); LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload); return true; } catch (Exception ex) { Debug.LogWarning($"[StoreOwnershipStorage] Save failed: {ex.Message}"); return false; } } public static StoreOwnershipPayload CreateDefaultPayload() { return new StoreOwnershipPayload { version = 1, lastUpdatedUtcTicks = DateTime.UtcNow.Ticks, entries = new System.Collections.Generic.List() }; } private static bool TryReadPayload(string path, out StoreOwnershipPayload payload) { payload = CreateDefaultPayload(); if (!File.Exists(path)) { return false; } try { var envelopeJson = File.ReadAllText(path, Encoding.UTF8); var envelope = JsonUtility.FromJson(envelopeJson); if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature)) { return false; } if (!TryValidateSignature(envelope.payload, envelope.signature)) { Debug.LogWarning("[StoreOwnershipStorage] Save signature mismatch. Possible tampering detected."); return false; } var encryptedBytes = Convert.FromBase64String(envelope.payload); StoreOwnershipPayload loadedPayload = null; var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants(); var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants(); for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++) { for (int d = 0; d < deviceBindingVariants.Count; d++) { try { var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d])); var payloadJson = Encoding.UTF8.GetString(plainBytes); loadedPayload = JsonUtility.FromJson(payloadJson); if (loadedPayload != null) { break; } } catch { } } } if (loadedPayload == null) { return false; } payload = loadedPayload; return true; } catch (Exception ex) { Debug.LogWarning($"[StoreOwnershipStorage] Load failed from '{path}': {ex.Message}"); return false; } } private static bool TryReadPayloadFromVariants(string fileName, out StoreOwnershipPayload payload) { payload = CreateDefaultPayload(); var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName); for (int i = 0; i < candidates.Count; i++) { if (TryReadPayload(candidates[i], out payload)) { return true; } } return false; } private static bool HasAnyVaultFile(string fileName) { var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName); for (int i = 0; i < candidates.Count; i++) { if (File.Exists(candidates[i])) { return true; } } return false; } private static string BuildEnvelopeJson(StoreOwnershipPayload payload) { payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks; var payloadJson = JsonUtility.ToJson(payload, false); var plainBytes = Encoding.UTF8.GetBytes(payloadJson); var encryptedBytes = XorTransform(plainBytes, BuildKeyBytes()); var payloadBase64 = Convert.ToBase64String(encryptedBytes); var envelope = new StoreOwnershipEnvelope { payload = payloadBase64, signature = ComputeSignature(payloadBase64) }; return JsonUtility.ToJson(envelope, false); } private static string ComputeSignature(string payloadBase64) { return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()); } private static byte[] BuildKeyBytes() { return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()); } private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding) { using (var sha = SHA256.Create()) { var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed; return sha.ComputeHash(Encoding.UTF8.GetBytes(seed)); } } private static bool TryValidateSignature(string payloadBase64, string signature) { var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants(); var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants(); for (int i = 0; i < identifierVariants.Count; i++) { for (int d = 0; d < deviceBindingVariants.Count; d++) { var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]); if (string.Equals(expectedSignature, signature, StringComparison.Ordinal)) { return true; } } } return false; } private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding) { var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding; using (var sha = SHA256.Create()) { var bytes = Encoding.UTF8.GetBytes(signText); var hash = sha.ComputeHash(bytes); return Convert.ToBase64String(hash); } } private static byte[] XorTransform(byte[] source, byte[] key) { var 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; } var attributes = File.GetAttributes(path); if ((attributes & FileAttributes.Hidden) == 0) { File.SetAttributes(path, attributes | FileAttributes.Hidden); } } catch { } } }