[주의!] 문서의 이전 버전(에 수정)을 보고 있습니다. 최신 버전으로 이동
이 사용자는 특수 권한을 가지고 있습니다.
[ 펼치기ㆍ접기 ]
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대 욕구인 수면욕을 채우기위해 만드는 것.

코드 예상 기능.
  1. 방송이 켜져있는지 확인한다.
  2. 날짜를 받는다.
  3. API로 태그를 받는다.
  4. API로 실시간 채팅 기록을 받는다.
  5. 일단 무조건 첫시작은 저챗이다.
  6. 게임을 할 경우 태그의 게임이름을 받고 {gamename}을/를 하였다. 로 정한다.
  7. 카페 탐방이나 노래방일 경우를 대비하여 채팅도 같이 확인하도록한다.
  8. 그뒤 10분후 3번부터 7번까지 똑같이 반복한다.
  9. 만약 방송이 끝난다면 반복을 종료한다.
  10. 결과값을 자동 편집 시킨다.
  11. 그 결과값을 딥러닝시켜 정확도를 높힌다.

딥러닝을 파이썬으로 해보는건 처음이다...
도와줘요 티스토리...

현재 더시드위키에 방송 역사 문단이 있는 텐코 시부키, 시라유키 히나 문서를 이용할 예정입니다.

CMD
pip install scikit-learn
pip install requests
pip install websockets


scikit-learn은 딥러닝/머신러닝을 위한 모듈, requests는 방송 상태 확인, websockets 실시간 채팅 확인을 위해 사용된다.


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] 메모리 관리 실패, 개인정보 침해 우려, 심각한 에러, 심각한 운영체제 에러 유발 등등