r246
r159
1[include(틀:유우리)]
r158
2[include(틀:스텔라이브)]
r163
3[include(틀:Aidenk/틀 모음, Aidenk=0)]
r85
4{{{#!wiki style="max-width: 500px; padding: 10px 0; border-radius: 20px; background: linear-gradient(to right, #ba9dff, #ba9dff); float: right; text-align: center"
r155
5||<-2><tablewidth=100%><table bgcolor=#fff,#1c1d1f><tablebordercolor=#ba9dff,#ba9dff><colcolor=#fff><color=#00d2ff> ||
r226
6||<-2><nopad>[youtube(bJqdy8925Oc)]||
r233
7||<nopad>{{{#!folding 기존 사진 접기
r229
8[[파일:special gift of kanna.jpg|width=100%]][br][br][[파일:블아임.png|width=100%]][br][br][[파일:규리감.jpg|width=100%]][br][br][[파일:2025/09/27 유니버스 타비/리제 신의상 합방.jpg|width=100%]][br][[파일:클리셰 공식 단체 사진.jpg|width=100%]]}}}[br][[파일:유니유니.jpg|width=100%]]||}}}
r56
9
r23
10[목차]
11[clearfix]
r167
12== 개요 ==
r230
13이젠 방송 역사 편집까지 봇이 하는 혁신. 그 혁신을 개발중인 사용자.
14
r168
15생방은 본지 몇개월 안된 유튜브로만 보던 1~2년차 파스텔. 초기의 시작은 강지와 김블루 우결. 그렇게 강지를 알게된후 칸나를 알게되어 이렇게 파스텔이 되었다 카더라.
r167
16
r196
17해둥이 주글께! - 해둥이 -
18
19주석은 혁명이야!
20
r214
21[[나무위키]]의 [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Aidenk|Aidenk]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Shirayuki_Hina|Shirayuki_Hina]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Hebi|Hebi]], [[https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90:Yuuri|Yuuri^^{{{-3 봇계정}}}^^]] 와 동일인. 더시드위키의 [[사용자:Tenko_Shibuki]], [[사용자:Shirayuki_Hina]]와 동일인.
r196
22프로필 사진 너무 예쁜데... 예쁘고 귀여우니... 별찜좀...
23
r218
24이나리인 해둥이(?)가 아닌데 맞은 그런것. 근데 바람은 아님(?) 음 그니깐... 쨋든 파스텔.
r196
25
26[[/연습장|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)"
27'''연습장 바로가기'''}}}]]
28
29[[/버츄얼|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)"
30'''수필 바로가기'''}}}]]
31
32==# 방송 역사 작성 요령 #==
r217
33간단합니다. 치지직에서 생방을 보면됩니다...
r135
34
r217
35가끔 x 또는 카페의 방송 후기나 기록을 가지고 작성합니다.
r143
36
r231
37== 각종 코드 모음 ==
r245
38깃허브에 올리기도 귀찮기에 여기다가 쓰기. 접근성이 여기가 더 편하다...
r231
39
40=== 방송 역사 작성 봇 코드 ===
r230
41[include(틀:GitHub 언어 통계, 언어1=Python, 언어비율1=100)]
42
r234
43생방을 보다보면 졸린 경우가 존재한다. 인간의 3대 욕구인 수면욕을 채우기위해 만드는 것.
44
r237
45코드 예상 기능.
46
r235
47 1. 방송이 켜져있는지 확인한다.
r246
48 1. 날짜를 얻는다.
r235
49 1. API로 태그를 받는다.
r246
50 1. --API로 실시간 채팅 기록을 받는다.--(공식 API 없어 포기)
51 1. 일단 무조건 첫시작은 뭐든지 저챗이다.
r235
52 1. 게임을 할 경우 태그의 게임이름을 받고 {gamename}을/를 하였다. 로 정한다.
r246
53 1. --카페 탐방이나 노래방일 경우를 대비하여 채팅도 같이 확인하도록한다.--(위와 동일사유)
r239
54 1. 그뒤 10분후 3번부터 7번까지 똑같이 반복한다.
r235
55 1. 만약 방송이 끝난다면 반복을 종료한다.
56 1. 결과값을 자동 편집 시킨다.
r236
57 1. 그 결과값을 딥러닝시켜 정확도를 높힌다.
r235
58
r240
59딥러닝을 파이썬으로 해보는건 처음이다...
60도와줘요 티스토리...
61
r238
62현재 [[더시드위키]]에 방송 역사 문단이 있는 [[텐코 시부키]], [[시라유키 히나]] 문서를 이용할 예정입니다.
63
r241
64CMD
r242
65{{{pip install scikit-learn
r243
66pip install requests
67pip install websockets}}}
r241
68
r243
69scikit-learn은 딥러닝/머신러닝을 위한 모듈, requests는 방송 상태 확인, websockets 실시간 채팅 확인을 위해 사용된다.
70
r230
71{{{#!syntax python
72
73}}}
74
r231
75===# 암호화 예전 코드 #===
r211
76[include(틀:GitHub 언어 통계, 언어1=C#, 언어비율1=100)]
r206
77
r208
78신규 코드가 이상하면[* 메모리 관리 실패, 개인정보 침해 우려, 심각한 에러, 심각한 운영체제 에러 유발 등등] 이걸 쓰기위해 백?업본 이건 오류가 안나는 정상적인 코드.
r206
79
80{{{#!syntax csharp
81using System;
82using System.IO;
83using System.Security.Cryptography;
84using System.Text;
85using UnityEngine;
86using System.Runtime.CompilerServices;
87
88[Serializable]
89public class PlayerData
90{
91 public string playerName;
92 public int score;
93}
94
95public class SecureManager : MonoBehaviour
96{
97 public static SecureManager Instance { get; private set; }
98
99 public bool EnableHardening = true;
100 public bool EnableAntiDebugChecks = true;
101 public bool EnableRootDetection = true;
102 public bool EnableDummyMix = false;
103 public bool EnableFileNameObfuscation = true;
104 public bool EnableRotation = true;
105
106 private const string Magic = "SJMP";
107 private const byte Version = 1;
108 private const int SaltLenDefault = 12;
109 private const int IvLen = 16;
110 private const int HmacLen = 32;
111 private const int KeyMaterialLen = 64;
112 private const int AesKeyLen = 32;
113 private const int HmacKeyLen = 32;
114 private const int Pbkdf2Iterations = 20000;
115 private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
116 private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
117 private const byte Mask = 0xAA;
118 private string filePath;
119 private byte[] sessionNonce;
120
121 void Awake()
122 {
123 if (Instance != null && Instance != this)
124 {
125 Destroy(gameObject);
126 return;
127 }
128 Instance = this;
129 DontDestroyOnLoad(gameObject);
130
131 sessionNonce = LoadOrCreateSessionNonce();
132 filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
133 Warmup();
134 }
135
136 private byte[] LoadOrCreateSessionNonce()
137 {
138 const string key = "SecureManager_SessionNonce_v1";
139 try
140 {
141 if (PlayerPrefs.HasKey(key))
142 {
143 string b64 = PlayerPrefs.GetString(key);
144 byte[] stored = Convert.FromBase64String(b64);
145 if (stored != null && stored.Length > 0) return stored;
146 }
147 }
148 catch { }
149
150 byte[] nonce = GenerateRandomBytes(16);
151 try
152 {
153 PlayerPrefs.SetString(key, Convert.ToBase64String(nonce));
154 PlayerPrefs.Save();
155 }
156 catch { }
157 return nonce;
158 }
159
160 private void Warmup()
161 {
162 try { _ = GetSecretBytes(); } catch { }
163 }
164
165 [MethodImpl(MethodImplOptions.NoInlining)]
166 private byte[] DumbMixA(byte[] input)
167 {
168 byte[] outb = new byte[input.Length];
169 for (int i = 0; i < input.Length; i++)
170 {
171 int v = input[i];
172 v = ((v << 3) | (v >> 5)) & 0xFF;
173 v ^= (i * 37) & 0xFF;
174 outb[i] = (byte)v;
175 }
176 return outb;
177 }
178
179 [MethodImpl(MethodImplOptions.NoInlining)]
180 private byte[] DumbMixB(byte[] input, int seed)
181 {
182 byte[] outb = new byte[input.Length];
183 for (int i = 0; i < input.Length; i++)
184 {
185 int v = input[i] ^ (seed >> (i % 8));
186 v = (v * 13 + (i * 7)) & 0xFF;
187 v = (v ^ 0x5A) & 0xFF;
188 outb[i] = (byte)v;
189 }
190 return outb;
191 }
192
193 [MethodImpl(MethodImplOptions.NoInlining)]
194 private byte[] DumbMixC(byte[] input, byte[] nonce)
195 {
196 byte[] outb = new byte[input.Length];
197 for (int i = 0; i < input.Length; i++)
198 {
199 int n = nonce[i % nonce.Length];
200 int v = input[i];
201 v = ((v + n) ^ (n >> (i % 4))) & 0xFF;
202 outb[i] = (byte)v;
203 }
204 return outb;
205 }
206
207 private byte[] GetSecretBytes()
208 {
209 byte[] k = new byte[obfPartA.Length + obfPartB.Length];
210 for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
211 for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
212
213 if (!EnableHardening || !EnableDummyMix)
214 {
215 byte[] simple = new byte[k.Length];
216 Buffer.BlockCopy(k, 0, simple, 0, k.Length);
217 Array.Clear(k, 0, k.Length);
218 return simple;
219 }
220
221 byte[] s1 = DumbMixA(k);
222
223 // 시간 의존 제거, 디바이스 기반 결정적 시드
224 int deviceSeed = Application.identifier.GetHashCode() ^ SystemInfo.deviceUniqueIdentifier.GetHashCode();
225
226 byte[] s2 = DumbMixB(s1, deviceSeed);
227 byte[] s3 = DumbMixC(s2, sessionNonce);
228 for (int r = 0; r < 2; r++)
229 {
230 byte[] t = DumbMixA(s3);
231 byte[] u = DumbMixB(t, deviceSeed ^ r);
232 for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]);
233 Array.Clear(t, 0, t.Length);
234 Array.Clear(u, 0, u.Length);
235 }
236 Array.Clear(s1, 0, s1.Length);
237 Array.Clear(s2, 0, s2.Length);
238
239 byte[] result = new byte[k.Length];
240 for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
241 Array.Clear(k, 0, k.Length);
242 Array.Clear(s3, 0, s3.Length);
243 return result;
244 }
245
246 private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce)
247 {
248 byte[] secret = GetSecretBytes();
249 string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
250 Array.Clear(secret, 0, secret.Length);
251
252 using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
253 {
254 byte[] km = kdf.GetBytes(KeyMaterialLen);
255 if (EnableHardening && EnableRotation && rotationNonce != null)
256 {
257 for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
258 }
259 byte[] aesKey = new byte[AesKeyLen];
260 byte[] hmacKey = new byte[HmacKeyLen];
261 Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
262 Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
263 Array.Clear(km, 0, km.Length);
264 return (aesKey, hmacKey);
265 }
266 }
267
268 public void Save<T>(T data)
269 {
270 if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return;
271 try
272 {
273 string json = JsonUtility.ToJson(data);
274 byte[] salt = GenerateRandomBytes(SaltLenDefault);
275 byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
276 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
277 byte[] aesKey = keys.aesKey;
278 byte[] hmacKey = keys.hmacKey;
279 byte[] plain = Encoding.UTF8.GetBytes(json);
280 byte[] iv;
281 byte[] cipher;
282 using (Aes aes = Aes.Create())
283 {
284 aes.KeySize = 256;
285 aes.Mode = CipherMode.CBC;
286 aes.Padding = PaddingMode.PKCS7;
287 aes.Key = aesKey;
288 aes.GenerateIV();
289 iv = aes.IV;
290 using (var enc = aes.CreateEncryptor(aes.Key, iv))
291 cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
292 }
293 byte[] ivCipher = new byte[iv.Length + cipher.Length];
294 Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
295 Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
296 byte[] hmac;
297 using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(ivCipher);
298 using (MemoryStream ms = new MemoryStream())
299 {
300 ms.Write(Encoding.ASCII.GetBytes(Magic), 0, 4);
301 ms.WriteByte(Version);
302 ms.WriteByte((byte)salt.Length);
303 ms.Write(salt, 0, salt.Length);
304 ms.WriteByte(EnableRotation ? (byte)rotation.Length : (byte)0);
305 if (EnableRotation) ms.Write(rotation, 0, rotation.Length);
306 ms.Write(iv, 0, iv.Length);
307 byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
308 if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
309 ms.Write(cipherLenBytes, 0, 4);
310 ms.Write(cipher, 0, cipher.Length);
311 ms.Write(hmac, 0, hmac.Length);
312 string outBlob = Convert.ToBase64String(ms.ToArray());
313 File.WriteAllText(filePath, outBlob);
314 }
315 ClearBytes(aesKey);
316 ClearBytes(hmacKey);
317 ClearBytes(plain);
318 ClearBytes(iv);
319 ClearBytes(cipher);
320 ClearBytes(ivCipher);
321 ClearBytes(hmac);
322 ClearBytes(salt);
323 ClearBytes(rotation);
324 }
325 catch { }
326 }
327
328 public T Load<T>()
329 {
330 if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return default;
331 if (!File.Exists(filePath)) return default;
332 string blob = File.ReadAllText(filePath);
333 byte[] total;
334 try { total = Convert.FromBase64String(blob); } catch { return default; }
335 try
336 {
337 using (MemoryStream ms = new MemoryStream(total))
338 using (BinaryReader br = new BinaryReader(ms))
339 {
340 byte[] magic = br.ReadBytes(4);
341 if (Encoding.ASCII.GetString(magic) != Magic) return default;
342 byte ver = br.ReadByte();
343 if (ver != Version) return default;
344 int saltLen = br.ReadByte();
345 if (saltLen <= 0 || saltLen > 64) return default;
346 byte[] salt = br.ReadBytes(saltLen);
347 int rotationLen = br.ReadByte();
348 byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
349 byte[] iv = br.ReadBytes(IvLen);
350 int cipherLen = br.ReadInt32();
351 if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
352 if (cipherLen <= 0 || cipherLen > total.Length) return default;
353 byte[] cipher = br.ReadBytes(cipherLen);
354 byte[] hmac = br.ReadBytes(HmacLen);
355 byte[] ivCipher = new byte[iv.Length + cipher.Length];
356 Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
357 Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
358 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
359 byte[] aesKey = keys.aesKey;
360 byte[] hmacKey = keys.hmacKey;
361 byte[] expected;
362 using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(ivCipher);
363 bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
364 if (!ok)
365 {
366 ClearBytes(aesKey);
367 ClearBytes(hmacKey);
368 ClearBytes(expected);
369 return default;
370 }
371 byte[] plain;
372 using (Aes aes = Aes.Create())
373 {
374 aes.KeySize = 256;
375 aes.Mode = CipherMode.CBC;
376 aes.Padding = PaddingMode.PKCS7;
377 aes.Key = aesKey;
378 aes.IV = iv;
379 using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
380 plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
381 }
382 string json = Encoding.UTF8.GetString(plain);
383 ClearBytes(aesKey);
384 ClearBytes(hmacKey);
385 ClearBytes(expected);
386 ClearBytes(plain);
387 ClearBytes(iv);
388 ClearBytes(cipher);
389 ClearBytes(salt);
390 ClearBytes(rotation);
391 return JsonUtility.FromJson<T>(json);
392 }
393 }
394 catch { return default; }
395 }
396
397 private static byte[] GenerateRandomBytes(int len)
398 {
399 byte[] b = new byte[len];
400 using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
401 return b;
402 }
403
404 private static void ClearBytes(byte[] b)
405 {
406 if (b == null) return;
407 Array.Clear(b, 0, b.Length);
408 }
409
410 private string ResolveFileName(string baseName)
411 {
412 if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
413 byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
414 using (var sha = SHA256.Create())
415 {
416 byte[] hash = sha.ComputeHash(nameBytes);
417 string b64 = Convert.ToBase64String(hash);
418 string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
419 ClearBytes(hash);
420 return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
421 }
422 }
423
424 private bool IsTamperedOrDebug()
425 {
426 if (!EnableHardening) return false;
427 try
428 {
429 if (EnableAntiDebugChecks)
430 {
431 if (System.Diagnostics.Debugger.IsAttached) return true;
432 if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower().Contains("debug")) return true;
433 }
434 }
435 catch { }
436 try
437 {
438 if (EnableRootDetection)
439 {
440 string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
441 foreach (var p in suspectPaths) if (File.Exists(p)) return true;
442 }
443 }
444 catch { }
445 return false;
446 }
447}
448}}}
r231
449
450
451===# 암호화 신규 코드 #===
452[include(틀:GitHub 언어 통계, 언어1=C#, 언어비율1=100)]
r244
453오류 안고친 코드.
454.NET이 있어야만 실행된다니...
r224
455
r182
456{{{#!syntax csharp
457using System;
458using System.IO;
r201
459using System.Text;
r182
460using System.Security.Cryptography;
r201
461using System.Threading.Tasks;
462using System.Reflection;
r182
463using UnityEngine;
r201
464using System.Diagnostics;
r182
465using System.Runtime.CompilerServices;
r232
466using System.Collections.Generic;
r182
467
468[Serializable]
469public class PlayerData
470{
471 public string playerName;
472 public int score;
473}
474
r201
475public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 }
476
r232
477public struct SecureMetrics
478{
479 public long TotalSaveTimeMs;
480 public long TotalLoadTimeMs;
481 public int SaveCount;
482 public int LoadCount;
483 public string SessionNonceStatus;
484 public bool IsTamperingForced;
485 public bool IsEnvironmentTampered;
486}
487
r182
488public class SecureManager : MonoBehaviour
489{
490 public static SecureManager Instance { get; private set; }
491
r232
492 [Header("보안 강화 제어")]
r182
493 public bool EnableHardening = true;
494 public bool EnableAntiDebugChecks = true;
495 public bool EnableRootDetection = true;
r201
496 public bool EnableDummyMix = true;
r197
497 public bool EnableFileNameObfuscation = true;
r182
498 public bool EnableRotation = true;
r232
499
500 [Header("개발자 진단 및 로깅")]
r201
501 public bool EnableDeveloperLogging = true;
502 public DevLogLevel DeveloperLogLevel = DevLogLevel.Info;
r232
503 [Tooltip("안티-치트 로직 테스트를 위해 강제로 Tampered 상태를 주입합니다.")]
504 public bool ForceTamperingForTest = false;
505
506 [Header("데이터 및 I/O 설정")]
r201
507 public bool EnableTimeValidation = true;
508 public long MaxAllowedTimeJumpSeconds = 3600 * 6;
r232
509 [Tooltip("비동기 I/O 사용 여부 (권장)")]
r201
510 public bool UseAsyncIO = true;
511 public byte CurrentVersion = 1;
r232
512 [Tooltip("어셈블리 해시 검증을 위한 16진수 문자열. 비어 있으면 검증하지 않습니다.")]
r201
513 public string ExpectedAssemblyHashHex = "";
r182
514
r201
515 private const int SaltLenDefault = 16;
r182
516 private const int IvLen = 16;
517 private const int HmacLen = 32;
518 private const int KeyMaterialLen = 64;
519 private const int AesKeyLen = 32;
520 private const int HmacKeyLen = 32;
521 private const int Pbkdf2Iterations = 20000;
r201
522
r182
523 private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
524 private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
525 private const byte Mask = 0xAA;
r201
526
527 private static readonly byte[] ObfMagicBytes = new byte[] { (byte)('S' ^ 0x5A ^ 0), (byte)('J' ^ 0x5A ^ 1), (byte)('M' ^ 0x5A ^ 2), (byte)('P' ^ 0x5A ^ 3) };
528 private const byte ObfMagicKey = 0x5A;
529
r182
530 private string filePath;
531 private byte[] sessionNonce;
r232
532 private bool nonceLoadedSecurely = false;
533
r201
534 private Stopwatch perfSave = new Stopwatch();
535 private Stopwatch perfLoad = new Stopwatch();
536 private long totalSaveMs = 0;
537 private long totalLoadMs = 0;
538 private int saveCount = 0;
539 private int loadCount = 0;
540 private string devLogFilePath;
r232
541
r201
542 private static class Obf
543 {
544 public static string Decode(byte[] cipher, byte key)
545 {
546 byte[] b = new byte[cipher.Length];
547 for (int i = 0; i < b.Length; i++) b[i] = (byte)(cipher[i] ^ key ^ (i & 0xFF));
548 string s = Encoding.UTF8.GetString(b);
549 Array.Clear(b, 0, b.Length);
550 return s;
551 }
552 }
553
r182
554 void Awake()
555 {
556 if (Instance != null && Instance != this)
557 {
558 Destroy(gameObject);
559 return;
560 }
561 Instance = this;
562 DontDestroyOnLoad(gameObject);
r232
563
r198
564 sessionNonce = LoadOrCreateSessionNonce();
r232
565 PrintSecurityStatus();
566
r182
567 filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
r201
568 devLogFilePath = Path.Combine(Application.persistentDataPath, "securemanager_devlog.txt");
r232
569
r201
570 if (!string.IsNullOrEmpty(ExpectedAssemblyHashHex))
571 {
572 try
573 {
574 string calc = CalculateAssemblyHashPartial(out bool ok);
r232
575 if (!ok) DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning);
r201
576 else
577 {
r232
578 if (!VerifyAssemblyHash(ExpectedAssemblyHashHex)) DevLog($"Assembly hash mismatch.", DevLogLevel.Critical);
579 else DevLog($"Assembly hash verified OK.", DevLogLevel.Info);
r201
580 }
581 }
r232
582 catch (Exception ex) { DevLog("Assembly verify failed due to exception.", DevLogLevel.Error, ex); }
r201
583 }
r232
584 DevLog("최종 데이터 경로: " + filePath, DevLogLevel.Info);
r201
585 DevLogEnvironmentSnapshot();
r182
586 }
587
r201
588 private void DevLogEnvironmentSnapshot()
589 {
590 DevLog($"Platform={Application.platform}, OS={SystemInfo.operatingSystem}, Device={SystemInfo.deviceModel}, CPU={SystemInfo.processorType}, Memory={SystemInfo.systemMemorySize}MB", DevLogLevel.Info);
591 }
r232
592
593 public void PrintSecurityStatus()
594 {
595 string status = nonceLoadedSecurely
596 ? "SUCCESS: 세션 논스 안전하게 로드됨 (ProtectedData/Native)."
597 : "WARNING: 세션 논스가 OS 보호 없이 저장됨 (초기 생성 또는 폴백 사용).";
598
599 DevLog($"--- 보안 상태 보고서 ---", DevLogLevel.Info);
600 DevLog($"[논스 상태] {status}", nonceLoadedSecurely ? DevLogLevel.Info : DevLogLevel.Warning);
601 DevLog($"[강화 설정] 활성: {EnableHardening}, 디버그 방어: {EnableAntiDebugChecks}, 루팅 감지: {EnableRootDetection}", DevLogLevel.Info);
602 DevLog($"[변조 강제] 강제 테스트 상태: {ForceTamperingForTest}", ForceTamperingForTest ? DevLogLevel.Warning : DevLogLevel.Info);
603 DevLog($"----------------------------", DevLogLevel.Info);
604 }
r201
605
r198
606 private byte[] LoadOrCreateSessionNonce()
607 {
r201
608 const string key = "SecureManager_SessionNonce_v4";
r232
609 byte[] newNonce = GenerateRandomBytes(16);
610
r198
611 try
612 {
613 if (PlayerPrefs.HasKey(key))
614 {
615 string b64 = PlayerPrefs.GetString(key);
r201
616 if (!string.IsNullOrEmpty(b64))
617 {
618 try
619 {
620 byte[] enc = Convert.FromBase64String(b64);
621 byte[] dec = TryProtectedUnprotect(enc);
r232
622
623 if (dec != null && dec.Length > 0)
624 {
625 nonceLoadedSecurely = true;
626 DevLog("Session Nonce successfully loaded and unprotected. [Recovery: Success]", DevLogLevel.Trace);
627 return dec;
628 }
629
630 DevLog("ProtectedData Unprotect returned null or failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning);
631 throw new CryptographicException("DPAPI/OS protection failed.");
r201
632 }
r232
633 catch (FormatException ex) { DevLog("Failed to decode session nonce from Base64. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
634 catch (CryptographicException ex) { DevLog("ProtectedData unprotect failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
635 catch (Exception ex) { DevLog("Unexpected error during Session Nonce loading/unprotecting.", DevLogLevel.Error, ex); }
r201
636 }
r198
637 }
638 }
r232
639 catch (Exception ex) { DevLog("Load session nonce failed (PlayerPrefs read error). [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
640
641 nonceLoadedSecurely = false;
642 DevLog("Creating new Session Nonce (Fallback/Initial).", DevLogLevel.Info);
643
r198
644 try
645 {
r232
646 byte[] protectedBytes = TryProtectedProtect(newNonce);
647 string toStore = protectedBytes != null
648 ? Convert.ToBase64String(protectedBytes)
649 : Convert.ToBase64String(newNonce);
650
r201
651 PlayerPrefs.SetString(key, toStore);
r198
652 PlayerPrefs.Save();
r232
653 if (protectedBytes == null) DevLog("New nonce saved without OS protection. [Recovery: Fallback to PlayerPrefs]", DevLogLevel.Warning);
654 else DevLog("New nonce saved with OS protection.", DevLogLevel.Trace);
r198
655 }
r232
656 catch (Exception ex) { DevLog("Save new session nonce failed. [Defense: Use in-memory nonce]", DevLogLevel.Critical, ex); }
657 return newNonce;
r198
658 }
659
r182
660 [MethodImpl(MethodImplOptions.NoInlining)]
661 private byte[] DumbMixA(byte[] input)
662 {
r201
663 if (input == null) return new byte[0];
r182
664 byte[] outb = new byte[input.Length];
665 for (int i = 0; i < input.Length; i++)
666 {
667 int v = input[i];
668 v = ((v << 3) | (v >> 5)) & 0xFF;
669 v ^= (i * 37) & 0xFF;
670 outb[i] = (byte)v;
671 }
672 return outb;
673 }
674
675 [MethodImpl(MethodImplOptions.NoInlining)]
676 private byte[] DumbMixB(byte[] input, int seed)
677 {
r201
678 if (input == null) return new byte[0];
r182
679 byte[] outb = new byte[input.Length];
680 for (int i = 0; i < input.Length; i++)
681 {
682 int v = input[i] ^ (seed >> (i % 8));
683 v = (v * 13 + (i * 7)) & 0xFF;
684 v = (v ^ 0x5A) & 0xFF;
685 outb[i] = (byte)v;
686 }
687 return outb;
688 }
689
690 [MethodImpl(MethodImplOptions.NoInlining)]
691 private byte[] DumbMixC(byte[] input, byte[] nonce)
692 {
r201
693 if (input == null) return new byte[0];
694 if (nonce == null || nonce.Length == 0) return (byte[])input.Clone();
r182
695 byte[] outb = new byte[input.Length];
696 for (int i = 0; i < input.Length; i++)
697 {
698 int n = nonce[i % nonce.Length];
699 int v = input[i];
700 v = ((v + n) ^ (n >> (i % 4))) & 0xFF;
701 outb[i] = (byte)v;
702 }
703 return outb;
704 }
705
706 private byte[] GetSecretBytes()
707 {
708 byte[] k = new byte[obfPartA.Length + obfPartB.Length];
709 for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
710 for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
711 if (!EnableHardening || !EnableDummyMix)
712 {
713 byte[] simple = new byte[k.Length];
714 Buffer.BlockCopy(k, 0, simple, 0, k.Length);
715 Array.Clear(k, 0, k.Length);
716 return simple;
717 }
718 byte[] s1 = DumbMixA(k);
r201
719 int deviceSeed = 0;
720 try { deviceSeed = Application.identifier.GetHashCode() ^ (SystemInfo.deviceUniqueIdentifier?.GetHashCode() ?? 0); } catch { deviceSeed = Application.identifier.GetHashCode(); }
r198
721 byte[] s2 = DumbMixB(s1, deviceSeed);
r182
722 byte[] s3 = DumbMixC(s2, sessionNonce);
723 for (int r = 0; r < 2; r++)
724 {
725 byte[] t = DumbMixA(s3);
r198
726 byte[] u = DumbMixB(t, deviceSeed ^ r);
r182
727 for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]);
728 Array.Clear(t, 0, t.Length);
729 Array.Clear(u, 0, u.Length);
730 }
731 Array.Clear(s1, 0, s1.Length);
732 Array.Clear(s2, 0, s2.Length);
733 byte[] result = new byte[k.Length];
734 for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
735 Array.Clear(k, 0, k.Length);
736 Array.Clear(s3, 0, s3.Length);
737 return result;
738 }
739
740 private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce)
741 {
742 byte[] secret = GetSecretBytes();
743 string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
744 Array.Clear(secret, 0, secret.Length);
r232
745 try
r182
746 {
r232
747 using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
r182
748 {
r232
749 byte[] km = kdf.GetBytes(KeyMaterialLen);
750 if (EnableHardening && EnableRotation && rotationNonce != null)
751 {
752 for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
753 }
754 byte[] aesKey = new byte[AesKeyLen];
755 byte[] hmacKey = new byte[HmacKeyLen];
756 Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
757 Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
758 Array.Clear(km, 0, km.Length);
759 return (aesKey, hmacKey);
r182
760 }
761 }
r232
762 catch (Exception ex)
763 {
764 DevLog("Key derivation failed. [Defense: Throw]", DevLogLevel.Critical, ex);
765 throw new InvalidOperationException("Key derivation failed, cannot proceed with crypto operations.", ex);
766 }
r182
767 }
768
r201
769 public Task SaveAsync<T>(T data) where T : class
770 {
r232
771 if (!UseAsyncIO)
772 {
773 string json = JsonUtility.ToJson(data);
774 SaveInternalFromJson(json);
775 return Task.CompletedTask;
776 }
777
r201
778 string json = JsonUtility.ToJson(data);
779 return Task.Run(() => SaveInternalFromJson(json));
780 }
781
782 public async Task<T> LoadAsync<T>() where T : class
783 {
r232
784 string json = null;
785
786 if (!UseAsyncIO) json = LoadInternalGetJson();
787 else json = await Task.Run(() => LoadInternalGetJson());
788
r201
789 if (json == null) return default;
790 T obj = null;
r182
791 try
792 {
r201
793 obj = JsonUtility.FromJson<T>(json);
794 }
r232
795 catch (ArgumentException ex)
796 {
797 DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
798 return default;
799 }
r201
800 catch (Exception ex)
801 {
r232
802 DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
r201
803 return default;
804 }
805 return obj;
806 }
807
808 private void SaveInternalFromJson(string json)
809 {
r232
810 if (IsTamperedOrDebug(out string reason))
r201
811 {
r232
812 DevLog($"Save aborted: Tamper/Debug detected - {reason}. [Defense: Abort Save]", DevLogLevel.Warning);
813 return;
r201
814 }
815 perfSave.Restart();
816 try
817 {
r182
818 byte[] salt = GenerateRandomBytes(SaltLenDefault);
819 byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
820 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
821 byte[] aesKey = keys.aesKey;
822 byte[] hmacKey = keys.hmacKey;
823 byte[] plain = Encoding.UTF8.GetBytes(json);
824 byte[] iv;
825 byte[] cipher;
r232
826
827 try
r182
828 {
r232
829 using (Aes aes = Aes.Create())
830 {
831 aes.KeySize = 256;
832 aes.Mode = CipherMode.CBC;
833 aes.Padding = PaddingMode.PKCS7;
834 aes.Key = aesKey;
835 aes.GenerateIV();
836 iv = aes.IV;
837 using (var enc = aes.CreateEncryptor(aes.Key, iv))
838 cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
839 }
r182
840 }
r232
841 catch (CryptographicException ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
842 catch (Exception ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
843
r201
844 long ticks = DateTime.UtcNow.Ticks;
845 byte[] ticksBytes = BitConverter.GetBytes(ticks);
846 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
847 int pos = 0;
848 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
849 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
850 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
851 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
852 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r182
853 byte[] hmac;
r201
854 using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput);
855 byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey));
r232
856
857 byte[] outBytes;
r182
858 using (MemoryStream ms = new MemoryStream())
r201
859 using (BinaryWriter bw = new BinaryWriter(ms))
r182
860 {
r201
861 bw.Write(magicBytes);
862 bw.Write(CurrentVersion);
863 bw.Write((byte)salt.Length);
864 bw.Write(salt);
865 bw.Write((byte)(EnableRotation ? rotation.Length : 0));
866 if (EnableRotation) bw.Write(rotation);
867 bw.Write(iv);
r182
868 byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
869 if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
r201
870 bw.Write(cipherLenBytes);
871 bw.Write(cipher);
872 bw.Write(hmac);
873 bw.Write(ticksBytes);
r232
874 outBytes = ms.ToArray();
r182
875 }
r232
876
877 string bak = filePath + ".bak";
878 try
879 {
880 if (File.Exists(filePath)) File.Copy(filePath, bak, true);
881 }
882 catch (Exception ex) { DevLog($"Backup creation failed. [Recovery: Continue with Main Write]", DevLogLevel.Warning, ex); }
883
884 try
885 {
886 File.WriteAllBytes(filePath, outBytes);
887 FileInfo fi = new FileInfo(filePath);
888 DevLog($"Save successful: {filePath} ({fi.Length} bytes)", DevLogLevel.Info);
889 }
890 catch (IOException ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
891 catch (Exception ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
892
893 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher);
894 ClearBytes(hmacInput); ClearBytes(hmac); ClearBytes(salt); ClearBytes(rotation);
895
r201
896 perfSave.Stop();
897 saveCount++;
898 totalSaveMs += perfSave.ElapsedMilliseconds;
r232
899 DevLog($"Save completed in {perfSave.ElapsedMilliseconds} ms", DevLogLevel.Info);
r182
900 }
r232
901 catch (Exception ex) { DevLog("Save operation failed (Critical Catch).", DevLogLevel.Critical, ex); }
r182
902 }
903
r201
904 private string LoadInternalGetJson()
r182
905 {
r232
906 if (IsTamperedOrDebug(out string reason))
r201
907 {
r232
908 DevLog($"Load aborted: Tamper/Debug detected - {reason}. [Defense: Abort Load]", DevLogLevel.Warning);
909 return null;
r201
910 }
r232
911
r201
912 perfLoad.Restart();
r232
913 string json = TryLoadFile(filePath, false);
914
915 if (json == null)
r201
916 {
917 string bak = filePath + ".bak";
918 if (File.Exists(bak))
919 {
r232
920 DevLog("Main file load failed. Attempting to load from backup. [Recovery: Try Backup]", DevLogLevel.Warning);
921 json = TryLoadFile(bak, true);
922
923 if (json != null)
r201
924 {
r232
925 try
926 {
927 File.Copy(bak, filePath, true);
928 DevLog("Main file restored from backup. [Recovery: Success]", DevLogLevel.Trace);
929 }
930 catch (IOException ex)
931 {
932 DevLog("Failed to restore main file from backup. [Recovery: Continue with JSON]", DevLogLevel.Warning, ex);
933 }
r201
934 }
r232
935 else
r201
936 {
r232
937 DevLog("Backup file also failed to load. [Defense: Return Null]", DevLogLevel.Error);
r201
938 }
939 }
r232
940 else
941 {
942 DevLog("No main file or backup found to load. [Defense: Return Null]", DevLogLevel.Info);
943 }
r201
944 }
r232
945
946 perfLoad.Stop();
947 if (json != null)
948 {
949 loadCount++;
950 totalLoadMs += perfLoad.ElapsedMilliseconds;
951 DevLog($"Load successful. Load time: {perfLoad.ElapsedMilliseconds} ms", DevLogLevel.Info);
952 }
953
954 return json;
955 }
956
957 private string TryLoadFile(string path, bool isBackup)
958 {
959 if (!File.Exists(path)) return null;
960 byte[] total = null;
961
r201
962 try
963 {
r232
964 total = File.ReadAllBytes(path);
965 }
966 catch (IOException ex) { DevLog($"File read failed for {path}. [Defense: Reject File]", DevLogLevel.Warning, ex); return null; }
967
968 try
969 {
r182
970 using (MemoryStream ms = new MemoryStream(total))
971 using (BinaryReader br = new BinaryReader(ms))
972 {
973 byte[] magic = br.ReadBytes(4);
r201
974 string mag = Encoding.ASCII.GetString(magic);
975 string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
r232
976
977 if (mag != expectedMagic) { DevLog($"Magic mismatch in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
978
r182
979 byte ver = br.ReadByte();
r232
980 if (ver != CurrentVersion) { DevLog($"Version mismatch in {path}: File Version {ver} != Current Version {CurrentVersion}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
981
r182
982 int saltLen = br.ReadByte();
r232
983 if (saltLen <= 0 || saltLen > 64) { DevLog($"Bad salt length ({saltLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
r182
984 byte[] salt = br.ReadBytes(saltLen);
r232
985
r182
986 int rotationLen = br.ReadByte();
987 byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
r232
988
r182
989 byte[] iv = br.ReadBytes(IvLen);
r232
990
r182
991 int cipherLen = br.ReadInt32();
992 if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
r232
993 if (cipherLen <= 0 || cipherLen > total.Length) { DevLog($"Bad cipher length ({cipherLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
r182
994 byte[] cipher = br.ReadBytes(cipherLen);
r232
995
r182
996 byte[] hmac = br.ReadBytes(HmacLen);
r201
997 byte[] ticksBytes = br.ReadBytes(8);
r232
998
r201
999 long ticks = BitConverter.ToInt64(ticksBytes, 0);
r232
1000
r201
1001 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
1002 int pos = 0;
1003 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
1004 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
1005 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
1006 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
1007 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r232
1008
r182
1009 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
1010 byte[] aesKey = keys.aesKey;
1011 byte[] hmacKey = keys.hmacKey;
r232
1012 byte[] expectedHmac;
1013 using (var h = new HMACSHA256(hmacKey)) expectedHmac = h.ComputeHash(hmacInput);
1014
1015 if (!CryptographicOperations.FixedTimeEquals(expectedHmac, hmac))
r182
1016 {
r232
1017 DevLog($"HMAC mismatch in {path} (data tamper). [Defense: Reject Load and Clear Keys]", DevLogLevel.Critical);
1018 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1019 return null;
r182
1020 }
r232
1021
r201
1022 if (EnableTimeValidation && ticks > 0)
1023 {
1024 long nowTicks = DateTime.UtcNow.Ticks;
1025 long diffSec = Math.Abs(nowTicks - ticks) / TimeSpan.TicksPerSecond;
1026 if (diffSec > MaxAllowedTimeJumpSeconds)
1027 {
r232
1028 DevLog($"Time jump detected: {diffSec} seconds > Max ({MaxAllowedTimeJumpSeconds}). [Defense: Reject Load]", DevLogLevel.Warning);
1029 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1030 return null;
1031 }
1032 }
r232
1033
r182
1034 byte[] plain;
r201
1035 try
1036 {
r232
1037 using (Aes aes = Aes.Create())
r201
1038 {
r232
1039 aes.KeySize = 256;
1040 aes.Mode = CipherMode.CBC;
1041 aes.Padding = PaddingMode.PKCS7;
1042 aes.Key = aesKey;
1043 aes.IV = iv;
1044 using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
1045 plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
r201
1046 }
1047 }
r232
1048 catch (CryptographicException ex)
r201
1049 {
r232
1050 DevLog($"AES Decryption failed in {path}. [Defense: Reject Load]", DevLogLevel.Warning, ex);
1051 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1052 return null;
1053 }
r232
1054
1055 string json = Encoding.UTF8.GetString(plain);
1056
1057 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); ClearBytes(plain);
1058 ClearBytes(iv); ClearBytes(cipher); ClearBytes(salt); ClearBytes(rotation);
1059
1060 return json;
r201
1061 }
1062 }
r232
1063 catch (EndOfStreamException ex) { DevLog($"File structure truncated in {path}. [Defense: Reject File]", DevLogLevel.Error, ex); return null; }
1064 catch (Exception ex) { DevLog($"Load processing failed (General Error) for {path}. [Defense: Reject File]", DevLogLevel.Critical, ex); return null; }
r182
1065 }
1066
r232
1067 public SecureMetrics GetDiagnosticMetrics()
r182
1068 {
r232
1069 string reason = "";
1070 bool isTampered = IsTamperedOrDebug(out reason);
1071
1072 return new SecureMetrics
r182
1073 {
r232
1074 TotalSaveTimeMs = totalSaveMs,
1075 TotalLoadTimeMs = totalLoadMs,
1076 SaveCount = saveCount,
1077 LoadCount = loadCount,
1078 SessionNonceStatus = nonceLoadedSecurely ? "SECURELY_LOADED" : "FALLBACK_USED",
1079 IsTamperingForced = ForceTamperingForTest,
1080 IsEnvironmentTampered = isTampered
1081 };
r182
1082 }
1083
r201
1084 private bool IsTamperedOrDebug(out string reason)
r182
1085 {
r201
1086 reason = "";
r232
1087
1088 if (ForceTamperingForTest) { reason = "Forced via Inspector/Dev Tools"; return true; }
r182
1089 if (!EnableHardening) return false;
r232
1090
r182
1091 try
1092 {
1093 if (EnableAntiDebugChecks)
1094 {
r232
1095 if (System.Diagnostics.Debugger.IsAttached) { reason = "Debugger Attached"; return true; }
1096 string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower();
1097 if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy")) { reason = $"ProcessName suspicious: {proc}"; return true; }
r182
1098 }
1099 }
r232
1100 catch { }
1101
r182
1102 try
1103 {
1104 if (EnableRootDetection)
1105 {
1106 string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
r232
1107 foreach (var p in suspectPaths) if (File.Exists(p)) { reason = $"Root path found: {p}"; return true; }
r182
1108 }
1109 }
r232
1110 catch { }
1111
r182
1112 return false;
1113 }
r201
1114
1115 private bool VerifyAssemblyHash(string expectedHex)
1116 {
1117 try
1118 {
1119 Assembly asm = typeof(SecureManager).Assembly;
1120 string loc = asm.Location;
1121 if (string.IsNullOrEmpty(loc)) return true;
1122 byte[] bytes = File.ReadAllBytes(loc);
1123 using (var sha = SHA256.Create())
1124 {
1125 byte[] h = sha.ComputeHash(bytes);
1126 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
1127 return hex == expectedHex.ToLowerInvariant();
1128 }
1129 }
1130 catch { return true; }
1131 }
1132
1133 private string CalculateAssemblyHashPartial(out bool success)
1134 {
1135 success = false;
1136 try
1137 {
1138 Assembly asm = typeof(SecureManager).Assembly;
1139 string loc = asm.Location;
r232
1140 if (string.IsNullOrEmpty(loc)) return "";
r201
1141 byte[] bytes = File.ReadAllBytes(loc);
1142 using (var sha = SHA256.Create())
1143 {
1144 byte[] h = sha.ComputeHash(bytes);
1145 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
1146 success = true;
1147 return hex;
1148 }
1149 }
r232
1150 catch { return ""; }
r201
1151 }
1152
r232
1153 private void DevLog(string message, DevLogLevel level = DevLogLevel.Info, Exception ex = null, [CallerMemberName] string methodName = "")
r201
1154 {
r232
1155 if (!EnableDeveloperLogging || level < DeveloperLogLevel) return;
1156
1157 string prefix = $"[SM:{level}][{methodName}] ";
r201
1158 string line = prefix + message;
r232
1159
1160 if (ex != null)
1161 {
1162 line += $" | EXCEPTION_TYPE: {ex.GetType().Name} | EXCEPTION_MSG: {ex.Message}";
1163 if (level >= DevLogLevel.Error) line += $"\n--- STACK TRACE ---\n{ex.StackTrace}";
1164 }
1165
r201
1166 try
1167 {
r232
1168 if (level == DevLogLevel.Critical) UnityEngine.Debug.LogError("[CRITICAL] " + line);
1169 else if (level == DevLogLevel.Error) UnityEngine.Debug.LogError(line);
1170 else if (level == DevLogLevel.Warning) UnityEngine.Debug.LogWarning(line);
1171 else UnityEngine.Debug.Log(line);
r201
1172 }
1173 catch { }
r232
1174
r201
1175 try
1176 {
1177 File.AppendAllText(devLogFilePath, DateTime.UtcNow.ToString("o") + " " + line + Environment.NewLine);
1178 }
1179 catch { }
1180 }
1181
r232
1182 private static byte[] GenerateRandomBytes(int len)
1183 {
1184 byte[] b = new byte[len];
1185 using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
1186 return b;
1187 }
1188
1189 private static void ClearBytes(byte[] b)
1190 {
1191 if (b == null) return;
1192 Array.Clear(b, 0, b.Length);
1193 }
1194
1195 private string ResolveFileName(string baseName)
1196 {
1197 if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
1198 byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
1199 using (var sha = SHA256.Create())
1200 {
1201 byte[] hash = sha.ComputeHash(nameBytes);
1202 string b64 = Convert.ToBase64String(hash);
1203 string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
1204 ClearBytes(hash);
1205 return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
1206 }
1207 }
1208
r201
1209 private byte[] TryProtectedProtect(byte[] data)
1210 {
1211 try
1212 {
1213#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
1214 return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
1215#else
1216 return null;
1217#endif
1218 }
1219 catch { return null; }
1220 }
1221
1222 private byte[] TryProtectedUnprotect(byte[] data)
1223 {
1224 try
1225 {
1226#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
1227 return ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser);
1228#else
1229 return null;
1230#endif
1231 }
1232 catch { return null; }
1233 }
r198
1234}
1235}}}
r231
1236====# 기능 #====
r204
1237 * 싱글톤 인스턴스 관리
1238 * 초기화
1239 * 세션 Nonce 생성
1240 * 파일 경로 생성
1241 * Warmup 호출
1242
1243 * 무작위 바이트 생성
1244 * DumbMixA/B/C
1245 * SecretBytes 생성
1246
1247 * 키 파생 (DeriveKeysRotating)
1248 * AES 키/HMAC 키 분리
1249 * Rotation 적용 (옵션)
1250
1251 * Save<T>
1252 * JSON 직렬화
1253 * 난수 Salt 생성
1254 * Rotation 바이트 생성
1255 * AES 키/HMAC 키 생성
1256 * AES 암호화 수행
1257 * HMAC 생성
1258 * 메모리 블롭 생성
1259 * 파일 저장
1260 * 메모리 정리
1261
1262 * Load<T>
1263 * 파일 존재 확인
1264 * Base64 디코딩
1265 * Magic/Version 체크
1266 * Salt/Rotation/IV/Cipher/HMAC 읽기
1267 * 키 파생
1268 * HMAC 검증
1269 * AES 복호화 수행
1270 * JSON 역직렬화
1271 * 메모리 정리
1272
1273 * 파일명 난독화 및 경로
1274 * ResolveFileName(baseName)
1275 * 최종 경로 반환(filePath)
1276
1277 * 개발자 로깅 (EnableDeveloperLogging)
1278 * 로그 레벨 관리
1279 * DevLog 함수: 콘솔 + 파일 기록
1280 * 초기화 환경 로그
1281 * 파일 경로, 백업, 파일 크기 로그
1282 * Save/Load 시간 측정 및 평균
1283 * 키 파생 옵션 로그
1284 * 보안 실패 상세 로그
1285 * 예외 메시지/스택트레이스 기록
1286 * 로그 필터링
1287
1288 * 변조·디버깅 탐지
1289 * Debugger.IsAttached
1290 * 프로세스 이름 확인
1291 * 루팅 경로 검사
1292 * 탐지 시 Save/Load 거부 및 로그 출력
1293
1294 * 어셈블리 무결성 (옵션)
1295 * ExpectedAssemblyHash 비교
1296 * DevLog 해시 일부 출력
1297
1298 * 시간 위변조 검증
1299 * Save 시 UTC ticks 저장
1300 * Load 시 ticks 비교
1301 * 시간 점프 감지 및 로그 출력
1302
1303 * 플랫폼 보호 (Windows DPAPI)
1304 * TryProtectedProtect / TryProtectedUnprotect
1305 * Windows 환경만 적용
1306
1307 * 성능/안정성 보조
1308 * Save/Load 시간 측정 및 평균
1309 * I/O 예외 시 백업 로드 시도
1310
1311 * 유틸리티 헬퍼
1312 * GenerateRandomBytes(len)
1313 * ClearBytes(byte[])
1314 * Obf.Decode
1315 * CalculateAssemblyHashPartial(out bool)
1316
1317 * 파일 버전 및 마이그레이션 자리
1318 * Version 바이트 관리
1319 * MigrateData 가능
1320
r231
1321===# 예전 코드 실행 코드 #===
r183
1322{{{#!syntax csharp
1323using UnityEngine;
1324
r185
1325/// <summary>
1326/// SecureManager의 Save 및 Load 메서드를 시연하는 예시 스크립트입니다.
1327/// </summary>
r183
1328public class ExampleUsage : MonoBehaviour
1329{
1330 private readonly string testPlayerName = "Gemini_Test_User";
1331 private readonly int initialScore = 1000;
1332 private readonly int newScore = 5000;
1333
1334 void Start()
1335 {
r185
1336 // SecureManager가 씬에 존재하는지 확인합니다.
r183
1337 if (SecureManager.Instance == null)
1338 {
1339 Debug.LogError("SecureManager 인스턴스를 찾을 수 없습니다. SecureManager.cs를 GameObject에 추가했는지 확인해주세요.");
1340 return;
1341 }
r185
1342
1343 // 1. 데이터 저장 테스트
r183
1344 SaveTestData();
1345
r185
1346 // 2. 데이터 로드 테스트
r183
1347 LoadTestData();
1348
r185
1349 // 3. 데이터 업데이트 및 재저장
r183
1350 UpdateAndSaveTestData();
r185
1351
1352 // 4. 업데이트된 데이터 로드 확인
r183
1353 LoadTestData();
1354 }
1355
1356 void SaveTestData()
1357 {
1358 Debug.Log("--- 1. 초기 데이터 저장 시도 ---");
1359 PlayerData dataToSave = new PlayerData
1360 {
1361 playerName = testPlayerName,
1362 score = initialScore
1363 };
1364
1365 SecureManager.Instance.Save(dataToSave);
1366 Debug.Log($"초기 데이터 저장 완료: Player={dataToSave.playerName}, Score={dataToSave.score}");
1367 }
1368
1369 void LoadTestData()
1370 {
1371 Debug.Log("--- 2. 데이터 로드 시도 ---");
1372 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
1373
1374 if (loadedData != null)
1375 {
1376 Debug.Log($"데이터 로드 성공: Player={loadedData.playerName}, Score={loadedData.score}");
1377 }
1378 else
1379 {
1380 Debug.LogWarning("데이터 로드 실패 (파일 없음, 오류 발생, 혹은 디버거 감지).");
1381 }
1382 }
1383
1384 void UpdateAndSaveTestData()
1385 {
1386 Debug.Log("--- 3. 데이터 업데이트 및 재저장 시도 ---");
1387 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
1388
1389 if (loadedData != null)
1390 {
1391 loadedData.score = newScore;
1392 SecureManager.Instance.Save(loadedData);
1393 Debug.Log($"데이터 업데이트 및 저장 완료: New Score={loadedData.score}");
1394 }
1395 }
1396
1397 void OnDestroy()
1398 {
r185
1399 // 애플리케이션 종료 시 디버거 체크가 활성화된 경우 에러 방지
r183
1400 if (SecureManager.Instance != null && SecureManager.Instance.EnableAntiDebugChecks)
1401 {
r187
1402 SecureManager.Instance.EnableAntiDebugChecks = false;
r183
1403 Debug.Log("SecureManager: Anti-Debug Checks가 활성화되어 있습니다.");
1404 }
1405 }
r182
1406} }}}