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