修了不少东西
This commit is contained in:
@@ -313,6 +313,12 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[AllyHeroDeployLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older idol growth data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncAllMirrorFlags();
|
||||
SyncAllLegacySelectedSlotExpKeys();
|
||||
AllyHeroDeployLedgerStorage.TrySave(BuildPayload());
|
||||
|
||||
@@ -128,20 +128,24 @@ public static class AllyHeroDeployLedgerStorage
|
||||
byte[] encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
AllyHeroDeployLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,25 +212,19 @@ public static class AllyHeroDeployLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
byte[] hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -234,21 +232,25 @@ public static class AllyHeroDeployLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
string 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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -197,6 +197,12 @@ public sealed class DushMaterialLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[DushMaterialLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
DushMaterialLedgerStorage.TrySave(BuildPayload());
|
||||
SyncToPlayerData();
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class DushMaterialLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
DushMaterialLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class DushMaterialLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class DushMaterialLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -10,6 +10,8 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
|
||||
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
@@ -65,11 +67,18 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
}
|
||||
|
||||
EquipmentConsumableLedgerPayload payload;
|
||||
EquipmentConsumableLedgerStorage.TryLoad(out payload);
|
||||
loadedFromSave = EquipmentConsumableLedgerStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || EquipmentConsumableLedgerStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
initialized = true;
|
||||
SyncMirrorCounts();
|
||||
SaveNow();
|
||||
|
||||
// If an older save exists but cannot be read right now, do not overwrite it
|
||||
// with an empty default payload during startup.
|
||||
if (loadedFromSave || !hasAnyRecoverableLocalState)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
}
|
||||
|
||||
public int GetCount(EquipmentConsumableKind kind)
|
||||
@@ -126,6 +135,12 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[EquipmentConsumableLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
EquipmentConsumableLedgerStorage.TrySave(BuildPayload());
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasEquipmentConsumableBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
@@ -136,20 +137,24 @@ public static class EquipmentConsumableLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
EquipmentConsumableLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,25 +207,19 @@ public static class EquipmentConsumableLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -228,21 +227,25 @@ public static class EquipmentConsumableLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -295,6 +295,13 @@ public sealed class ExpBottleLedger : MonoBehaviour
|
||||
public void SaveNow()
|
||||
{
|
||||
InitializeIfNeededForSave();
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[ExpBottleLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
ExpBottleLedgerStorage.TrySave(BuildPayload());
|
||||
SyncToPlayerData();
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class ExpBottleLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
ExpBottleLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
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
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class ExpBottleLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class ExpBottleLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -48,11 +48,99 @@ public static class FirstRunFactoryResetService
|
||||
return;
|
||||
}
|
||||
|
||||
// 物理文件兜底探测(最后一道防线):
|
||||
// HasAnyRecoverableLocalState() 依赖各 Storage 的解密/结构判断,若因根目录别名未覆盖、
|
||||
// 或存档结构异常而误判为"无存档",仍可能走到出厂重置。这里直接扫描磁盘上是否存在
|
||||
// 任何存档物理文件(不解密、只看存在),只要有就绝不重置,宁可等待其它恢复路径。
|
||||
if (HasAnyPhysicalSaveFileOnDisk())
|
||||
{
|
||||
Debug.LogWarning("[FactoryReset] 磁盘上存在存档物理文件但未能读出," +
|
||||
"为避免误删玩家数据,跳过出厂重置并补写初始化标记。");
|
||||
EnsureInitializedMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。");
|
||||
ApplyFactoryReset();
|
||||
EnsureInitializedMarker();
|
||||
}
|
||||
|
||||
// 扫描所有可能的存档根目录(含新旧包名/公司名别名),只要磁盘上存在任意一个存档物理文件
|
||||
// 就返回 true。只判存在、不解密——因此不受 deviceUniqueIdentifier 变化影响。
|
||||
private static bool HasAnyPhysicalSaveFileOnDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) 加密存档目录 .cache_bridge:递归找任意 .dat / .bak
|
||||
string vaultDir = Path.Combine(root, ".cache_bridge");
|
||||
if (DirectoryHasFileWithAnyExtension(vaultDir, new[] { ".dat", ".bak" }))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2) 设备无关明文备份 player_progress.bbackup
|
||||
if (File.Exists(Path.Combine(root, "player_progress.bbackup")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3) 恢复镜像目录 .save_recovery:存在任意文件即视为有存档
|
||||
string recoveryDir = Path.Combine(root, ".save_recovery");
|
||||
if (DirectoryHasAnyFile(recoveryDir))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 探测失败时采取保守策略:报告"存在存档",宁可跳过重置也不误删。
|
||||
Debug.LogWarning("[FactoryReset] 物理存档探测异常,保守跳过出厂重置: " + ex.Message);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DirectoryHasFileWithAnyExtension(string directory, string[] extensions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < extensions.Length; i++)
|
||||
{
|
||||
string[] matches = Directory.GetFiles(directory, "*" + extensions[i], SearchOption.AllDirectories);
|
||||
if (matches != null && matches.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DirectoryHasAnyFile(string directory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] matches = Directory.GetFiles(directory, "*", SearchOption.AllDirectories);
|
||||
return matches != null && matches.Length > 0;
|
||||
}
|
||||
|
||||
private static bool HasInitializedMarker()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -238,6 +238,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
public void SaveNow()
|
||||
{
|
||||
InitializeIfNeededForSave();
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[PlayerEconomyLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older economy data.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerEconomyStorage.TrySave(payload);
|
||||
SyncToPlayerData();
|
||||
GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins);
|
||||
|
||||
@@ -138,21 +138,25 @@ public static class PlayerEconomyStorage
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
PlayerEconomyPayload loadedPayload = null;
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
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
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,25 +223,19 @@ public static class PlayerEconomyStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -245,21 +243,25 @@ public static class PlayerEconomyStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -191,6 +191,12 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
payload = CreateDefaultPayload();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[PlayerExperienceLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older player experience data.");
|
||||
return;
|
||||
}
|
||||
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
|
||||
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
|
||||
|
||||
@@ -78,6 +78,12 @@ public static class PlayerProgressBackupService
|
||||
return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null;
|
||||
}
|
||||
|
||||
public static bool HasEquipmentConsumableBackup()
|
||||
{
|
||||
PlayerProgressBackupBundle bundle;
|
||||
return TryLoadBundle(out bundle) && bundle != null && bundle.equipmentConsumable != null;
|
||||
}
|
||||
|
||||
public static bool HasStoreOwnershipBackup()
|
||||
{
|
||||
PlayerProgressBackupBundle bundle;
|
||||
|
||||
@@ -301,7 +301,7 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
if (grantedCoins > 0)
|
||||
{
|
||||
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
|
||||
grantedEntries.Add(gItemGet.Create("\u91d1\u5e01", null, grantedCoins, new Color32(240, 196, 74, 255)));
|
||||
grantedEntries.Add(gItemGet.Create("算力", null, grantedCoins, new Color32(240, 196, 74, 255)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
|
||||
int grantedCoins = purchaseAmount * 4;
|
||||
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
|
||||
rewardEntry = gItemGet.Create("\u91d1\u5e01", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
|
||||
rewardEntry = gItemGet.Create("算力", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,69 @@ public static class SaveIdentityUtility
|
||||
"com.DefaultCompany.EaseOut_Main_FreshNew"
|
||||
};
|
||||
|
||||
// 设备无关绑定令牌:新版存档的加密/签名统一改用它,取代 SystemInfo.deviceUniqueIdentifier。
|
||||
// 根因修复——deviceUniqueIdentifier 在安卓更新/换签名/换渠道后会变化,导致旧密钥永远解不出存档。
|
||||
// 用固定令牌后,更新或换机都不再影响解密。读取时仍会尝试真实设备 ID 变体以兼容本机旧存档。
|
||||
public const string DeviceIndependentBindingToken = "device_independent_binding_v1";
|
||||
|
||||
// 【性能】这些值在进程生命周期内恒定:SystemInfo.deviceUniqueIdentifier、Application.identifier/
|
||||
// companyName/productName 在安卓上开销较大(尤其 deviceUniqueIdentifier 涉及系统调用与哈希)。
|
||||
// 存档每次读取/验签都会取用它们,若每次重算并分配 List,会在 UI 大量刷新(如 idols 角色切换、
|
||||
// 场景切换批量重建)时造成明显卡顿。这里缓存一次,之后直接复用,行为完全不变。
|
||||
private static IReadOnlyList<string> s_cachedApplicationIdentifierVariants;
|
||||
private static IReadOnlyList<string> s_cachedDeviceBindingVariants;
|
||||
private static string s_cachedDeviceUniqueIdentifier;
|
||||
private static bool s_deviceUniqueIdentifierResolved;
|
||||
|
||||
public static string GetPrimaryApplicationIdentifier()
|
||||
{
|
||||
return StableApplicationIdentifier;
|
||||
}
|
||||
|
||||
// 写入存档时使用的设备绑定:固定为设备无关令牌。
|
||||
public static string GetPrimaryDeviceBinding()
|
||||
{
|
||||
return DeviceIndependentBindingToken;
|
||||
}
|
||||
|
||||
// 读取/验签时尝试的设备绑定变体,按命中概率排序:
|
||||
// 1) 设备无关令牌(新版写入所用,最常命中)
|
||||
// 2) 当前设备真实 deviceUniqueIdentifier(兼容本机旧版写入的设备绑定存档,首次读到后会被重写为设备无关,实现无缝迁移)
|
||||
// 结果缓存,避免每次读取都触发一次昂贵的 deviceUniqueIdentifier 查询与 List 分配。
|
||||
public static IReadOnlyList<string> GetDeviceBindingVariants()
|
||||
{
|
||||
if (s_cachedDeviceBindingVariants != null)
|
||||
{
|
||||
return s_cachedDeviceBindingVariants;
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
AddDistinct(result, DeviceIndependentBindingToken);
|
||||
AddDistinct(result, SafeDeviceUniqueIdentifier());
|
||||
s_cachedDeviceBindingVariants = result;
|
||||
return s_cachedDeviceBindingVariants;
|
||||
}
|
||||
|
||||
private static string SafeDeviceUniqueIdentifier()
|
||||
{
|
||||
if (s_deviceUniqueIdentifierResolved)
|
||||
{
|
||||
return s_cachedDeviceUniqueIdentifier;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
s_cachedDeviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
s_cachedDeviceUniqueIdentifier = null;
|
||||
}
|
||||
|
||||
s_deviceUniqueIdentifierResolved = true;
|
||||
return s_cachedDeviceUniqueIdentifier;
|
||||
}
|
||||
|
||||
public static string GetCanonicalPersistentRoot()
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
|
||||
@@ -50,6 +108,11 @@ public static class SaveIdentityUtility
|
||||
|
||||
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
|
||||
{
|
||||
if (s_cachedApplicationIdentifierVariants != null)
|
||||
{
|
||||
return s_cachedApplicationIdentifierVariants;
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
AddDistinct(result, StableApplicationIdentifier);
|
||||
AddDistinct(result, Application.identifier);
|
||||
@@ -74,7 +137,8 @@ public static class SaveIdentityUtility
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
s_cachedApplicationIdentifierVariants = result;
|
||||
return s_cachedApplicationIdentifierVariants;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
|
||||
|
||||
@@ -373,12 +373,7 @@ public static class SecureSaveVault
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(category, payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] ProtectBytes(string category, string key, byte[] plainBytes)
|
||||
@@ -394,7 +389,7 @@ public static class SecureSaveVault
|
||||
{
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
aes.GenerateIV();
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
@@ -419,37 +414,41 @@ public static class SecureSaveVault
|
||||
}
|
||||
#endif
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
for (int dv = 0; dv < deviceBindingVariants.Count; dv++)
|
||||
{
|
||||
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)
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
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;
|
||||
try
|
||||
{
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, identifierVariants[i], deviceBindingVariants[dv]);
|
||||
int ivLength = aes.BlockSize / 8;
|
||||
if (protectedBytes == null || protectedBytes.Length <= ivLength)
|
||||
{
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
if (plainBytes != null)
|
||||
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;
|
||||
try
|
||||
{
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
return true;
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
if (plainBytes != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,7 +475,7 @@ public static class SecureSaveVault
|
||||
|
||||
try
|
||||
{
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()), s_dpapiCurrentUserScope }) as byte[];
|
||||
return protectedBytes != null && protectedBytes.Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -496,18 +495,22 @@ public static class SecureSaveVault
|
||||
}
|
||||
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
|
||||
if (plainBytes != null && plainBytes.Length > 0)
|
||||
try
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i], deviceBindingVariants[d]), s_dpapiCurrentUserScope }) as byte[];
|
||||
if (plainBytes != null && plainBytes.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,20 +563,20 @@ public static class SecureSaveVault
|
||||
}
|
||||
#endif
|
||||
|
||||
private static byte[] BuildEntropy(string category, string applicationIdentifier)
|
||||
private static byte[] BuildEntropy(string category, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
|
||||
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category + "|" + key;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -635,21 +638,25 @@ public static class SecureSaveVault
|
||||
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
|
||||
{
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
|
||||
@@ -97,13 +97,13 @@ public static class StoreExpBottlePurchaseService
|
||||
|
||||
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
|
||||
{
|
||||
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
|
||||
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
|
||||
{
|
||||
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
|
||||
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public static class StoreExpBottlePurchaseService
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("[StorePurchase] 已成功购买:");
|
||||
builder.Append(itemSO != null ? itemSO.itemName : LocalizationService.Get("item.unknown.store_item", "Unknown Item"));
|
||||
builder.Append(" | 花费Coins=");
|
||||
builder.Append(" | 花费算力=");
|
||||
builder.Append(totalCost);
|
||||
builder.Append(" | 发放数量=");
|
||||
builder.Append(grantedCount);
|
||||
@@ -341,6 +341,11 @@ public static class StoreExpBottlePurchaseService
|
||||
|| currencyType == storeItemSO.CurrencyType.material;
|
||||
}
|
||||
|
||||
private static string GetInsufficientCurrencyMessage(storeItemSO.CurrencyType currencyType)
|
||||
{
|
||||
return currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
|
||||
}
|
||||
|
||||
private static bool HasEnoughCurrency(storeItemSO.CurrencyType currencyType, int amount)
|
||||
{
|
||||
switch (currencyType)
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
|
||||
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private int lastSaveFrame = -1;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
@@ -73,8 +75,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
|
||||
initialized = true;
|
||||
StoreOwnershipPayload payload;
|
||||
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
|
||||
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
|
||||
loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
LoadStoreItems();
|
||||
|
||||
@@ -208,6 +210,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[StoreOwnershipLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older store ownership data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class StoreOwnershipStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
StoreOwnershipPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class StoreOwnershipStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class StoreOwnershipStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
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)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
Reference in New Issue
Block a user