r231 vs r232
......
434434
using UnityEngine;
435435
using System.Diagnostics;
436436
using System.Runtime.CompilerServices;
437
using System.Collections.Generic;
437438
438439
[Serializable]
439440
public class PlayerData
......
444445
445446
public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 }
446447
448
public struct SecureMetrics
449
{
450
public long TotalSaveTimeMs;
451
public long TotalLoadTimeMs;
452
public int SaveCount;
453
public int LoadCount;
454
public string SessionNonceStatus;
455
public bool IsTamperingForced;
456
public bool IsEnvironmentTampered;
457
}
458
447459
public class SecureManager : MonoBehaviour
448460
{
449461
public static SecureManager Instance { get; private set; }
450462
463
[Header("보안 강화 제어")]
451464
public bool EnableHardening = true;
452465
public bool EnableAntiDebugChecks = true;
453466
public bool EnableRootDetection = true;
454467
public bool EnableDummyMix = true;
455468
public bool EnableFileNameObfuscation = true;
456469
public bool EnableRotation = true;
470
471
[Header("개발자 진단 및 로깅")]
457472
public bool EnableDeveloperLogging = true;
458473
public DevLogLevel DeveloperLogLevel = DevLogLevel.Info;
474
[Tooltip("안티-치트 로직 테스트를 위해 강제로 Tampered 상태를 주입합니다.")]
475
public bool ForceTamperingForTest = false;
476
477
[Header("데이터 및 I/O 설정")]
459478
public bool EnableTimeValidation = true;
460479
public long MaxAllowedTimeJumpSeconds = 3600 * 6;
480
[Tooltip("비동기 I/O 사용 여부 (권장)")]
461481
public bool UseAsyncIO = true;
462482
public byte CurrentVersion = 1;
483
[Tooltip("어셈블리 해시 검증을 위한 16진수 문자열. 비어 있으면 검증하지 않습니다.")]
463484
public string ExpectedAssemblyHashHex = "";
464485
465486
private const int SaltLenDefault = 16;
......
479500
480501
private string filePath;
481502
private byte[] sessionNonce;
503
private bool nonceLoadedSecurely = false;
504
482505
private Stopwatch perfSave = new Stopwatch();
483506
private Stopwatch perfLoad = new Stopwatch();
484507
private long totalSaveMs = 0;
......
486509
private int saveCount = 0;
487510
private int loadCount = 0;
488511
private string devLogFilePath;
489
512
490513
private static class Obf
491514
{
492515
public static string Decode(byte[] cipher, byte key)
......
508531
}
509532
Instance = this;
510533
DontDestroyOnLoad(gameObject);
534
511535
sessionNonce = LoadOrCreateSessionNonce();
536
PrintSecurityStatus();
537
512538
filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
513539
devLogFilePath = Path.Combine(Application.persistentDataPath, "securemanager_devlog.txt");
540
514541
if (!string.IsNullOrEmpty(ExpectedAssemblyHashHex))
515542
{
516543
try
517544
{
518545
string calc = CalculateAssemblyHashPartial(out bool ok);
519
if (!ok)
520
{
521
DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning);
522
}
546
if (!ok) DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning);
523547
else
524548
{
525
string exp = ExpectedAssemblyHashHex.ToLowerInvariant();
526
string calcLow = calc.ToLowerInvariant();
527
if (exp.Length >= 8 && calcLow.Length >= 8)
528
DevLog($"Assembly hash compare: Expected={exp.Substring(0, Math.Min(8, exp.Length))}..., Calculated={calcLow.Substring(0, 8)}...", DevLogLevel.Info);
529
if (!VerifyAssemblyHash(ExpectedAssemblyHashHex))
530
{
531
DevLog($"Assembly hash mismatch: Expected={ExpectedAssemblyHashHex.Substring(0, Math.Min(8, ExpectedAssemblyHashHex.Length))}..., Calculated={calc.Substring(0, Math.Min(8, calc.Length))}...", DevLogLevel.Warning);
532
}
549
if (!VerifyAssemblyHash(ExpectedAssemblyHashHex)) DevLog($"Assembly hash mismatch.", DevLogLevel.Critical);
550
else DevLog($"Assembly hash verified OK.", DevLogLevel.Info);
533551
}
534552
}
535
catch (Exception ex)
536
{
537
DevLog("Assembly verify exception: " + ex.ToString(), DevLogLevel.Warning);
538
}
553
catch (Exception ex) { DevLog("Assembly verify failed due to exception.", DevLogLevel.Error, ex); }
539554
}
540
DevLog("Final data path: " + filePath, DevLogLevel.Info);
555
DevLog("최종 데이터 경로: " + filePath, DevLogLevel.Info);
541556
DevLogEnvironmentSnapshot();
542557
}
543558
......
545560
{
546561
DevLog($"Platform={Application.platform}, OS={SystemInfo.operatingSystem}, Device={SystemInfo.deviceModel}, CPU={SystemInfo.processorType}, Memory={SystemInfo.systemMemorySize}MB", DevLogLevel.Info);
547562
}
563
564
public void PrintSecurityStatus()
565
{
566
string status = nonceLoadedSecurely
567
? "SUCCESS: 세션 논스 안전하게 로드됨 (ProtectedData/Native)."
568
: "WARNING: 세션 논스가 OS 보호 없이 저장됨 (초기 생성 또는 폴백 사용).";
569
570
DevLog($"--- 보안 상태 보고서 ---", DevLogLevel.Info);
571
DevLog($"[논스 상태] {status}", nonceLoadedSecurely ? DevLogLevel.Info : DevLogLevel.Warning);
572
DevLog($"[강화 설정] 활성: {EnableHardening}, 디버그 방어: {EnableAntiDebugChecks}, 루팅 감지: {EnableRootDetection}", DevLogLevel.Info);
573
DevLog($"[변조 강제] 강제 테스트 상태: {ForceTamperingForTest}", ForceTamperingForTest ? DevLogLevel.Warning : DevLogLevel.Info);
574
DevLog($"----------------------------", DevLogLevel.Info);
575
}
548576
549577
private byte[] LoadOrCreateSessionNonce()
550578
{
551579
const string key = "SecureManager_SessionNonce_v4";
580
byte[] newNonce = GenerateRandomBytes(16);
581
552582
try
553583
{
554584
if (PlayerPrefs.HasKey(key))
......
560590
{
561591
byte[] enc = Convert.FromBase64String(b64);
562592
byte[] dec = TryProtectedUnprotect(enc);
563
if (dec != null && dec.Length > 0) return dec;
564
return enc;
593
594
if (dec != null && dec.Length > 0)
595
{
596
nonceLoadedSecurely = true;
597
DevLog("Session Nonce successfully loaded and unprotected. [Recovery: Success]", DevLogLevel.Trace);
598
return dec;
599
}
600
601
DevLog("ProtectedData Unprotect returned null or failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning);
602
throw new CryptographicException("DPAPI/OS protection failed.");
565603
}
566
catch { }
604
catch (FormatException ex) { DevLog("Failed to decode session nonce from Base64. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
605
catch (CryptographicException ex) { DevLog("ProtectedData unprotect failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
606
catch (Exception ex) { DevLog("Unexpected error during Session Nonce loading/unprotecting.", DevLogLevel.Error, ex); }
567607
}
568608
}
569609
}
570
catch (Exception ex)
571
{
572
DevLog("Load session nonce failed: " + ex.Message, DevLogLevel.Warning);
573
}
574
byte[] nonce = GenerateRandomBytes(16);
610
catch (Exception ex) { DevLog("Load session nonce failed (PlayerPrefs read error). [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
611
612
nonceLoadedSecurely = false;
613
DevLog("Creating new Session Nonce (Fallback/Initial).", DevLogLevel.Info);
614
575615
try
576616
{
577
byte[] protectedBytes = TryProtectedProtect(nonce);
578
string toStore = protectedBytes != null ? Convert.ToBase64String(protectedBytes) : Convert.ToBase64String(nonce);
617
byte[] protectedBytes = TryProtectedProtect(newNonce);
618
string toStore = protectedBytes != null
619
? Convert.ToBase64String(protectedBytes)
620
: Convert.ToBase64String(newNonce);
621
579622
PlayerPrefs.SetString(key, toStore);
580623
PlayerPrefs.Save();
624
if (protectedBytes == null) DevLog("New nonce saved without OS protection. [Recovery: Fallback to PlayerPrefs]", DevLogLevel.Warning);
625
else DevLog("New nonce saved with OS protection.", DevLogLevel.Trace);
581626
}
582
catch (Exception ex)
583
{
584
DevLog("Save session nonce failed: " + ex.Message, DevLogLevel.Warning);
585
}
586
return nonce;
627
catch (Exception ex) { DevLog("Save new session nonce failed. [Defense: Use in-memory nonce]", DevLogLevel.Critical, ex); }
628
return newNonce;
587629
}
588630
589631
[MethodImpl(MethodImplOptions.NoInlining)]
......
671713
byte[] secret = GetSecretBytes();
672714
string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
673715
Array.Clear(secret, 0, secret.Length);
674
using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
716
try
675717
{
676
byte[] km = kdf.GetBytes(KeyMaterialLen);
677
if (EnableHardening && EnableRotation && rotationNonce != null)
718
using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
678719
{
679
for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
720
byte[] km = kdf.GetBytes(KeyMaterialLen);
721
if (EnableHardening && EnableRotation && rotationNonce != null)
722
{
723
for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
724
}
725
byte[] aesKey = new byte[AesKeyLen];
726
byte[] hmacKey = new byte[HmacKeyLen];
727
Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
728
Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
729
Array.Clear(km, 0, km.Length);
730
return (aesKey, hmacKey);
680731
}
681
byte[] aesKey = new byte[AesKeyLen];
682
byte[] hmacKey = new byte[HmacKeyLen];
683
Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
684
Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
685
Array.Clear(km, 0, km.Length);
686
DevLog($"DeriveKeysRotating called. EnableDummyMix={EnableDummyMix}, EnableRotation={EnableRotation}", DevLogLevel.Trace);
687
return (aesKey, hmacKey);
688732
}
733
catch (Exception ex)
734
{
735
DevLog("Key derivation failed. [Defense: Throw]", DevLogLevel.Critical, ex);
736
throw new InvalidOperationException("Key derivation failed, cannot proceed with crypto operations.", ex);
737
}
689738
}
690739
691
public void Save<T>(T data) where T : class
692
{
693
if (UseAsyncIO) SaveAsync(data).GetAwaiter().GetResult();
694
else SaveInternal(data);
695
}
696
697
public T Load<T>() where T : class
698
{
699
if (UseAsyncIO) return LoadAsync<T>().GetAwaiter().GetResult();
700
return LoadInternal<T>();
701
}
702
703740
public Task SaveAsync<T>(T data) where T : class
704741
{
742
if (!UseAsyncIO)
743
{
744
string json = JsonUtility.ToJson(data);
745
SaveInternalFromJson(json);
746
return Task.CompletedTask;
747
}
748
705749
string json = JsonUtility.ToJson(data);
706750
return Task.Run(() => SaveInternalFromJson(json));
707751
}
708752
709753
public async Task<T> LoadAsync<T>() where T : class
710754
{
711
string json = await Task.Run(() => LoadInternalGetJson());
755
string json = null;
756
757
if (!UseAsyncIO) json = LoadInternalGetJson();
758
else json = await Task.Run(() => LoadInternalGetJson());
759
712760
if (json == null) return default;
713761
T obj = null;
714762
try
715763
{
716764
obj = JsonUtility.FromJson<T>(json);
717
DevLog("JSON deserialized on main thread.", DevLogLevel.Trace);
718765
}
766
catch (ArgumentException ex)
767
{
768
DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
769
return default;
770
}
719771
catch (Exception ex)
720772
{
721
DevLog("JsonUtility.FromJson failed: " + ex.ToString(), DevLogLevel.Error);
773
DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
722774
return default;
723775
}
724776
return obj;
725777
}
726778
727
private void SaveInternal<T>(T data) where T : class
728
{
729
string json = JsonUtility.ToJson(data);
730
SaveInternalFromJson(json);
731
}
732
733779
private void SaveInternalFromJson(string json)
734780
{
735
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection))
781
if (IsTamperedOrDebug(out string reason))
736782
{
737
string reason;
738
if (IsTamperedOrDebug(out reason))
739
{
740
DevLog($"Save aborted: Tamper/Debug detected - {reason}", DevLogLevel.Warning);
741
return;
742
}
783
DevLog($"Save aborted: Tamper/Debug detected - {reason}. [Defense: Abort Save]", DevLogLevel.Warning);
784
return;
743785
}
744786
perfSave.Restart();
745787
try
......
752794
byte[] plain = Encoding.UTF8.GetBytes(json);
753795
byte[] iv;
754796
byte[] cipher;
755
using (Aes aes = Aes.Create())
797
798
try
756799
{
757
aes.KeySize = 256;
758
aes.Mode = CipherMode.CBC;
759
aes.Padding = PaddingMode.PKCS7;
760
aes.Key = aesKey;
761
aes.GenerateIV();
762
iv = aes.IV;
763
using (var enc = aes.CreateEncryptor(aes.Key, iv))
764
cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
800
using (Aes aes = Aes.Create())
801
{
802
aes.KeySize = 256;
803
aes.Mode = CipherMode.CBC;
804
aes.Padding = PaddingMode.PKCS7;
805
aes.Key = aesKey;
806
aes.GenerateIV();
807
iv = aes.IV;
808
using (var enc = aes.CreateEncryptor(aes.Key, iv))
809
cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
810
}
765811
}
812
catch (CryptographicException ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
813
catch (Exception ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
814
766815
long ticks = DateTime.UtcNow.Ticks;
767816
byte[] ticksBytes = BitConverter.GetBytes(ticks);
768817
byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
......
775824
byte[] hmac;
776825
using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput);
777826
byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey));
827
828
byte[] outBytes;
778829
using (MemoryStream ms = new MemoryStream())
779830
using (BinaryWriter bw = new BinaryWriter(ms))
780831
{
......
791842
bw.Write(cipher);
792843
bw.Write(hmac);
793844
bw.Write(ticksBytes);
794
byte[] outBytes = ms.ToArray();
795
string bak = filePath + ".bak";
796
try
797
{
798
if (File.Exists(filePath))
799
{
800
File.Copy(filePath, bak, true);
801
DevLog("Backup created: " + bak, DevLogLevel.Info);
802
}
803
}
804
catch (Exception ex)
805
{
806
DevLog("Backup creation failed: " + ex.Message, DevLogLevel.Warning);
807
}
808
try
809
{
810
File.WriteAllBytes(filePath, outBytes);
811
FileInfo fi = new FileInfo(filePath);
812
DevLog($"Save complete: {filePath} ({fi.Length} bytes)", DevLogLevel.Info);
813
}
814
catch (Exception ex)
815
{
816
DevLog("File write failed: " + ex.ToString(), DevLogLevel.Error);
817
throw;
818
}
845
outBytes = ms.ToArray();
819846
}
820
ClearBytes(aesKey);
821
ClearBytes(hmacKey);
822
ClearBytes(plain);
823
ClearBytes(iv);
824
ClearBytes(cipher);
825
ClearBytes(hmacInput);
826
ClearBytes(hmac);
827
ClearBytes(salt);
828
ClearBytes(rotation);
847
848
string bak = filePath + ".bak";
849
try
850
{
851
if (File.Exists(filePath)) File.Copy(filePath, bak, true);
852
}
853
catch (Exception ex) { DevLog($"Backup creation failed. [Recovery: Continue with Main Write]", DevLogLevel.Warning, ex); }
854
855
try
856
{
857
File.WriteAllBytes(filePath, outBytes);
858
FileInfo fi = new FileInfo(filePath);
859
DevLog($"Save successful: {filePath} ({fi.Length} bytes)", DevLogLevel.Info);
860
}
861
catch (IOException ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
862
catch (Exception ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
863
864
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher);
865
ClearBytes(hmacInput); ClearBytes(hmac); ClearBytes(salt); ClearBytes(rotation);
866
829867
perfSave.Stop();
830868
saveCount++;
831869
totalSaveMs += perfSave.ElapsedMilliseconds;
832
DevLog($"Save time: {perfSave.ElapsedMilliseconds} ms (avg {totalSaveMs / (double)saveCount:F1} ms over {saveCount} saves)", DevLogLevel.Info);
870
DevLog($"Save completed in {perfSave.ElapsedMilliseconds} ms", DevLogLevel.Info);
833871
}
834
catch (Exception ex)
835
{
836
DevLog("Save exception: " + ex.ToString(), DevLogLevel.Error);
837
}
872
catch (Exception ex) { DevLog("Save operation failed (Critical Catch).", DevLogLevel.Critical, ex); }
838873
}
839874
840875
private string LoadInternalGetJson()
841876
{
842
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection))
877
if (IsTamperedOrDebug(out string reason))
843878
{
844
string reason;
845
if (IsTamperedOrDebug(out reason))
846
{
847
DevLog($"Load aborted: Tamper/Debug detected - {reason}", DevLogLevel.Warning);
848
return null;
849
}
879
DevLog($"Load aborted: Tamper/Debug detected - {reason}. [Defense: Abort Load]", DevLogLevel.Warning);
880
return null;
850881
}
882
851883
perfLoad.Restart();
852
if (!File.Exists(filePath))
884
string json = TryLoadFile(filePath, false);
885
886
if (json == null)
853887
{
854
DevLog("No file to load: " + filePath, DevLogLevel.Info);
855
return null;
856
}
857
byte[] total;
858
try
859
{
860
total = File.ReadAllBytes(filePath);
861
}
862
catch (Exception ex)
863
{
864
DevLog("Read failed: " + ex.Message, DevLogLevel.Warning);
865888
string bak = filePath + ".bak";
866889
if (File.Exists(bak))
867890
{
868
try
891
DevLog("Main file load failed. Attempting to load from backup. [Recovery: Try Backup]", DevLogLevel.Warning);
892
json = TryLoadFile(bak, true);
893
894
if (json != null)
869895
{
870
total = File.ReadAllBytes(bak);
871
DevLog("Loaded from backup: " + bak, DevLogLevel.Info);
896
try
897
{
898
File.Copy(bak, filePath, true);
899
DevLog("Main file restored from backup. [Recovery: Success]", DevLogLevel.Trace);
900
}
901
catch (IOException ex)
902
{
903
DevLog("Failed to restore main file from backup. [Recovery: Continue with JSON]", DevLogLevel.Warning, ex);
904
}
872905
}
873
catch (Exception ex2)
906
else
874907
{
875
DevLog("Backup read failed: " + ex2.Message, DevLogLevel.Error);
876
return null;
908
DevLog("Backup file also failed to load. [Defense: Return Null]", DevLogLevel.Error);
877909
}
878910
}
879
else return null;
911
else
912
{
913
DevLog("No main file or backup found to load. [Defense: Return Null]", DevLogLevel.Info);
914
}
880915
}
916
917
perfLoad.Stop();
918
if (json != null)
919
{
920
loadCount++;
921
totalLoadMs += perfLoad.ElapsedMilliseconds;
922
DevLog($"Load successful. Load time: {perfLoad.ElapsedMilliseconds} ms", DevLogLevel.Info);
923
}
924
925
return json;
926
}
927
928
private string TryLoadFile(string path, bool isBackup)
929
{
930
if (!File.Exists(path)) return null;
931
byte[] total = null;
932
881933
try
882934
{
935
total = File.ReadAllBytes(path);
936
}
937
catch (IOException ex) { DevLog($"File read failed for {path}. [Defense: Reject File]", DevLogLevel.Warning, ex); return null; }
938
939
try
940
{
883941
using (MemoryStream ms = new MemoryStream(total))
884942
using (BinaryReader br = new BinaryReader(ms))
885943
{
886944
byte[] magic = br.ReadBytes(4);
887945
string mag = Encoding.ASCII.GetString(magic);
888946
string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
889
if (mag != expectedMagic)
890
{
891
DevLog("Magic mismatch.", DevLogLevel.Warning);
892
return null;
893
}
947
948
if (mag != expectedMagic) { DevLog($"Magic mismatch in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
949
894950
byte ver = br.ReadByte();
895
if (ver != CurrentVersion)
896
{
897
DevLog($"Version mismatch: {ver} != {CurrentVersion}", DevLogLevel.Warning);
898
return null;
899
}
951
if (ver != CurrentVersion) { DevLog($"Version mismatch in {path}: File Version {ver} != Current Version {CurrentVersion}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
952
900953
int saltLen = br.ReadByte();
901
if (saltLen <= 0 || saltLen > 64) { DevLog("Bad salt length.", DevLogLevel.Warning); return null; }
954
if (saltLen <= 0 || saltLen > 64) { DevLog($"Bad salt length ({saltLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
902955
byte[] salt = br.ReadBytes(saltLen);
956
903957
int rotationLen = br.ReadByte();
904958
byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
959
905960
byte[] iv = br.ReadBytes(IvLen);
961
906962
int cipherLen = br.ReadInt32();
907963
if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
908
if (cipherLen <= 0 || cipherLen > total.Length) { DevLog("Bad cipher length.", DevLogLevel.Warning); return null; }
964
if (cipherLen <= 0 || cipherLen > total.Length) { DevLog($"Bad cipher length ({cipherLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
909965
byte[] cipher = br.ReadBytes(cipherLen);
966
910967
byte[] hmac = br.ReadBytes(HmacLen);
911968
byte[] ticksBytes = br.ReadBytes(8);
969
912970
long ticks = BitConverter.ToInt64(ticksBytes, 0);
971
913972
byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
914973
int pos = 0;
915974
Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
......
917976
Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
918977
Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
919978
Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
979
920980
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
921981
byte[] aesKey = keys.aesKey;
922982
byte[] hmacKey = keys.hmacKey;
923
byte[] expected;
924
using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(hmacInput);
925
bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
926
if (!ok)
983
byte[] expectedHmac;
984
using (var h = new HMACSHA256(hmacKey)) expectedHmac = h.ComputeHash(hmacInput);
985
986
if (!CryptographicOperations.FixedTimeEquals(expectedHmac, hmac))
927987
{
928
DevLog("HMAC mismatch - possible tamper or key mismatch.", DevLogLevel.Warning);
929
ClearBytes(aesKey);
930
ClearBytes(hmacKey);
931
ClearBytes(expected);
988
DevLog($"HMAC mismatch in {path} (data tamper). [Defense: Reject Load and Clear Keys]", DevLogLevel.Critical);
989
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
932990
return null;
933991
}
992
934993
if (EnableTimeValidation && ticks > 0)
935994
{
936995
long nowTicks = DateTime.UtcNow.Ticks;
937996
long diffSec = Math.Abs(nowTicks - ticks) / TimeSpan.TicksPerSecond;
938
DevLog($"Saved timestamp: {new DateTime(ticks).ToString("o")}, Current UTC: {DateTime.UtcNow.ToString("o")}, diffSec={diffSec}", DevLogLevel.Info);
939997
if (diffSec > MaxAllowedTimeJumpSeconds)
940998
{
941
DevLog($"Time jump detected: {diffSec} seconds -> rejecting load.", DevLogLevel.Warning);
942
ClearBytes(aesKey);
943
ClearBytes(hmacKey);
944
ClearBytes(expected);
999
DevLog($"Time jump detected: {diffSec} seconds > Max ({MaxAllowedTimeJumpSeconds}). [Defense: Reject Load]", DevLogLevel.Warning);
1000
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
9451001
return null;
9461002
}
9471003
}
1004
9481005
byte[] plain;
949
using (Aes aes = Aes.Create())
950
{
951
aes.KeySize = 256;
952
aes.Mode = CipherMode.CBC;
953
aes.Padding = PaddingMode.PKCS7;
954
aes.Key = aesKey;
955
aes.IV = iv;
956
using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
957
plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
958
}
959
string json = Encoding.UTF8.GetString(plain);
960
ClearBytes(aesKey);
961
ClearBytes(hmacKey);
962
ClearBytes(expected);
963
ClearBytes(plain);
964
ClearBytes(iv);
965
ClearBytes(cipher);
966
ClearBytes(salt);
967
ClearBytes(rotation);
968
perfLoad.Stop();
969
loadCount++;
970
totalLoadMs += perfLoad.ElapsedMilliseconds;
971
DevLog($"Load complete: {filePath} ({total.Length} bytes). Load time: {perfLoad.ElapsedMilliseconds} ms (avg {totalLoadMs / (double)loadCount:F1} ms over {loadCount} loads)", DevLogLevel.Info);
972
return json;
973
}
974
}
975
catch (Exception ex)
976
{
977
DevLog("Load exception: " + ex.ToString(), DevLogLevel.Error);
978
string bak = filePath + ".bak";
979
if (File.Exists(bak))
980
{
981
DevLog("Attempting to load from backup.", DevLogLevel.Info);
9821006
try
9831007
{
984
byte[] bakTotal = File.ReadAllBytes(bak);
985
using (MemoryStream ms = new MemoryStream(bakTotal))
986
using (BinaryReader br = new BinaryReader(ms))
1008
using (Aes aes = Aes.Create())
9871009
{
988
byte[] magic = br.ReadBytes(4);
989
string mag = Encoding.ASCII.GetString(magic);
990
string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
991
if (mag != expectedMagic) { DevLog("Backup magic mismatch.", DevLogLevel.Warning); return null; }
992
byte ver = br.ReadByte();
993
if (ver != CurrentVersion) { DevLog("Backup version mismatch.", DevLogLevel.Warning); return null; }
994
int saltLen = br.ReadByte();
995
if (saltLen <= 0 || saltLen > 64) { DevLog("Backup bad salt length.", DevLogLevel.Warning); return null; }
996
byte[] salt = br.ReadBytes(saltLen);
997
int rotationLen = br.ReadByte();
998
byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
999
byte[] iv = br.ReadBytes(IvLen);
1000
int cipherLen = br.ReadInt32();
1001
if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
1002
if (cipherLen <= 0 || cipherLen > bakTotal.Length) { DevLog("Backup bad cipher length.", DevLogLevel.Warning); return null; }
1003
byte[] cipher = br.ReadBytes(cipherLen);
1004
byte[] hmac = br.ReadBytes(HmacLen);
1005
byte[] ticksBytes = br.ReadBytes(8);
1006
byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
1007
int pos = 0;
1008
Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
1009
Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
1010
Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
1011
Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
1012
Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
1013
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
1014
byte[] aesKey = keys.aesKey;
1015
byte[] hmacKey = keys.hmacKey;
1016
byte[] expected;
1017
using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(hmacInput);
1018
bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
1019
if (!ok) { ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); DevLog("Backup HMAC mismatch.", DevLogLevel.Warning); return null; }
1020
byte[] plain;
1021
using (Aes aes = Aes.Create())
1022
{
1023
aes.KeySize = 256;
1024
aes.Mode = CipherMode.CBC;
1025
aes.Padding = PaddingMode.PKCS7;
1026
aes.Key = aesKey;
1027
aes.IV = iv;
1028
using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
1029
plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
1030
}
1031
string json = Encoding.UTF8.GetString(plain);
1032
DevLog("Backup load successful.", DevLogLevel.Info);
1033
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); ClearBytes(plain);
1034
try
1035
{
1036
File.Copy(bak, filePath, true);
1037
DevLog("Main file restored from backup.", DevLogLevel.Info);
1038
}
1039
catch (Exception ex2)
1040
{
1041
DevLog("Failed to restore main from backup: " + ex2.Message, DevLogLevel.Warning);
1042
}
1043
return json;
1010
aes.KeySize = 256;
1011
aes.Mode = CipherMode.CBC;
1012
aes.Padding = PaddingMode.PKCS7;
1013
aes.Key = aesKey;
1014
aes.IV = iv;
1015
using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
1016
plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
10441017
}
10451018
}
1046
catch (Exception ex2)
1019
catch (CryptographicException ex)
10471020
{
1048
DevLog("Backup load failed: " + ex2.Message, DevLogLevel.Error);
1021
DevLog($"AES Decryption failed in {path}. [Defense: Reject Load]", DevLogLevel.Warning, ex);
1022
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
10491023
return null;
10501024
}
1025
1026
string json = Encoding.UTF8.GetString(plain);
1027
1028
ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); ClearBytes(plain);
1029
ClearBytes(iv); ClearBytes(cipher); ClearBytes(salt); ClearBytes(rotation);
1030
1031
return json;
10511032
}
1052
return null;
10531033
}
1034
catch (EndOfStreamException ex) { DevLog($"File structure truncated in {path}. [Defense: Reject File]", DevLogLevel.Error, ex); return null; }
1035
catch (Exception ex) { DevLog($"Load processing failed (General Error) for {path}. [Defense: Reject File]", DevLogLevel.Critical, ex); return null; }
10541036
}
10551037
1056
private static byte[] GenerateRandomBytes(int len)
1038
public SecureMetrics GetDiagnosticMetrics()
10571039
{
1058
byte[] b = new byte[len];
1059
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
1060
return b;
1061
}
1062
1063
private static void ClearBytes(byte[] b)
1064
{
1065
if (b == null) return;
1066
Array.Clear(b, 0, b.Length);
1067
}
1068
1069
private string ResolveFileName(string baseName)
1070
{
1071
if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
1072
byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
1073
using (var sha = SHA256.Create())
1040
string reason = "";
1041
bool isTampered = IsTamperedOrDebug(out reason);
1042
1043
return new SecureMetrics
10741044
{
1075
byte[] hash = sha.ComputeHash(nameBytes);
1076
string b64 = Convert.ToBase64String(hash);
1077
string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
1078
ClearBytes(hash);
1079
return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
1080
}
1045
TotalSaveTimeMs = totalSaveMs,
1046
TotalLoadTimeMs = totalLoadMs,
1047
SaveCount = saveCount,
1048
LoadCount = loadCount,
1049
SessionNonceStatus = nonceLoadedSecurely ? "SECURELY_LOADED" : "FALLBACK_USED",
1050
IsTamperingForced = ForceTamperingForTest,
1051
IsEnvironmentTampered = isTampered
1052
};
10811053
}
10821054
10831055
private bool IsTamperedOrDebug(out string reason)
10841056
{
10851057
reason = "";
1058
1059
if (ForceTamperingForTest) { reason = "Forced via Inspector/Dev Tools"; return true; }
10861060
if (!EnableHardening) return false;
1061
10871062
try
10881063
{
10891064
if (EnableAntiDebugChecks)
10901065
{
1091
if (System.Diagnostics.Debugger.IsAttached)
1092
{
1093
reason = "Debugger Attached";
1094
return true;
1095
}
1096
try
1097
{
1098
string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower();
1099
if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy"))
1100
{
1101
reason = $"ProcessName suspicious: {proc}";
1102
return true;
1103
}
1104
}
1105
catch (Exception ex)
1106
{
1107
DevLog("Process name check failed: " + ex.Message, DevLogLevel.Trace);
1108
}
1066
if (System.Diagnostics.Debugger.IsAttached) { reason = "Debugger Attached"; return true; }
1067
string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower();
1068
if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy")) { reason = $"ProcessName suspicious: {proc}"; return true; }
11091069
}
11101070
}
1111
catch (Exception ex) { DevLog("AntiDebugChecks failed: " + ex.Message, DevLogLevel.Trace); }
1071
catch { }
1072
11121073
try
11131074
{
11141075
if (EnableRootDetection)
11151076
{
11161077
string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
1117
foreach (var p in suspectPaths) if (File.Exists(p))
1118
{
1119
reason = $"Root path found: {p}";
1120
return true;
1121
}
1078
foreach (var p in suspectPaths) if (File.Exists(p)) { reason = $"Root path found: {p}"; return true; }
11221079
}
11231080
}
1124
catch (Exception ex) { DevLog("Root detection failed: " + ex.Message, DevLogLevel.Trace); }
1081
catch { }
1082
11251083
return false;
11261084
}
11271085
......
11501108
{
11511109
Assembly asm = typeof(SecureManager).Assembly;
11521110
string loc = asm.Location;
1153
if (string.IsNullOrEmpty(loc)) { success = false; return ""; }
1111
if (string.IsNullOrEmpty(loc)) return "";
11541112
byte[] bytes = File.ReadAllBytes(loc);
11551113
using (var sha = SHA256.Create())
11561114
{
......
11601118
return hex;
11611119
}
11621120
}
1163
catch { success = false; return ""; }
1121
catch { return ""; }
11641122
}
11651123
1166
private void DevLog(string message, DevLogLevel level = DevLogLevel.Info)
1124
private void DevLog(string message, DevLogLevel level = DevLogLevel.Info, Exception ex = null, [CallerMemberName] string methodName = "")
11671125
{
1168
if (!EnableDeveloperLogging) return;
1169
if (level < DeveloperLogLevel) return;
1170
string prefix = $"[SM:{level}] ";
1126
if (!EnableDeveloperLogging || level < DeveloperLogLevel) return;
1127
1128
string prefix = $"[SM:{level}][{methodName}] ";
11711129
string line = prefix + message;
1130
1131
if (ex != null)
1132
{
1133
line += $" | EXCEPTION_TYPE: {ex.GetType().Name} | EXCEPTION_MSG: {ex.Message}";
1134
if (level >= DevLogLevel.Error) line += $"\n--- STACK TRACE ---\n{ex.StackTrace}";
1135
}
1136
11721137
try
11731138
{
1174
if (level == DevLogLevel.Error || level == DevLogLevel.Critical) Debug.LogError(line);
1175
else if (level == DevLogLevel.Warning) Debug.LogWarning(line);
1176
else Debug.Log(line);
1139
if (level == DevLogLevel.Critical) UnityEngine.Debug.LogError("[CRITICAL] " + line);
1140
else if (level == DevLogLevel.Error) UnityEngine.Debug.LogError(line);
1141
else if (level == DevLogLevel.Warning) UnityEngine.Debug.LogWarning(line);
1142
else UnityEngine.Debug.Log(line);
11771143
}
11781144
catch { }
1145
11791146
try
11801147
{
11811148
File.AppendAllText(devLogFilePath, DateTime.UtcNow.ToString("o") + " " + line + Environment.NewLine);
......
11831150
catch { }
11841151
}
11851152
1153
private static byte[] GenerateRandomBytes(int len)
1154
{
1155
byte[] b = new byte[len];
1156
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
1157
return b;
1158
}
1159
1160
private static void ClearBytes(byte[] b)
1161
{
1162
if (b == null) return;
1163
Array.Clear(b, 0, b.Length);
1164
}
1165
1166
private string ResolveFileName(string baseName)
1167
{
1168
if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
1169
byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
1170
using (var sha = SHA256.Create())
1171
{
1172
byte[] hash = sha.ComputeHash(nameBytes);
1173
string b64 = Convert.ToBase64String(hash);
1174
string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
1175
ClearBytes(hash);
1176
return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
1177
}
1178
}
1179
11861180
private byte[] TryProtectedProtect(byte[] data)
11871181
{
11881182
try
......