사용자:Aidenk(r249 RAW)보기RAWBlame되돌리기비교[include(틀:유우리)] [include(틀:스텔라이브)] [include(틀:Aidenk/틀 모음, Aidenk=0)] {{{#!wiki style="max-width: 500px; padding: 10px 0; border-radius: 20px; background: linear-gradient(to right, #ba9dff, #ba9dff); float: right; text-align: center" ||<-2><tablewidth=100%><table bgcolor=#fff,#1c1d1f><tablebordercolor=#ba9dff,#ba9dff><colcolor=#fff><color=#00d2ff> || ||<-2><nopad>[youtube(bJqdy8925Oc)]|| ||<nopad>{{{#!folding 기존 사진 접기 [[파일:special gift of kanna.jpg|width=100%]][br][br][[파일:블아임.png|width=100%]][br][br][[파일:규리감.jpg|width=100%]][br][br][[파일:2025/09/27 유니버스 타비/리제 신의상 합방.jpg|width=100%]][br][[파일:클리셰 공식 단체 사진.jpg|width=100%]]}}}[br][[파일:유니유니.jpg|width=100%]]||}}} [목차] [clearfix] https://chzzk.naver.com/live/b044e3a3b9259246bc92e863e7d3f3b8 [include(틀:치지직, 라이브불러옴=b044e3a3b9259246bc92e863e7d3f3b8)] == 개요 == 이젠 방송 역사 편집까지 봇이 하는 혁신. 그 혁신을 개발중인 사용자. 생방은 본지 몇개월 안된 유튜브로만 보던 1~2년차 파스텔. 초기의 시작은 강지와 김블루 우결. 그렇게 강지를 알게된후 칸나를 알게되어 이렇게 파스텔이 되었다 카더라. 해둥이 주글께! - 해둥이 - 주석은 혁명이야! [[나무위키]]의 [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Aidenk|Aidenk]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Shirayuki_Hina|Shirayuki_Hina]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Hebi|Hebi]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Yuuri|Yuuri^^{{{-3 봇계정}}}^^]] 와 동일인. 더시드위키의 [[사용자:Tenko_Shibuki]], [[사용자:Shirayuki_Hina]]와 동일인. 프로필 사진 너무 예쁜데... 예쁘고 귀여우니... 별찜좀... 이나리인 해둥이(?)가 아닌데 맞은 그런것. 근데 바람은 아님(?) 음 그니깐... 쨋든 파스텔. [[/연습장|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)" '''연습장 바로가기'''}}}]] [[/버츄얼|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)" '''수필 바로가기'''}}}]] ==# 방송 역사 작성 요령 #== 간단합니다. 치지직에서 생방을 보면됩니다... 가끔 x 또는 카페의 방송 후기나 기록을 가지고 작성합니다. == 각종 코드 모음 == 깃허브에 올리기도 귀찮기에 여기다가 쓰기. 접근성이 여기가 더 편하다... === 방송 역사 작성 봇 코드 === [include(틀:GitHub 언어 통계, 언어1=Python, 언어비율1=100)] 생방을 보다보면 졸린 경우가 존재한다. 인간의 3대 욕구인 수면욕을 채우기위해 만드는 것. 코드 예상 기능. 1. 방송이 켜져있는지 확인한다. 1. 날짜를 얻는다. 1. API로 태그를 받는다. 1. --API로 실시간 채팅 기록을 받는다.--(공식 API 없어 포기) 1. 일단 무조건 첫시작은 뭐든지 저챗이다. 1. 게임을 할 경우 태그의 게임이름을 받고 {gamename}을/를 하였다. 로 정한다. 1. --카페 탐방이나 노래방일 경우를 대비하여 채팅도 같이 확인하도록한다.--(위와 동일사유) 1. 그뒤 10분후 3번부터 7번까지 똑같이 반복한다. 1. 만약 방송이 끝난다면 반복을 종료한다. 1. 결과값을 자동 편집 시킨다. 1. 그 결과값을 딥러닝시켜 정확도를 높힌다. 딥러닝을 파이썬으로 해보는건 처음이다... 도와줘요 티스토리... 현재 [[더시드위키]]에 방송 역사 문단이 있는 [[텐코 시부키]], [[시라유키 히나]] 문서를 이용할 예정입니다. CMD {{{pip install scikit-learn pip install requests pip install pytz}}} scikit-learn은 딥러닝/머신러닝을 위한 모듈, requests는 방송 상태 확인, pytz는 UTC에서 KST로 변환하는데 필요합니다. {{{#!syntax python }}} ===# 암호화 예전 코드 #=== [include(틀:GitHub 언어 통계, 언어1=C#, 언어비율1=100)] 신규 코드가 이상하면[* 메모리 관리 실패, 개인정보 침해 우려, 심각한 에러, 심각한 운영체제 에러 유발 등등] 이걸 쓰기위해 백?업본 이건 오류가 안나는 정상적인 코드. {{{#!syntax csharp using System; using System.IO; using System.Security.Cryptography; using System.Text; using UnityEngine; using System.Runtime.CompilerServices; [Serializable] public class PlayerData { public string playerName; public int score; } public class SecureManager : MonoBehaviour { public static SecureManager Instance { get; private set; } public bool EnableHardening = true; public bool EnableAntiDebugChecks = true; public bool EnableRootDetection = true; public bool EnableDummyMix = false; public bool EnableFileNameObfuscation = true; public bool EnableRotation = true; private const string Magic = "SJMP"; private const byte Version = 1; private const int SaltLenDefault = 12; private const int IvLen = 16; private const int HmacLen = 32; private const int KeyMaterialLen = 64; private const int AesKeyLen = 32; private const int HmacKeyLen = 32; private const int Pbkdf2Iterations = 20000; private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C }; private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE }; private const byte Mask = 0xAA; private string filePath; private byte[] sessionNonce; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); sessionNonce = LoadOrCreateSessionNonce(); filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData")); Warmup(); } private byte[] LoadOrCreateSessionNonce() { const string key = "SecureManager_SessionNonce_v1"; try { if (PlayerPrefs.HasKey(key)) { string b64 = PlayerPrefs.GetString(key); byte[] stored = Convert.FromBase64String(b64); if (stored != null && stored.Length > 0) return stored; } } catch { } byte[] nonce = GenerateRandomBytes(16); try { PlayerPrefs.SetString(key, Convert.ToBase64String(nonce)); PlayerPrefs.Save(); } catch { } return nonce; } private void Warmup() { try { _ = GetSecretBytes(); } catch { } } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixA(byte[] input) { byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int v = input[i]; v = ((v << 3) | (v >> 5)) & 0xFF; v ^= (i * 37) & 0xFF; outb[i] = (byte)v; } return outb; } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixB(byte[] input, int seed) { byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int v = input[i] ^ (seed >> (i % 8)); v = (v * 13 + (i * 7)) & 0xFF; v = (v ^ 0x5A) & 0xFF; outb[i] = (byte)v; } return outb; } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixC(byte[] input, byte[] nonce) { byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int n = nonce[i % nonce.Length]; int v = input[i]; v = ((v + n) ^ (n >> (i % 4))) & 0xFF; outb[i] = (byte)v; } return outb; } private byte[] GetSecretBytes() { byte[] k = new byte[obfPartA.Length + obfPartB.Length]; for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask); for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask); if (!EnableHardening || !EnableDummyMix) { byte[] simple = new byte[k.Length]; Buffer.BlockCopy(k, 0, simple, 0, k.Length); Array.Clear(k, 0, k.Length); return simple; } byte[] s1 = DumbMixA(k); // 시간 의존 제거, 디바이스 기반 결정적 시드 int deviceSeed = Application.identifier.GetHashCode() ^ SystemInfo.deviceUniqueIdentifier.GetHashCode(); byte[] s2 = DumbMixB(s1, deviceSeed); byte[] s3 = DumbMixC(s2, sessionNonce); for (int r = 0; r < 2; r++) { byte[] t = DumbMixA(s3); byte[] u = DumbMixB(t, deviceSeed ^ r); for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]); Array.Clear(t, 0, t.Length); Array.Clear(u, 0, u.Length); } Array.Clear(s1, 0, s1.Length); Array.Clear(s2, 0, s2.Length); byte[] result = new byte[k.Length]; for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]); Array.Clear(k, 0, k.Length); Array.Clear(s3, 0, s3.Length); return result; } private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce) { byte[] secret = GetSecretBytes(); string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier; Array.Clear(secret, 0, secret.Length); using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256)) { byte[] km = kdf.GetBytes(KeyMaterialLen); if (EnableHardening && EnableRotation && rotationNonce != null) { for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length]; } byte[] aesKey = new byte[AesKeyLen]; byte[] hmacKey = new byte[HmacKeyLen]; Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen); Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen); Array.Clear(km, 0, km.Length); return (aesKey, hmacKey); } } public void Save<T>(T data) { if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return; try { string json = JsonUtility.ToJson(data); byte[] salt = GenerateRandomBytes(SaltLenDefault); byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16]; var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); byte[] aesKey = keys.aesKey; byte[] hmacKey = keys.hmacKey; byte[] plain = Encoding.UTF8.GetBytes(json); byte[] iv; byte[] cipher; using (Aes aes = Aes.Create()) { aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = aesKey; aes.GenerateIV(); iv = aes.IV; using (var enc = aes.CreateEncryptor(aes.Key, iv)) cipher = enc.TransformFinalBlock(plain, 0, plain.Length); } byte[] ivCipher = new byte[iv.Length + cipher.Length]; Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length); Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length); byte[] hmac; using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(ivCipher); using (MemoryStream ms = new MemoryStream()) { ms.Write(Encoding.ASCII.GetBytes(Magic), 0, 4); ms.WriteByte(Version); ms.WriteByte((byte)salt.Length); ms.Write(salt, 0, salt.Length); ms.WriteByte(EnableRotation ? (byte)rotation.Length : (byte)0); if (EnableRotation) ms.Write(rotation, 0, rotation.Length); ms.Write(iv, 0, iv.Length); byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length); if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes); ms.Write(cipherLenBytes, 0, 4); ms.Write(cipher, 0, cipher.Length); ms.Write(hmac, 0, hmac.Length); string outBlob = Convert.ToBase64String(ms.ToArray()); File.WriteAllText(filePath, outBlob); } ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher); ClearBytes(ivCipher); ClearBytes(hmac); ClearBytes(salt); ClearBytes(rotation); } catch { } } public T Load<T>() { if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return default; if (!File.Exists(filePath)) return default; string blob = File.ReadAllText(filePath); byte[] total; try { total = Convert.FromBase64String(blob); } catch { return default; } try { using (MemoryStream ms = new MemoryStream(total)) using (BinaryReader br = new BinaryReader(ms)) { byte[] magic = br.ReadBytes(4); if (Encoding.ASCII.GetString(magic) != Magic) return default; byte ver = br.ReadByte(); if (ver != Version) return default; int saltLen = br.ReadByte(); if (saltLen <= 0 || saltLen > 64) return default; byte[] salt = br.ReadBytes(saltLen); int rotationLen = br.ReadByte(); byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16]; byte[] iv = br.ReadBytes(IvLen); int cipherLen = br.ReadInt32(); if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen); if (cipherLen <= 0 || cipherLen > total.Length) return default; byte[] cipher = br.ReadBytes(cipherLen); byte[] hmac = br.ReadBytes(HmacLen); byte[] ivCipher = new byte[iv.Length + cipher.Length]; Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length); Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length); var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); byte[] aesKey = keys.aesKey; byte[] hmacKey = keys.hmacKey; byte[] expected; using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(ivCipher); bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac); if (!ok) { ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); return default; } byte[] plain; using (Aes aes = Aes.Create()) { aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = aesKey; aes.IV = iv; using (var dec = aes.CreateDecryptor(aes.Key, aes.IV)) plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); } string json = Encoding.UTF8.GetString(plain); ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher); ClearBytes(salt); ClearBytes(rotation); return JsonUtility.FromJson<T>(json); } } catch { return default; } } private static byte[] GenerateRandomBytes(int len) { byte[] b = new byte[len]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b); return b; } private static void ClearBytes(byte[] b) { if (b == null) return; Array.Clear(b, 0, b.Length); } private string ResolveFileName(string baseName) { if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat"; byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier); using (var sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(nameBytes); string b64 = Convert.ToBase64String(hash); string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x'); ClearBytes(hash); return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat"; } } private bool IsTamperedOrDebug() { if (!EnableHardening) return false; try { if (EnableAntiDebugChecks) { if (System.Diagnostics.Debugger.IsAttached) return true; if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower().Contains("debug")) return true; } } catch { } try { if (EnableRootDetection) { string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" }; foreach (var p in suspectPaths) if (File.Exists(p)) return true; } } catch { } return false; } } }}} ===# 암호화 신규 코드 #=== [include(틀:GitHub 언어 통계, 언어1=C#, 언어비율1=100)] 오류 안고친 코드. .NET이 있어야만 실행된다니... {{{#!syntax csharp using System; using System.IO; using System.Text; using System.Security.Cryptography; using System.Threading.Tasks; using System.Reflection; using UnityEngine; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Collections.Generic; [Serializable] public class PlayerData { public string playerName; public int score; } public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 } public struct SecureMetrics { public long TotalSaveTimeMs; public long TotalLoadTimeMs; public int SaveCount; public int LoadCount; public string SessionNonceStatus; public bool IsTamperingForced; public bool IsEnvironmentTampered; } public class SecureManager : MonoBehaviour { public static SecureManager Instance { get; private set; } [Header("보안 강화 제어")] public bool EnableHardening = true; public bool EnableAntiDebugChecks = true; public bool EnableRootDetection = true; public bool EnableDummyMix = true; public bool EnableFileNameObfuscation = true; public bool EnableRotation = true; [Header("개발자 진단 및 로깅")] public bool EnableDeveloperLogging = true; public DevLogLevel DeveloperLogLevel = DevLogLevel.Info; [Tooltip("안티-치트 로직 테스트를 위해 강제로 Tampered 상태를 주입합니다.")] public bool ForceTamperingForTest = false; [Header("데이터 및 I/O 설정")] public bool EnableTimeValidation = true; public long MaxAllowedTimeJumpSeconds = 3600 * 6; [Tooltip("비동기 I/O 사용 여부 (권장)")] public bool UseAsyncIO = true; public byte CurrentVersion = 1; [Tooltip("어셈블리 해시 검증을 위한 16진수 문자열. 비어 있으면 검증하지 않습니다.")] public string ExpectedAssemblyHashHex = ""; private const int SaltLenDefault = 16; private const int IvLen = 16; private const int HmacLen = 32; private const int KeyMaterialLen = 64; private const int AesKeyLen = 32; private const int HmacKeyLen = 32; private const int Pbkdf2Iterations = 20000; private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C }; private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE }; private const byte Mask = 0xAA; private static readonly byte[] ObfMagicBytes = new byte[] { (byte)('S' ^ 0x5A ^ 0), (byte)('J' ^ 0x5A ^ 1), (byte)('M' ^ 0x5A ^ 2), (byte)('P' ^ 0x5A ^ 3) }; private const byte ObfMagicKey = 0x5A; private string filePath; private byte[] sessionNonce; private bool nonceLoadedSecurely = false; private Stopwatch perfSave = new Stopwatch(); private Stopwatch perfLoad = new Stopwatch(); private long totalSaveMs = 0; private long totalLoadMs = 0; private int saveCount = 0; private int loadCount = 0; private string devLogFilePath; private static class Obf { public static string Decode(byte[] cipher, byte key) { byte[] b = new byte[cipher.Length]; for (int i = 0; i < b.Length; i++) b[i] = (byte)(cipher[i] ^ key ^ (i & 0xFF)); string s = Encoding.UTF8.GetString(b); Array.Clear(b, 0, b.Length); return s; } } void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); sessionNonce = LoadOrCreateSessionNonce(); PrintSecurityStatus(); filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData")); devLogFilePath = Path.Combine(Application.persistentDataPath, "securemanager_devlog.txt"); if (!string.IsNullOrEmpty(ExpectedAssemblyHashHex)) { try { string calc = CalculateAssemblyHashPartial(out bool ok); if (!ok) DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning); else { if (!VerifyAssemblyHash(ExpectedAssemblyHashHex)) DevLog($"Assembly hash mismatch.", DevLogLevel.Critical); else DevLog($"Assembly hash verified OK.", DevLogLevel.Info); } } catch (Exception ex) { DevLog("Assembly verify failed due to exception.", DevLogLevel.Error, ex); } } DevLog("최종 데이터 경로: " + filePath, DevLogLevel.Info); DevLogEnvironmentSnapshot(); } private void DevLogEnvironmentSnapshot() { DevLog($"Platform={Application.platform}, OS={SystemInfo.operatingSystem}, Device={SystemInfo.deviceModel}, CPU={SystemInfo.processorType}, Memory={SystemInfo.systemMemorySize}MB", DevLogLevel.Info); } public void PrintSecurityStatus() { string status = nonceLoadedSecurely ? "SUCCESS: 세션 논스 안전하게 로드됨 (ProtectedData/Native)." : "WARNING: 세션 논스가 OS 보호 없이 저장됨 (초기 생성 또는 폴백 사용)."; DevLog($"--- 보안 상태 보고서 ---", DevLogLevel.Info); DevLog($"[논스 상태] {status}", nonceLoadedSecurely ? DevLogLevel.Info : DevLogLevel.Warning); DevLog($"[강화 설정] 활성: {EnableHardening}, 디버그 방어: {EnableAntiDebugChecks}, 루팅 감지: {EnableRootDetection}", DevLogLevel.Info); DevLog($"[변조 강제] 강제 테스트 상태: {ForceTamperingForTest}", ForceTamperingForTest ? DevLogLevel.Warning : DevLogLevel.Info); DevLog($"----------------------------", DevLogLevel.Info); } private byte[] LoadOrCreateSessionNonce() { const string key = "SecureManager_SessionNonce_v4"; byte[] newNonce = GenerateRandomBytes(16); try { if (PlayerPrefs.HasKey(key)) { string b64 = PlayerPrefs.GetString(key); if (!string.IsNullOrEmpty(b64)) { try { byte[] enc = Convert.FromBase64String(b64); byte[] dec = TryProtectedUnprotect(enc); if (dec != null && dec.Length > 0) { nonceLoadedSecurely = true; DevLog("Session Nonce successfully loaded and unprotected. [Recovery: Success]", DevLogLevel.Trace); return dec; } DevLog("ProtectedData Unprotect returned null or failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning); throw new CryptographicException("DPAPI/OS protection failed."); } catch (FormatException ex) { DevLog("Failed to decode session nonce from Base64. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); } catch (CryptographicException ex) { DevLog("ProtectedData unprotect failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); } catch (Exception ex) { DevLog("Unexpected error during Session Nonce loading/unprotecting.", DevLogLevel.Error, ex); } } } } catch (Exception ex) { DevLog("Load session nonce failed (PlayerPrefs read error). [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); } nonceLoadedSecurely = false; DevLog("Creating new Session Nonce (Fallback/Initial).", DevLogLevel.Info); try { byte[] protectedBytes = TryProtectedProtect(newNonce); string toStore = protectedBytes != null ? Convert.ToBase64String(protectedBytes) : Convert.ToBase64String(newNonce); PlayerPrefs.SetString(key, toStore); PlayerPrefs.Save(); if (protectedBytes == null) DevLog("New nonce saved without OS protection. [Recovery: Fallback to PlayerPrefs]", DevLogLevel.Warning); else DevLog("New nonce saved with OS protection.", DevLogLevel.Trace); } catch (Exception ex) { DevLog("Save new session nonce failed. [Defense: Use in-memory nonce]", DevLogLevel.Critical, ex); } return newNonce; } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixA(byte[] input) { if (input == null) return new byte[0]; byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int v = input[i]; v = ((v << 3) | (v >> 5)) & 0xFF; v ^= (i * 37) & 0xFF; outb[i] = (byte)v; } return outb; } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixB(byte[] input, int seed) { if (input == null) return new byte[0]; byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int v = input[i] ^ (seed >> (i % 8)); v = (v * 13 + (i * 7)) & 0xFF; v = (v ^ 0x5A) & 0xFF; outb[i] = (byte)v; } return outb; } [MethodImpl(MethodImplOptions.NoInlining)] private byte[] DumbMixC(byte[] input, byte[] nonce) { if (input == null) return new byte[0]; if (nonce == null || nonce.Length == 0) return (byte[])input.Clone(); byte[] outb = new byte[input.Length]; for (int i = 0; i < input.Length; i++) { int n = nonce[i % nonce.Length]; int v = input[i]; v = ((v + n) ^ (n >> (i % 4))) & 0xFF; outb[i] = (byte)v; } return outb; } private byte[] GetSecretBytes() { byte[] k = new byte[obfPartA.Length + obfPartB.Length]; for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask); for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask); if (!EnableHardening || !EnableDummyMix) { byte[] simple = new byte[k.Length]; Buffer.BlockCopy(k, 0, simple, 0, k.Length); Array.Clear(k, 0, k.Length); return simple; } byte[] s1 = DumbMixA(k); int deviceSeed = 0; try { deviceSeed = Application.identifier.GetHashCode() ^ (SystemInfo.deviceUniqueIdentifier?.GetHashCode() ?? 0); } catch { deviceSeed = Application.identifier.GetHashCode(); } byte[] s2 = DumbMixB(s1, deviceSeed); byte[] s3 = DumbMixC(s2, sessionNonce); for (int r = 0; r < 2; r++) { byte[] t = DumbMixA(s3); byte[] u = DumbMixB(t, deviceSeed ^ r); for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]); Array.Clear(t, 0, t.Length); Array.Clear(u, 0, u.Length); } Array.Clear(s1, 0, s1.Length); Array.Clear(s2, 0, s2.Length); byte[] result = new byte[k.Length]; for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]); Array.Clear(k, 0, k.Length); Array.Clear(s3, 0, s3.Length); return result; } private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce) { byte[] secret = GetSecretBytes(); string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier; Array.Clear(secret, 0, secret.Length); try { using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256)) { byte[] km = kdf.GetBytes(KeyMaterialLen); if (EnableHardening && EnableRotation && rotationNonce != null) { for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length]; } byte[] aesKey = new byte[AesKeyLen]; byte[] hmacKey = new byte[HmacKeyLen]; Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen); Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen); Array.Clear(km, 0, km.Length); return (aesKey, hmacKey); } } catch (Exception ex) { DevLog("Key derivation failed. [Defense: Throw]", DevLogLevel.Critical, ex); throw new InvalidOperationException("Key derivation failed, cannot proceed with crypto operations.", ex); } } public Task SaveAsync<T>(T data) where T : class { if (!UseAsyncIO) { string json = JsonUtility.ToJson(data); SaveInternalFromJson(json); return Task.CompletedTask; } string json = JsonUtility.ToJson(data); return Task.Run(() => SaveInternalFromJson(json)); } public async Task<T> LoadAsync<T>() where T : class { string json = null; if (!UseAsyncIO) json = LoadInternalGetJson(); else json = await Task.Run(() => LoadInternalGetJson()); if (json == null) return default; T obj = null; try { obj = JsonUtility.FromJson<T>(json); } catch (ArgumentException ex) { DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex); return default; } catch (Exception ex) { DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex); return default; } return obj; } private void SaveInternalFromJson(string json) { if (IsTamperedOrDebug(out string reason)) { DevLog($"Save aborted: Tamper/Debug detected - {reason}. [Defense: Abort Save]", DevLogLevel.Warning); return; } perfSave.Restart(); try { byte[] salt = GenerateRandomBytes(SaltLenDefault); byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16]; var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); byte[] aesKey = keys.aesKey; byte[] hmacKey = keys.hmacKey; byte[] plain = Encoding.UTF8.GetBytes(json); byte[] iv; byte[] cipher; try { using (Aes aes = Aes.Create()) { aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = aesKey; aes.GenerateIV(); iv = aes.IV; using (var enc = aes.CreateEncryptor(aes.Key, iv)) cipher = enc.TransformFinalBlock(plain, 0, plain.Length); } } catch (CryptographicException ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; } catch (Exception ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; } long ticks = DateTime.UtcNow.Ticks; byte[] ticksBytes = BitConverter.GetBytes(ticks); byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length]; int pos = 0; Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length; Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length; Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length; Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length; Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length); byte[] hmac; using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput); byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey)); byte[] outBytes; using (MemoryStream ms = new MemoryStream()) using (BinaryWriter bw = new BinaryWriter(ms)) { bw.Write(magicBytes); bw.Write(CurrentVersion); bw.Write((byte)salt.Length); bw.Write(salt); bw.Write((byte)(EnableRotation ? rotation.Length : 0)); if (EnableRotation) bw.Write(rotation); bw.Write(iv); byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length); if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes); bw.Write(cipherLenBytes); bw.Write(cipher); bw.Write(hmac); bw.Write(ticksBytes); outBytes = ms.ToArray(); } string bak = filePath + ".bak"; try { if (File.Exists(filePath)) File.Copy(filePath, bak, true); } catch (Exception ex) { DevLog($"Backup creation failed. [Recovery: Continue with Main Write]", DevLogLevel.Warning, ex); } try { File.WriteAllBytes(filePath, outBytes); FileInfo fi = new FileInfo(filePath); DevLog($"Save successful: {filePath} ({fi.Length} bytes)", DevLogLevel.Info); } catch (IOException ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; } catch (Exception ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; } ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher); ClearBytes(hmacInput); ClearBytes(hmac); ClearBytes(salt); ClearBytes(rotation); perfSave.Stop(); saveCount++; totalSaveMs += perfSave.ElapsedMilliseconds; DevLog($"Save completed in {perfSave.ElapsedMilliseconds} ms", DevLogLevel.Info); } catch (Exception ex) { DevLog("Save operation failed (Critical Catch).", DevLogLevel.Critical, ex); } } private string LoadInternalGetJson() { if (IsTamperedOrDebug(out string reason)) { DevLog($"Load aborted: Tamper/Debug detected - {reason}. [Defense: Abort Load]", DevLogLevel.Warning); return null; } perfLoad.Restart(); string json = TryLoadFile(filePath, false); if (json == null) { string bak = filePath + ".bak"; if (File.Exists(bak)) { DevLog("Main file load failed. Attempting to load from backup. [Recovery: Try Backup]", DevLogLevel.Warning); json = TryLoadFile(bak, true); if (json != null) { try { File.Copy(bak, filePath, true); DevLog("Main file restored from backup. [Recovery: Success]", DevLogLevel.Trace); } catch (IOException ex) { DevLog("Failed to restore main file from backup. [Recovery: Continue with JSON]", DevLogLevel.Warning, ex); } } else { DevLog("Backup file also failed to load. [Defense: Return Null]", DevLogLevel.Error); } } else { DevLog("No main file or backup found to load. [Defense: Return Null]", DevLogLevel.Info); } } perfLoad.Stop(); if (json != null) { loadCount++; totalLoadMs += perfLoad.ElapsedMilliseconds; DevLog($"Load successful. Load time: {perfLoad.ElapsedMilliseconds} ms", DevLogLevel.Info); } return json; } private string TryLoadFile(string path, bool isBackup) { if (!File.Exists(path)) return null; byte[] total = null; try { total = File.ReadAllBytes(path); } catch (IOException ex) { DevLog($"File read failed for {path}. [Defense: Reject File]", DevLogLevel.Warning, ex); return null; } try { using (MemoryStream ms = new MemoryStream(total)) using (BinaryReader br = new BinaryReader(ms)) { byte[] magic = br.ReadBytes(4); string mag = Encoding.ASCII.GetString(magic); string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey); if (mag != expectedMagic) { DevLog($"Magic mismatch in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; } byte ver = br.ReadByte(); if (ver != CurrentVersion) { DevLog($"Version mismatch in {path}: File Version {ver} != Current Version {CurrentVersion}. [Defense: Reject Load]", DevLogLevel.Warning); return null; } int saltLen = br.ReadByte(); if (saltLen <= 0 || saltLen > 64) { DevLog($"Bad salt length ({saltLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; } byte[] salt = br.ReadBytes(saltLen); int rotationLen = br.ReadByte(); byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16]; byte[] iv = br.ReadBytes(IvLen); int cipherLen = br.ReadInt32(); if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen); if (cipherLen <= 0 || cipherLen > total.Length) { DevLog($"Bad cipher length ({cipherLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; } byte[] cipher = br.ReadBytes(cipherLen); byte[] hmac = br.ReadBytes(HmacLen); byte[] ticksBytes = br.ReadBytes(8); long ticks = BitConverter.ToInt64(ticksBytes, 0); byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length]; int pos = 0; Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length; Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length; Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length; Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length; Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length); var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); byte[] aesKey = keys.aesKey; byte[] hmacKey = keys.hmacKey; byte[] expectedHmac; using (var h = new HMACSHA256(hmacKey)) expectedHmac = h.ComputeHash(hmacInput); if (!CryptographicOperations.FixedTimeEquals(expectedHmac, hmac)) { DevLog($"HMAC mismatch in {path} (data tamper). [Defense: Reject Load and Clear Keys]", DevLogLevel.Critical); ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); return null; } if (EnableTimeValidation && ticks > 0) { long nowTicks = DateTime.UtcNow.Ticks; long diffSec = Math.Abs(nowTicks - ticks) / TimeSpan.TicksPerSecond; if (diffSec > MaxAllowedTimeJumpSeconds) { DevLog($"Time jump detected: {diffSec} seconds > Max ({MaxAllowedTimeJumpSeconds}). [Defense: Reject Load]", DevLogLevel.Warning); ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); return null; } } byte[] plain; try { using (Aes aes = Aes.Create()) { aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = aesKey; aes.IV = iv; using (var dec = aes.CreateDecryptor(aes.Key, aes.IV)) plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); } } catch (CryptographicException ex) { DevLog($"AES Decryption failed in {path}. [Defense: Reject Load]", DevLogLevel.Warning, ex); ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); return null; } string json = Encoding.UTF8.GetString(plain); ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher); ClearBytes(salt); ClearBytes(rotation); return json; } } catch (EndOfStreamException ex) { DevLog($"File structure truncated in {path}. [Defense: Reject File]", DevLogLevel.Error, ex); return null; } catch (Exception ex) { DevLog($"Load processing failed (General Error) for {path}. [Defense: Reject File]", DevLogLevel.Critical, ex); return null; } } public SecureMetrics GetDiagnosticMetrics() { string reason = ""; bool isTampered = IsTamperedOrDebug(out reason); return new SecureMetrics { TotalSaveTimeMs = totalSaveMs, TotalLoadTimeMs = totalLoadMs, SaveCount = saveCount, LoadCount = loadCount, SessionNonceStatus = nonceLoadedSecurely ? "SECURELY_LOADED" : "FALLBACK_USED", IsTamperingForced = ForceTamperingForTest, IsEnvironmentTampered = isTampered }; } private bool IsTamperedOrDebug(out string reason) { reason = ""; if (ForceTamperingForTest) { reason = "Forced via Inspector/Dev Tools"; return true; } if (!EnableHardening) return false; try { if (EnableAntiDebugChecks) { if (System.Diagnostics.Debugger.IsAttached) { reason = "Debugger Attached"; return true; } string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower(); if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy")) { reason = $"ProcessName suspicious: {proc}"; return true; } } } catch { } try { if (EnableRootDetection) { string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" }; foreach (var p in suspectPaths) if (File.Exists(p)) { reason = $"Root path found: {p}"; return true; } } } catch { } return false; } private bool VerifyAssemblyHash(string expectedHex) { try { Assembly asm = typeof(SecureManager).Assembly; string loc = asm.Location; if (string.IsNullOrEmpty(loc)) return true; byte[] bytes = File.ReadAllBytes(loc); using (var sha = SHA256.Create()) { byte[] h = sha.ComputeHash(bytes); string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant(); return hex == expectedHex.ToLowerInvariant(); } } catch { return true; } } private string CalculateAssemblyHashPartial(out bool success) { success = false; try { Assembly asm = typeof(SecureManager).Assembly; string loc = asm.Location; if (string.IsNullOrEmpty(loc)) return ""; byte[] bytes = File.ReadAllBytes(loc); using (var sha = SHA256.Create()) { byte[] h = sha.ComputeHash(bytes); string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant(); success = true; return hex; } } catch { return ""; } } private void DevLog(string message, DevLogLevel level = DevLogLevel.Info, Exception ex = null, [CallerMemberName] string methodName = "") { if (!EnableDeveloperLogging || level < DeveloperLogLevel) return; string prefix = $"[SM:{level}][{methodName}] "; string line = prefix + message; if (ex != null) { line += $" | EXCEPTION_TYPE: {ex.GetType().Name} | EXCEPTION_MSG: {ex.Message}"; if (level >= DevLogLevel.Error) line += $"\n--- STACK TRACE ---\n{ex.StackTrace}"; } try { if (level == DevLogLevel.Critical) UnityEngine.Debug.LogError("[CRITICAL] " + line); else if (level == DevLogLevel.Error) UnityEngine.Debug.LogError(line); else if (level == DevLogLevel.Warning) UnityEngine.Debug.LogWarning(line); else UnityEngine.Debug.Log(line); } catch { } try { File.AppendAllText(devLogFilePath, DateTime.UtcNow.ToString("o") + " " + line + Environment.NewLine); } catch { } } private static byte[] GenerateRandomBytes(int len) { byte[] b = new byte[len]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b); return b; } private static void ClearBytes(byte[] b) { if (b == null) return; Array.Clear(b, 0, b.Length); } private string ResolveFileName(string baseName) { if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat"; byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier); using (var sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(nameBytes); string b64 = Convert.ToBase64String(hash); string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x'); ClearBytes(hash); return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat"; } } private byte[] TryProtectedProtect(byte[] data) { try { #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser); #else return null; #endif } catch { return null; } } private byte[] TryProtectedUnprotect(byte[] data) { try { #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN return ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser); #else return null; #endif } catch { return null; } } } }}} ====# 기능 #==== * 싱글톤 인스턴스 관리 * 초기화 * 세션 Nonce 생성 * 파일 경로 생성 * Warmup 호출 * 무작위 바이트 생성 * DumbMixA/B/C * SecretBytes 생성 * 키 파생 (DeriveKeysRotating) * AES 키/HMAC 키 분리 * Rotation 적용 (옵션) * Save<T> * JSON 직렬화 * 난수 Salt 생성 * Rotation 바이트 생성 * AES 키/HMAC 키 생성 * AES 암호화 수행 * HMAC 생성 * 메모리 블롭 생성 * 파일 저장 * 메모리 정리 * Load<T> * 파일 존재 확인 * Base64 디코딩 * Magic/Version 체크 * Salt/Rotation/IV/Cipher/HMAC 읽기 * 키 파생 * HMAC 검증 * AES 복호화 수행 * JSON 역직렬화 * 메모리 정리 * 파일명 난독화 및 경로 * ResolveFileName(baseName) * 최종 경로 반환(filePath) * 개발자 로깅 (EnableDeveloperLogging) * 로그 레벨 관리 * DevLog 함수: 콘솔 + 파일 기록 * 초기화 환경 로그 * 파일 경로, 백업, 파일 크기 로그 * Save/Load 시간 측정 및 평균 * 키 파생 옵션 로그 * 보안 실패 상세 로그 * 예외 메시지/스택트레이스 기록 * 로그 필터링 * 변조·디버깅 탐지 * Debugger.IsAttached * 프로세스 이름 확인 * 루팅 경로 검사 * 탐지 시 Save/Load 거부 및 로그 출력 * 어셈블리 무결성 (옵션) * ExpectedAssemblyHash 비교 * DevLog 해시 일부 출력 * 시간 위변조 검증 * Save 시 UTC ticks 저장 * Load 시 ticks 비교 * 시간 점프 감지 및 로그 출력 * 플랫폼 보호 (Windows DPAPI) * TryProtectedProtect / TryProtectedUnprotect * Windows 환경만 적용 * 성능/안정성 보조 * Save/Load 시간 측정 및 평균 * I/O 예외 시 백업 로드 시도 * 유틸리티 헬퍼 * GenerateRandomBytes(len) * ClearBytes(byte[]) * Obf.Decode * CalculateAssemblyHashPartial(out bool) * 파일 버전 및 마이그레이션 자리 * Version 바이트 관리 * MigrateData 가능 ===# 예전 코드 실행 코드 #=== {{{#!syntax csharp using UnityEngine; /// <summary> /// SecureManager의 Save 및 Load 메서드를 시연하는 예시 스크립트입니다. /// </summary> public class ExampleUsage : MonoBehaviour { private readonly string testPlayerName = "Gemini_Test_User"; private readonly int initialScore = 1000; private readonly int newScore = 5000; void Start() { // SecureManager가 씬에 존재하는지 확인합니다. if (SecureManager.Instance == null) { Debug.LogError("SecureManager 인스턴스를 찾을 수 없습니다. SecureManager.cs를 GameObject에 추가했는지 확인해주세요."); return; } // 1. 데이터 저장 테스트 SaveTestData(); // 2. 데이터 로드 테스트 LoadTestData(); // 3. 데이터 업데이트 및 재저장 UpdateAndSaveTestData(); // 4. 업데이트된 데이터 로드 확인 LoadTestData(); } void SaveTestData() { Debug.Log("--- 1. 초기 데이터 저장 시도 ---"); PlayerData dataToSave = new PlayerData { playerName = testPlayerName, score = initialScore }; SecureManager.Instance.Save(dataToSave); Debug.Log($"초기 데이터 저장 완료: Player={dataToSave.playerName}, Score={dataToSave.score}"); } void LoadTestData() { Debug.Log("--- 2. 데이터 로드 시도 ---"); PlayerData loadedData = SecureManager.Instance.Load<PlayerData>(); if (loadedData != null) { Debug.Log($"데이터 로드 성공: Player={loadedData.playerName}, Score={loadedData.score}"); } else { Debug.LogWarning("데이터 로드 실패 (파일 없음, 오류 발생, 혹은 디버거 감지)."); } } void UpdateAndSaveTestData() { Debug.Log("--- 3. 데이터 업데이트 및 재저장 시도 ---"); PlayerData loadedData = SecureManager.Instance.Load<PlayerData>(); if (loadedData != null) { loadedData.score = newScore; SecureManager.Instance.Save(loadedData); Debug.Log($"데이터 업데이트 및 저장 완료: New Score={loadedData.score}"); } } void OnDestroy() { // 애플리케이션 종료 시 디버거 체크가 활성화된 경우 에러 방지 if (SecureManager.Instance != null && SecureManager.Instance.EnableAntiDebugChecks) { SecureManager.Instance.EnableAntiDebugChecks = false; Debug.Log("SecureManager: Anti-Debug Checks가 활성화되어 있습니다."); } } } }}}