Files
bansonic_beta_main/Assets/scripts/_runtimecache/ExpBottleLedgerStorage.cs
T
2026-07-16 23:04:59 +08:00

303 lines
9.6 KiB
C#

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class ExpBottleLedgerStorage
{
private const string SecretSeed = "ban_total.exp_bottle_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".xpv.dat";
private const string BackupFileName = ".xpv.bak";
private const string TempFileName = ".xpv.tmp";
private const string RecoverySlotKey = "exp_bottle_ledger_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 ExpBottleLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreExpBottle(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.HasExpBottleBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(ExpBottleLedgerPayload 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.SaveExpBottle(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[ExpBottleLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static ExpBottleLedgerPayload CreateDefaultPayload()
{
return new ExpBottleLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<ExpBottleEntry>()
};
}
private static bool TryReadPayload(string path, out ExpBottleLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<ExpBottleLedgerEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[ExpBottleLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
ExpBottleLedgerPayload 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<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
}
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[ExpBottleLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static bool TryReadPayloadFromVariants(string fileName, out ExpBottleLedgerPayload 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(ExpBottleLedgerPayload 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 ExpBottleLedgerEnvelope
{
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
{
}
}
}