304 lines
9.6 KiB
C#
304 lines
9.6 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
public static class PlayerEconomyStorage
|
|
{
|
|
private const string SecretSeed = "ban_total.player_economy.v1";
|
|
private const string VaultDirectoryName = ".cache_bridge";
|
|
private const string MainFileName = ".eco.dat";
|
|
private const string BackupFileName = ".eco.bak";
|
|
private const string TempFileName = ".eco.tmp";
|
|
private const string RecoverySlotKey = "player_economy_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 PlayerEconomyPayload payload)
|
|
{
|
|
payload = CreateDefaultPayload();
|
|
|
|
if (TryReadPayloadFromVariants(MainFileName, out payload))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (TryReadPayloadFromVariants(BackupFileName, out payload))
|
|
{
|
|
TrySave(payload);
|
|
return true;
|
|
}
|
|
|
|
if (PlayerProgressBackupService.TryRestoreEconomy(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.HasEconomyBackup()
|
|
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
|
}
|
|
|
|
public static bool TrySave(PlayerEconomyPayload 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.SaveEconomy(payload);
|
|
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"[PlayerEconomyStorage] Save failed: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static PlayerEconomyPayload CreateDefaultPayload()
|
|
{
|
|
return new PlayerEconomyPayload
|
|
{
|
|
version = 1,
|
|
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
|
|
coins = 0,
|
|
material = 0
|
|
};
|
|
}
|
|
|
|
private static bool TryReadPayload(string path, out PlayerEconomyPayload payload)
|
|
{
|
|
payload = CreateDefaultPayload();
|
|
if (!File.Exists(path))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
|
|
var envelope = JsonUtility.FromJson<PlayerEconomyEnvelope>(envelopeJson);
|
|
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryValidateSignature(envelope.payload, envelope.signature))
|
|
{
|
|
Debug.LogWarning("[PlayerEconomyStorage] Save signature mismatch. Possible tampering detected.");
|
|
return false;
|
|
}
|
|
|
|
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
|
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
|
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
|
PlayerEconomyPayload loadedPayload = null;
|
|
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<PlayerEconomyPayload>(payloadJson);
|
|
if (loadedPayload != null)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
if (loadedPayload == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
payload = loadedPayload;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"[PlayerEconomyStorage] Load failed from '{path}': {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryReadPayloadFromVariants(string fileName, out PlayerEconomyPayload 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(PlayerEconomyPayload 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 PlayerEconomyEnvelope
|
|
{
|
|
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
|
|
{
|
|
}
|
|
}
|
|
}
|