[주의!] 문서의 이전 버전(에 수정)을 보고 있습니다. 최신 버전으로 이동
이 사용자는 특수 권한을 가지고 있습니다.
[ 펼치기ㆍ접기 ]
1기생
칸나(졸업) · 유니 · 후야(편입)
2기생
3기생
임직원
콘서트


1. 개요2. 방송 역사 작성 요령3. 각종 코드 모음
3.1. 방송 역사 작성 봇 코드3.2. 암호화 예전 코드3.3. 암호화 신규 코드
3.3.1. 기능
3.4. 예전 코드 실행 코드

1. 개요[편집]

이젠 방송 역사 편집까지 봇이 하는 혁신. 그 혁신을 개발중인 사용자.

생방은 본지 몇개월 안된 유튜브로만 보던 1~2년차 파스텔. 초기의 시작은 강지와 김블루 우결. 그렇게 강지를 알게된후 칸나를 알게되어 이렇게 파스텔이 되었다 카더라.

해둥이 주글께! - 해둥이 -

주석은 혁명이야!

나무위키Aidenk, Shirayuki_Hina, Hebi, Yuuri봇계정 와 동일인. 더시드위키의 사용자:Tenko_Shibuki, 사용자:Shirayuki_Hina와 동일인.
프로필 사진 너무 예쁜데... 예쁘고 귀여우니... 별찜좀...

이나리인 해둥이(?)가 아닌데 맞은 그런것. 근데 바람은 아님(?) 음 그니깐... 쨋든 파스텔.

연습장 바로가기


수필 바로가기

2. 방송 역사 작성 요령[편집]

간단합니다. 치지직에서 생방을 보면됩니다...

가끔 x 또는 카페의 방송 후기나 기록을 가지고 작성합니다.

3. 각종 코드 모음[편집]














































C#
100%
깃허브에 올리기도 귀찮기에 여기다가 쓰기.

3.1. 방송 역사 작성 봇 코드[편집]














































Python
100%


3.2. 암호화 예전 코드[편집]














































C#
100%

신규 코드가 이상하면[1] 이걸 쓰기위해 백?업본 이건 오류가 안나는 정상적인 코드.

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;
    }
}


3.3. 암호화 신규 코드[편집]














































C#
100%
오류 안고친 코드.

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; }
    }
}

3.3.1. 기능[편집]

  • 싱글톤 인스턴스 관리
    • 초기화
    • 세션 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 가능

3.4. 예전 코드 실행 코드[편집]

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가 활성화되어 있습니다.");
        }
    }
}
[1] 메모리 관리 실패, 개인정보 침해 우려, 심각한 에러, 심각한 운영체제 에러 유발 등등