ui基本完毕,修了一大把的bug

This commit is contained in:
FloatGaming
2026-07-13 02:28:39 +08:00
parent 1e20d73e90
commit fd22501f71
958 changed files with 378289 additions and 41038 deletions
@@ -12,26 +12,50 @@ public static class AllyHeroDeployLedgerStorage
private const string BackupFileName = ".ahd.bak";
private const string TempFileName = ".ahd.tmp";
private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName);
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), 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);
var mainCandidates = SaveIdentityUtility.GetVaultFilePathVariants(MainFileName);
for (int i = 0; i < mainCandidates.Count; i++)
{
if (File.Exists(mainCandidates[i]))
{
return true;
}
}
var backupCandidates = SaveIdentityUtility.GetVaultFilePathVariants(BackupFileName);
for (int i = 0; i < backupCandidates.Count; i++)
{
if (File.Exists(backupCandidates[i]))
{
return true;
}
}
return false;
}
public static bool TryLoad(out AllyHeroDeployLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreAllyHeroDeploy(out payload))
{
TrySave(payload);
return true;
@@ -60,6 +84,7 @@ public static class AllyHeroDeployLedgerStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveAllyHeroDeploy(payload);
return true;
}
catch (Exception ex)
@@ -96,17 +121,32 @@ public static class AllyHeroDeployLedgerStorage
return false;
}
string expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
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<AllyHeroDeployLedgerPayload>(payloadJson);
AllyHeroDeployLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
string payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -122,6 +162,21 @@ public static class AllyHeroDeployLedgerStorage
}
}
private static bool TryReadPayloadFromVariants(string fileName, out AllyHeroDeployLedgerPayload 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 string BuildEnvelopeJson(AllyHeroDeployLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -141,7 +196,7 @@ public static class AllyHeroDeployLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
string signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (SHA256 sha = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(signText);
@@ -151,14 +206,45 @@ public static class AllyHeroDeployLedgerStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (SHA256 sha = SHA256.Create())
{
string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + 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[] XorTransform(byte[] source, byte[] key)
{
byte[] result = new byte[source.Length];
@@ -43,9 +43,13 @@ public static class DlcManifestService
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, ManifestFolderName));
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName));
#if !UNITY_ANDROID
// 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义;
// 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。
string playerRoot = GetPlayerRootDirectory();
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, ManifestFolderName));
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName));
#endif
return result.ToArray();
}
@@ -91,9 +91,13 @@ public static class DlcPackageArchiveService
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName, PackageFolderName));
AppendIfValid(result, Path.Combine(Application.persistentDataPath, RuntimeFolderName));
#if !UNITY_ANDROID
// 项目外(可执行文件同级目录)的 DLC 扫描仅在 PC 端有意义;
// 安卓无此目录概念,Application.dataPath 指向 APK,跳过以避免无效/异常路径。
string playerRoot = GetPlayerRootDirectory();
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName, PackageFolderName));
AppendIfValid(result, Path.Combine(playerRoot, RuntimeFolderName));
#endif
return result.ToArray();
}
@@ -14,7 +14,7 @@ public static class DushMaterialLedgerStorage
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
private static string MainFilePath
@@ -36,12 +36,18 @@ public static class DushMaterialLedgerStorage
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreDushMaterial(out payload))
{
TrySave(payload);
return true;
@@ -70,6 +76,7 @@ public static class DushMaterialLedgerStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveDushMaterial(payload);
return true;
}
catch (Exception ex)
@@ -106,17 +113,32 @@ public static class DushMaterialLedgerStorage
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[DushMaterialLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
DushMaterialLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -132,6 +154,21 @@ public static class DushMaterialLedgerStorage
}
}
private static bool TryReadPayloadFromVariants(string fileName, out DushMaterialLedgerPayload 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 string BuildEnvelopeJson(DushMaterialLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -151,7 +188,7 @@ public static class DushMaterialLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -161,14 +198,45 @@ public static class DushMaterialLedgerStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
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];
@@ -12,7 +12,7 @@ public static class EquipmentConsumableLedgerStorage
private const string BackupFileName = ".eqc.bak";
private const string TempFileName = ".eqc.tmp";
private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName);
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), 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);
@@ -21,12 +21,18 @@ public static class EquipmentConsumableLedgerStorage
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreEquipmentConsumable(out payload))
{
TrySave(payload);
return true;
@@ -55,6 +61,7 @@ public static class EquipmentConsumableLedgerStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveEquipmentConsumable(payload);
return true;
}
catch (Exception ex)
@@ -91,17 +98,32 @@ public static class EquipmentConsumableLedgerStorage
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[EquipmentConsumableLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
EquipmentConsumableLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -117,6 +139,21 @@ public static class EquipmentConsumableLedgerStorage
}
}
private static bool TryReadPayloadFromVariants(string fileName, out EquipmentConsumableLedgerPayload 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 string BuildEnvelopeJson(EquipmentConsumableLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -136,7 +173,7 @@ public static class EquipmentConsumableLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -146,14 +183,45 @@ public static class EquipmentConsumableLedgerStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
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];
@@ -14,7 +14,7 @@ public static class ExpBottleLedgerStorage
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
private static string MainFilePath
@@ -36,12 +36,18 @@ public static class ExpBottleLedgerStorage
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreExpBottle(out payload))
{
TrySave(payload);
return true;
@@ -70,6 +76,7 @@ public static class ExpBottleLedgerStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveExpBottle(payload);
return true;
}
catch (Exception ex)
@@ -106,17 +113,32 @@ public static class ExpBottleLedgerStorage
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[ExpBottleLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
ExpBottleLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -132,6 +154,21 @@ public static class ExpBottleLedgerStorage
}
}
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 string BuildEnvelopeJson(ExpBottleLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -151,7 +188,7 @@ public static class ExpBottleLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -161,14 +198,45 @@ public static class ExpBottleLedgerStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
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];
@@ -14,7 +14,7 @@ public static class PlayerEconomyStorage
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
private static string MainFilePath
@@ -36,12 +36,18 @@ public static class PlayerEconomyStorage
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreEconomy(out payload))
{
TrySave(payload);
return true;
@@ -70,6 +76,7 @@ public static class PlayerEconomyStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveEconomy(payload);
return true;
}
catch (Exception ex)
@@ -107,17 +114,32 @@ public static class PlayerEconomyStorage
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[PlayerEconomyStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
PlayerEconomyPayload loadedPayload = null;
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -133,6 +155,21 @@ public static class PlayerEconomyStorage
}
}
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 string BuildEnvelopeJson(PlayerEconomyPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -152,7 +189,7 @@ public static class PlayerEconomyStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -162,14 +199,45 @@ public static class PlayerEconomyStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
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];
@@ -78,7 +78,17 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
loadedFromSave = SecureSaveVault.TryLoadJson("player_experience", "runtime", out payload, GetLegacySavePath());
if (payload == null)
{
payload = CreateDefaultPayload();
int backupExperience;
if (PlayerProgressBackupService.TryRestorePlayerExperience(out backupExperience))
{
payload = CreateDefaultPayload();
payload.playerExp = Mathf.Max(0, backupExperience);
loadedFromSave = true;
}
else
{
payload = CreateDefaultPayload();
}
}
initialized = true;
@@ -166,6 +176,7 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
SyncToPlayerData();
NotifyExperienceChanged();
}
@@ -0,0 +1,555 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
[Serializable]
public class PlayerProgressBackupBundle
{
public int version = 1;
public long savedUtcTicks;
public PlayerEconomyPayload economy;
public int playerExperience;
public float bestOverallRks;
public PlayerSkillSaveData playerSkill;
public AllyHeroDeployLedgerPayload allyHeroDeploy;
public ExpBottleLedgerPayload expBottle;
public DushMaterialLedgerPayload dushMaterial;
public EquipmentConsumableLedgerPayload equipmentConsumable;
public StoreOwnershipPayload storeOwnership;
}
public static class PlayerProgressBackupService
{
private const string BackupFileName = "player_progress.bbackup";
private static PlayerProgressBackupBundle s_cachedBundle;
private static bool s_cacheLoaded;
private static bool s_isWriting;
public static bool TryRestoreEconomy(out PlayerEconomyPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.economy == null)
{
return false;
}
payload = CloneEconomy(bundle.economy);
return payload != null;
}
public static void SaveEconomy(PlayerEconomyPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.economy = CloneEconomy(payload));
}
public static bool TryRestorePlayerExperience(out int experience)
{
experience = 0;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null)
{
return false;
}
if (bundle.playerExperience <= 0)
{
return false;
}
experience = Mathf.Max(0, bundle.playerExperience);
return true;
}
public static void SavePlayerExperience(int experience)
{
UpdateBundle(bundle => bundle.playerExperience = Mathf.Max(0, experience));
}
public static bool TryRestoreRks(out float rks)
{
rks = 0f;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.bestOverallRks <= 0f)
{
return false;
}
rks = Mathf.Max(0f, bundle.bestOverallRks);
return true;
}
public static void SaveRks(float rks)
{
UpdateBundle(bundle => bundle.bestOverallRks = Mathf.Max(0f, rks));
}
public static bool TryRestorePlayerSkill(out PlayerSkillSaveData saveData)
{
saveData = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.playerSkill == null)
{
return false;
}
saveData = ClonePlayerSkill(bundle.playerSkill);
return saveData != null;
}
public static void SavePlayerSkill(PlayerSkillSaveData saveData)
{
if (saveData == null)
{
return;
}
UpdateBundle(bundle => bundle.playerSkill = ClonePlayerSkill(saveData));
}
public static bool TryRestoreAllyHeroDeploy(out AllyHeroDeployLedgerPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.allyHeroDeploy == null)
{
return false;
}
payload = CloneAllyHeroDeploy(bundle.allyHeroDeploy);
return payload != null;
}
public static void SaveAllyHeroDeploy(AllyHeroDeployLedgerPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.allyHeroDeploy = CloneAllyHeroDeploy(payload));
}
public static bool TryRestoreExpBottle(out ExpBottleLedgerPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.expBottle == null)
{
return false;
}
payload = CloneExpBottle(bundle.expBottle);
return payload != null;
}
public static void SaveExpBottle(ExpBottleLedgerPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.expBottle = CloneExpBottle(payload));
}
public static bool TryRestoreDushMaterial(out DushMaterialLedgerPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.dushMaterial == null)
{
return false;
}
payload = CloneDushMaterial(bundle.dushMaterial);
return payload != null;
}
public static void SaveDushMaterial(DushMaterialLedgerPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.dushMaterial = CloneDushMaterial(payload));
}
public static bool TryRestoreEquipmentConsumable(out EquipmentConsumableLedgerPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.equipmentConsumable == null)
{
return false;
}
payload = CloneEquipmentConsumable(bundle.equipmentConsumable);
return payload != null;
}
public static void SaveEquipmentConsumable(EquipmentConsumableLedgerPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.equipmentConsumable = CloneEquipmentConsumable(payload));
}
public static bool TryRestoreStoreOwnership(out StoreOwnershipPayload payload)
{
payload = null;
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null || bundle.storeOwnership == null)
{
return false;
}
payload = CloneStoreOwnership(bundle.storeOwnership);
return payload != null;
}
public static void SaveStoreOwnership(StoreOwnershipPayload payload)
{
if (payload == null)
{
return;
}
UpdateBundle(bundle => bundle.storeOwnership = CloneStoreOwnership(payload));
}
private static void UpdateBundle(Action<PlayerProgressBackupBundle> mutator)
{
if (mutator == null || s_isWriting)
{
return;
}
PlayerProgressBackupBundle bundle;
if (!TryLoadBundle(out bundle) || bundle == null)
{
bundle = CreateDefaultBundle();
}
mutator(bundle);
bundle.savedUtcTicks = DateTime.UtcNow.Ticks;
SaveBundle(bundle);
}
private static bool TryLoadBundle(out PlayerProgressBackupBundle bundle)
{
if (s_cacheLoaded)
{
bundle = s_cachedBundle;
return bundle != null;
}
s_cacheLoaded = true;
s_cachedBundle = null;
IReadOnlyList<string> candidates = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < candidates.Count; i++)
{
string root = candidates[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
string path = Path.Combine(root, BackupFileName);
if (!File.Exists(path))
{
continue;
}
try
{
string json = File.ReadAllText(path, Encoding.UTF8);
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
PlayerProgressBackupBundle loaded = JsonUtility.FromJson<PlayerProgressBackupBundle>(json);
if (loaded == null)
{
continue;
}
s_cachedBundle = loaded;
break;
}
catch (Exception ex)
{
Debug.LogWarning("[PlayerProgressBackup] Failed to read backup '" + path + "': " + ex.Message);
}
}
bundle = s_cachedBundle;
return bundle != null;
}
private static void SaveBundle(PlayerProgressBackupBundle bundle)
{
if (bundle == null)
{
return;
}
string root = SaveIdentityUtility.GetCanonicalPersistentRoot();
string path = Path.Combine(root, BackupFileName);
try
{
s_isWriting = true;
Directory.CreateDirectory(root);
string json = JsonUtility.ToJson(bundle, false);
File.WriteAllText(path, json, Encoding.UTF8);
s_cachedBundle = bundle;
s_cacheLoaded = true;
}
catch (Exception ex)
{
Debug.LogWarning("[PlayerProgressBackup] Failed to save backup '" + path + "': " + ex.Message);
}
finally
{
s_isWriting = false;
}
}
private static PlayerProgressBackupBundle CreateDefaultBundle()
{
return new PlayerProgressBackupBundle
{
version = 1,
savedUtcTicks = DateTime.UtcNow.Ticks
};
}
private static PlayerEconomyPayload CloneEconomy(PlayerEconomyPayload source)
{
if (source == null)
{
return null;
}
return new PlayerEconomyPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
coins = source.coins,
material = source.material
};
}
private static PlayerSkillSaveData ClonePlayerSkill(PlayerSkillSaveData source)
{
if (source == null)
{
return null;
}
return new PlayerSkillSaveData
{
selectedSkillIndex = source.selectedSkillIndex,
postMatchRewardCounter = source.postMatchRewardCounter,
skillSwitchCooldownRemainingMatches = source.skillSwitchCooldownRemainingMatches
};
}
private static AllyHeroDeployLedgerPayload CloneAllyHeroDeploy(AllyHeroDeployLedgerPayload source)
{
if (source == null)
{
return null;
}
var clone = new AllyHeroDeployLedgerPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
entries = new List<AllyHeroDeployEntry>()
};
if (source.entries != null)
{
for (int i = 0; i < source.entries.Count; i++)
{
AllyHeroDeployEntry entry = source.entries[i];
if (entry == null)
{
continue;
}
clone.entries.Add(new AllyHeroDeployEntry
{
heroId = entry.heroId,
currentExp = entry.currentExp,
unlockedTierIndex = entry.unlockedTierIndex,
levelLock = entry.levelLock,
autoBreakthroughEnabled = entry.autoBreakthroughEnabled,
deployCount = entry.deployCount,
finishCount = entry.finishCount,
mvpCount = entry.mvpCount,
joinDateUtcTicks = entry.joinDateUtcTicks
});
}
}
return clone;
}
private static ExpBottleLedgerPayload CloneExpBottle(ExpBottleLedgerPayload source)
{
if (source == null)
{
return null;
}
var clone = new ExpBottleLedgerPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
entries = new List<ExpBottleEntry>()
};
if (source.entries != null)
{
for (int i = 0; i < source.entries.Count; i++)
{
ExpBottleEntry entry = source.entries[i];
if (entry == null)
{
continue;
}
clone.entries.Add(new ExpBottleEntry
{
key = entry.key,
count = entry.count
});
}
}
return clone;
}
private static DushMaterialLedgerPayload CloneDushMaterial(DushMaterialLedgerPayload source)
{
if (source == null)
{
return null;
}
var clone = new DushMaterialLedgerPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
entries = new List<DushMaterialEntry>()
};
if (source.entries != null)
{
for (int i = 0; i < source.entries.Count; i++)
{
DushMaterialEntry entry = source.entries[i];
if (entry == null)
{
continue;
}
clone.entries.Add(new DushMaterialEntry
{
key = entry.key,
count = entry.count
});
}
}
return clone;
}
private static EquipmentConsumableLedgerPayload CloneEquipmentConsumable(EquipmentConsumableLedgerPayload source)
{
if (source == null)
{
return null;
}
var clone = new EquipmentConsumableLedgerPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
entries = new List<EquipmentConsumableEntry>()
};
if (source.entries != null)
{
for (int i = 0; i < source.entries.Count; i++)
{
EquipmentConsumableEntry entry = source.entries[i];
if (entry == null)
{
continue;
}
clone.entries.Add(new EquipmentConsumableEntry
{
key = entry.key,
count = entry.count
});
}
}
return clone;
}
private static StoreOwnershipPayload CloneStoreOwnership(StoreOwnershipPayload source)
{
if (source == null)
{
return null;
}
var clone = new StoreOwnershipPayload
{
version = source.version,
lastUpdatedUtcTicks = source.lastUpdatedUtcTicks,
entries = new List<StoreOwnershipEntry>()
};
if (source.entries != null)
{
for (int i = 0; i < source.entries.Count; i++)
{
StoreOwnershipEntry entry = source.entries[i];
if (entry == null)
{
continue;
}
clone.entries.Add(new StoreOwnershipEntry
{
storeItemId = entry.storeItemId,
owned = entry.owned,
unlockedStorySonIds = entry.unlockedStorySonIds != null
? new List<int>(entry.unlockedStorySonIds)
: new List<int>()
});
}
}
return clone;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c620f726faf81ae4aa12a68cd611cb81
@@ -36,6 +36,10 @@ public static class PlayerRksService
{
bestOverallRks = Mathf.Max(0f, saveData.bestOverallRks);
}
else if (PlayerProgressBackupService.TryRestoreRks(out float backupRks))
{
bestOverallRks = Mathf.Max(0f, backupRks);
}
else if (player != null)
{
bestOverallRks = Mathf.Max(0f, player.URankingScore);
@@ -66,6 +70,7 @@ public static class PlayerRksService
{
bestOverallRks = calculated;
SecureSaveVault.SaveJson(SaveCategory, SaveKey, new PlayerRksSaveData { bestOverallRks = bestOverallRks });
PlayerProgressBackupService.SaveRks(bestOverallRks);
SyncPlayerSo(player);
OnRksChanged?.Invoke(bestOverallRks);
}
@@ -111,6 +111,11 @@ public sealed class PlayerSkillService : MonoBehaviour
public static void NotifySettlementCompleted(int idolScore)
{
if (GameConfig.autoPlayEnabled)
{
return;
}
EnsureInstance().HandleSettlementCompletedInternal(idolScore);
}
@@ -185,7 +190,10 @@ public sealed class PlayerSkillService : MonoBehaviour
initialized = true;
if (!SecureSaveVault.TryLoadJson(SaveCategory, SaveKey, out saveData) || saveData == null)
{
saveData = new PlayerSkillSaveData();
if (!PlayerProgressBackupService.TryRestorePlayerSkill(out saveData) || saveData == null)
{
saveData = new PlayerSkillSaveData();
}
}
ResolveSkillAssetIfNeeded();
@@ -692,5 +700,6 @@ public sealed class PlayerSkillService : MonoBehaviour
}
SecureSaveVault.SaveJson(SaveCategory, SaveKey, saveData);
PlayerProgressBackupService.SavePlayerSkill(saveData);
}
}
@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public static class SaveIdentityUtility
{
public const string StableCompanyName = "FloatGamingStudio";
public const string StableProductName = "Bansonic";
public const string StableApplicationIdentifier = "com.FloatGamingStudio.Bansonic";
private static readonly string[] LegacyProductAliases =
{
"Bansonic",
"ban_total",
"EaseOut_Main_FreshNew"
};
private static readonly string[] LegacyCompanyAliases =
{
"FloatGamingStudio",
"DefaultCompany"
};
private static readonly string[] LegacyIdentifierAliases =
{
"com.DefaultCompany.Bansonic",
"com.FloatGamingStudio.ban_total",
"com.DefaultCompany.ban_total",
"com.FloatGamingStudio.EaseOut_Main_FreshNew",
"com.DefaultCompany.EaseOut_Main_FreshNew"
};
public static string GetPrimaryApplicationIdentifier()
{
return StableApplicationIdentifier;
}
public static string GetCanonicalPersistentRoot()
{
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow"));
return Path.Combine(localLow, StableCompanyName, StableProductName);
}
return Application.persistentDataPath;
}
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
{
var result = new List<string>();
AddDistinct(result, StableApplicationIdentifier);
AddDistinct(result, Application.identifier);
string currentCompany = SafeTrim(Application.companyName);
string currentProduct = SafeTrim(Application.productName);
if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct))
{
AddDistinct(result, $"com.{currentCompany}.{currentProduct}");
}
for (int i = 0; i < LegacyIdentifierAliases.Length; i++)
{
AddDistinct(result, LegacyIdentifierAliases[i]);
}
for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++)
{
for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++)
{
AddDistinct(result, $"com.{LegacyCompanyAliases[companyIndex]}.{LegacyProductAliases[productIndex]}");
}
}
return result;
}
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
{
var result = new List<string>();
AddDistinct(result, GetCanonicalPersistentRoot());
AddDistinct(result, Application.persistentDataPath);
string extraDirectory = string.IsNullOrEmpty(extraLegacyPath) ? null : Path.GetDirectoryName(extraLegacyPath);
AddDistinct(result, extraDirectory);
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow"));
string currentCompany = SafeTrim(Application.companyName);
string currentProduct = SafeTrim(Application.productName);
if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct))
{
AddDistinct(result, Path.Combine(localLow, currentCompany, currentProduct));
}
for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++)
{
for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++)
{
AddDistinct(result, Path.Combine(localLow, LegacyCompanyAliases[companyIndex], LegacyProductAliases[productIndex]));
}
}
}
return result;
}
public static IReadOnlyList<string> GetVaultFilePathVariants(string fileName)
{
var result = new List<string>();
if (string.IsNullOrWhiteSpace(fileName))
{
return result;
}
IReadOnlyList<string> roots = GetPersistentRootVariants();
for (int i = 0; i < roots.Count; i++)
{
string root = roots[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
AddDistinct(result, Path.Combine(root, ".cache_bridge", fileName));
}
return result;
}
private static void AddDistinct(List<string> target, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
for (int i = 0; i < target.Count; i++)
{
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
{
return;
}
}
target.Add(value);
}
private static string SafeTrim(string value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: acd513b6738225b46ab31a779eb308fa
+147 -46
View File
@@ -27,7 +27,7 @@ public static class SecureSaveVault
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
public static bool SaveJson<T>(string category, string key, T data, string legacyPlainPath = null)
@@ -121,20 +121,11 @@ public static class SecureSaveVault
return false;
}
string mainPath = GetFilePath(category, key, ".dat");
string backupPath = GetFilePath(category, key, ".bak");
if (TryReadEncryptedFile(category, key, mainPath, out json))
if (TryLoadFromCurrentOrLegacyEncryptedFiles(category, key, legacyPlainPath, 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
@@ -262,8 +253,7 @@ public static class SecureSaveVault
return false;
}
string expectedSignature = ComputeSignature(category, envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(category, envelope.payload, envelope.signature))
{
Debug.LogWarning($"[SecureSaveVault] Signature mismatch ({category}/{key}). Possible tampering detected.");
return false;
@@ -329,7 +319,7 @@ public static class SecureSaveVault
private static string ComputeSignature(string category, string payloadBase64)
{
string signText = payloadBase64 + "|" + category + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
@@ -350,7 +340,7 @@ public static class SecureSaveVault
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key);
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
{
@@ -374,28 +364,44 @@ public static class SecureSaveVault
return plainBytes != null;
}
#endif
using (var aes = Aes.Create())
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
using (var aes = Aes.Create())
{
return false;
}
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, identifierVariants[i]);
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;
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;
try
{
using (var decryptor = aes.CreateDecryptor())
{
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
if (plainBytes != null)
{
return true;
}
}
}
catch
{
}
}
}
plainBytes = null;
return false;
}
catch (Exception ex)
{
@@ -416,7 +422,7 @@ public static class SecureSaveVault
try
{
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category), s_dpapiCurrentUserScope }) as byte[];
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
return protectedBytes != null && protectedBytes.Length > 0;
}
catch (Exception ex)
@@ -435,16 +441,24 @@ public static class SecureSaveVault
return false;
}
try
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
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;
try
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
if (plainBytes != null && plainBytes.Length > 0)
{
return true;
}
}
catch
{
}
}
plainBytes = null;
return false;
}
private static bool EnsureDpapi()
@@ -492,29 +506,116 @@ public static class SecureSaveVault
}
#endif
private static byte[] BuildEntropy(string category)
private static byte[] BuildEntropy(string category, string applicationIdentifier)
{
string seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
using (var sha = SHA256.Create())
{
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] BuildAesKey(string category, string key)
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
{
return BuildEntropy(category);
using (var sha = SHA256.Create())
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static string ShortHash(string value)
{
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + Application.identifier + "|" + SecretSeed));
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed));
return BitConverter.ToString(hash, 0, 12).Replace("-", string.Empty).ToLowerInvariant();
}
}
private static string ShortHash(string value, string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value + "|" + applicationIdentifier + "|" + SecretSeed));
return BitConverter.ToString(hash, 0, 12).Replace("-", string.Empty).ToLowerInvariant();
}
}
private static bool TryLoadFromCurrentOrLegacyEncryptedFiles(string category, string key, string legacyPlainPath, out string json)
{
json = null;
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants(legacyPlainPath);
for (int i = 0; i < roots.Count; i++)
{
string root = roots[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int variantIndex = 0; variantIndex < identifierVariants.Count; variantIndex++)
{
string applicationIdentifier = identifierVariants[variantIndex];
string mainPath = GetFilePathForRoot(root, category, key, ".dat", applicationIdentifier);
if (TryReadEncryptedFile(category, key, mainPath, out json))
{
SaveRawJson(category, key, json, legacyPlainPath);
return true;
}
string backupPath = GetFilePathForRoot(root, category, key, ".bak", applicationIdentifier);
if (TryReadEncryptedFile(category, key, backupPath, out json))
{
SaveRawJson(category, key, json, legacyPlainPath);
return true;
}
}
}
return false;
}
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
{
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
{
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
return Convert.ToBase64String(hash);
}
}
private static string GetCategoryDirectoryForRoot(string rootPath, string category, string applicationIdentifier)
{
string safeCategory = ShortHash("cat|" + category, applicationIdentifier);
return Path.Combine(rootPath, VaultDirectoryName, "." + safeCategory);
}
private static string GetFilePathForRoot(string rootPath, string category, string key, string extension, string applicationIdentifier)
{
string categoryDirectory = GetCategoryDirectoryForRoot(rootPath, category, applicationIdentifier);
string safeKey = ShortHash("key|" + key, applicationIdentifier);
return Path.Combine(categoryDirectory, "." + safeKey + extension);
}
private static void DeleteLegacyPlainFile(string legacyPlainPath)
{
if (string.IsNullOrEmpty(legacyPlainPath) || !File.Exists(legacyPlainPath))
@@ -14,7 +14,7 @@ public static class StoreOwnershipStorage
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
private static string MainFilePath
@@ -36,12 +36,18 @@ public static class StoreOwnershipStorage
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
if (TryReadPayloadFromVariants(MainFileName, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
if (TryReadPayloadFromVariants(BackupFileName, out payload))
{
TrySave(payload);
return true;
}
if (PlayerProgressBackupService.TryRestoreStoreOwnership(out payload))
{
TrySave(payload);
return true;
@@ -70,6 +76,7 @@ public static class StoreOwnershipStorage
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveStoreOwnership(payload);
return true;
}
catch (Exception ex)
@@ -106,17 +113,32 @@ public static class StoreOwnershipStorage
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
if (!TryValidateSignature(envelope.payload, envelope.signature))
{
Debug.LogWarning("[StoreOwnershipStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
StoreOwnershipPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
}
}
if (loadedPayload == null)
{
return false;
@@ -132,6 +154,21 @@ public static class StoreOwnershipStorage
}
}
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 string BuildEnvelopeJson(StoreOwnershipPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -151,7 +188,7 @@ public static class StoreOwnershipStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -161,14 +198,45 @@ public static class StoreOwnershipStorage
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
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];