r243
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== 각종 코드 모음 ==
38[include(틀:GitHub 언어 통계, 언어1=C#, 언어비율1=100)] 깃허브에 올리기도 귀찮기에 여기다가 쓰기.
39
40=== 방송 역사 작성 봇 코드 ===
r230
41[include(틀:GitHub 언어 통계, 언어1=Python, 언어비율1=100)]
42
r234
43생방을 보다보면 졸린 경우가 존재한다. 인간의 3대 욕구인 수면욕을 채우기위해 만드는 것.
44
r237
45코드 예상 기능.
46
r235
47 1. 방송이 켜져있는지 확인한다.
r238
48 1. 날짜를 받는다.
r235
49 1. API로 태그를 받는다.
50 1. API로 실시간 채팅 기록을 받는다.
51 1. 일단 무조건 첫시작은 저챗이다.
52 1. 게임을 할 경우 태그의 게임이름을 받고 {gamename}을/를 하였다. 로 정한다.
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)]
r224
453오류 안고친 코드.
454
r182
455{{{#!syntax csharp
456using System;
457using System.IO;
r201
458using System.Text;
r182
459using System.Security.Cryptography;
r201
460using System.Threading.Tasks;
461using System.Reflection;
r182
462using UnityEngine;
r201
463using System.Diagnostics;
r182
464using System.Runtime.CompilerServices;
r232
465using System.Collections.Generic;
r182
466
467[Serializable]
468public class PlayerData
469{
470 public string playerName;
471 public int score;
472}
473
r201
474public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 }
475
r232
476public struct SecureMetrics
477{
478 public long TotalSaveTimeMs;
479 public long TotalLoadTimeMs;
480 public int SaveCount;
481 public int LoadCount;
482 public string SessionNonceStatus;
483 public bool IsTamperingForced;
484 public bool IsEnvironmentTampered;
485}
486
r182
487public class SecureManager : MonoBehaviour
488{
489 public static SecureManager Instance { get; private set; }
490
r232
491 [Header("보안 강화 제어")]
r182
492 public bool EnableHardening = true;
493 public bool EnableAntiDebugChecks = true;
494 public bool EnableRootDetection = true;
r201
495 public bool EnableDummyMix = true;
r197
496 public bool EnableFileNameObfuscation = true;
r182
497 public bool EnableRotation = true;
r232
498
499 [Header("개발자 진단 및 로깅")]
r201
500 public bool EnableDeveloperLogging = true;
501 public DevLogLevel DeveloperLogLevel = DevLogLevel.Info;
r232
502 [Tooltip("안티-치트 로직 테스트를 위해 강제로 Tampered 상태를 주입합니다.")]
503 public bool ForceTamperingForTest = false;
504
505 [Header("데이터 및 I/O 설정")]
r201
506 public bool EnableTimeValidation = true;
507 public long MaxAllowedTimeJumpSeconds = 3600 * 6;
r232
508 [Tooltip("비동기 I/O 사용 여부 (권장)")]
r201
509 public bool UseAsyncIO = true;
510 public byte CurrentVersion = 1;
r232
511 [Tooltip("어셈블리 해시 검증을 위한 16진수 문자열. 비어 있으면 검증하지 않습니다.")]
r201
512 public string ExpectedAssemblyHashHex = "";
r182
513
r201
514 private const int SaltLenDefault = 16;
r182
515 private const int IvLen = 16;
516 private const int HmacLen = 32;
517 private const int KeyMaterialLen = 64;
518 private const int AesKeyLen = 32;
519 private const int HmacKeyLen = 32;
520 private const int Pbkdf2Iterations = 20000;
r201
521
r182
522 private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
523 private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
524 private const byte Mask = 0xAA;
r201
525
526 private static readonly byte[] ObfMagicBytes = new byte[] { (byte)('S' ^ 0x5A ^ 0), (byte)('J' ^ 0x5A ^ 1), (byte)('M' ^ 0x5A ^ 2), (byte)('P' ^ 0x5A ^ 3) };
527 private const byte ObfMagicKey = 0x5A;
528
r182
529 private string filePath;
530 private byte[] sessionNonce;
r232
531 private bool nonceLoadedSecurely = false;
532
r201
533 private Stopwatch perfSave = new Stopwatch();
534 private Stopwatch perfLoad = new Stopwatch();
535 private long totalSaveMs = 0;
536 private long totalLoadMs = 0;
537 private int saveCount = 0;
538 private int loadCount = 0;
539 private string devLogFilePath;
r232
540
r201
541 private static class Obf
542 {
543 public static string Decode(byte[] cipher, byte key)
544 {
545 byte[] b = new byte[cipher.Length];
546 for (int i = 0; i < b.Length; i++) b[i] = (byte)(cipher[i] ^ key ^ (i & 0xFF));
547 string s = Encoding.UTF8.GetString(b);
548 Array.Clear(b, 0, b.Length);
549 return s;
550 }
551 }
552
r182
553 void Awake()
554 {
555 if (Instance != null && Instance != this)
556 {
557 Destroy(gameObject);
558 return;
559 }
560 Instance = this;
561 DontDestroyOnLoad(gameObject);
r232
562
r198
563 sessionNonce = LoadOrCreateSessionNonce();
r232
564 PrintSecurityStatus();
565
r182
566 filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
r201
567 devLogFilePath = Path.Combine(Application.persistentDataPath, "securemanager_devlog.txt");
r232
568
r201
569 if (!string.IsNullOrEmpty(ExpectedAssemblyHashHex))
570 {
571 try
572 {
573 string calc = CalculateAssemblyHashPartial(out bool ok);
r232
574 if (!ok) DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning);
r201
575 else
576 {
r232
577 if (!VerifyAssemblyHash(ExpectedAssemblyHashHex)) DevLog($"Assembly hash mismatch.", DevLogLevel.Critical);
578 else DevLog($"Assembly hash verified OK.", DevLogLevel.Info);
r201
579 }
580 }
r232
581 catch (Exception ex) { DevLog("Assembly verify failed due to exception.", DevLogLevel.Error, ex); }
r201
582 }
r232
583 DevLog("최종 데이터 경로: " + filePath, DevLogLevel.Info);
r201
584 DevLogEnvironmentSnapshot();
r182
585 }
586
r201
587 private void DevLogEnvironmentSnapshot()
588 {
589 DevLog($"Platform={Application.platform}, OS={SystemInfo.operatingSystem}, Device={SystemInfo.deviceModel}, CPU={SystemInfo.processorType}, Memory={SystemInfo.systemMemorySize}MB", DevLogLevel.Info);
590 }
r232
591
592 public void PrintSecurityStatus()
593 {
594 string status = nonceLoadedSecurely
595 ? "SUCCESS: 세션 논스 안전하게 로드됨 (ProtectedData/Native)."
596 : "WARNING: 세션 논스가 OS 보호 없이 저장됨 (초기 생성 또는 폴백 사용).";
597
598 DevLog($"--- 보안 상태 보고서 ---", DevLogLevel.Info);
599 DevLog($"[논스 상태] {status}", nonceLoadedSecurely ? DevLogLevel.Info : DevLogLevel.Warning);
600 DevLog($"[강화 설정] 활성: {EnableHardening}, 디버그 방어: {EnableAntiDebugChecks}, 루팅 감지: {EnableRootDetection}", DevLogLevel.Info);
601 DevLog($"[변조 강제] 강제 테스트 상태: {ForceTamperingForTest}", ForceTamperingForTest ? DevLogLevel.Warning : DevLogLevel.Info);
602 DevLog($"----------------------------", DevLogLevel.Info);
603 }
r201
604
r198
605 private byte[] LoadOrCreateSessionNonce()
606 {
r201
607 const string key = "SecureManager_SessionNonce_v4";
r232
608 byte[] newNonce = GenerateRandomBytes(16);
609
r198
610 try
611 {
612 if (PlayerPrefs.HasKey(key))
613 {
614 string b64 = PlayerPrefs.GetString(key);
r201
615 if (!string.IsNullOrEmpty(b64))
616 {
617 try
618 {
619 byte[] enc = Convert.FromBase64String(b64);
620 byte[] dec = TryProtectedUnprotect(enc);
r232
621
622 if (dec != null && dec.Length > 0)
623 {
624 nonceLoadedSecurely = true;
625 DevLog("Session Nonce successfully loaded and unprotected. [Recovery: Success]", DevLogLevel.Trace);
626 return dec;
627 }
628
629 DevLog("ProtectedData Unprotect returned null or failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning);
630 throw new CryptographicException("DPAPI/OS protection failed.");
r201
631 }
r232
632 catch (FormatException ex) { DevLog("Failed to decode session nonce from Base64. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
633 catch (CryptographicException ex) { DevLog("ProtectedData unprotect failed. [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
634 catch (Exception ex) { DevLog("Unexpected error during Session Nonce loading/unprotecting.", DevLogLevel.Error, ex); }
r201
635 }
r198
636 }
637 }
r232
638 catch (Exception ex) { DevLog("Load session nonce failed (PlayerPrefs read error). [Recovery: Regenerate Nonce]", DevLogLevel.Warning, ex); }
639
640 nonceLoadedSecurely = false;
641 DevLog("Creating new Session Nonce (Fallback/Initial).", DevLogLevel.Info);
642
r198
643 try
644 {
r232
645 byte[] protectedBytes = TryProtectedProtect(newNonce);
646 string toStore = protectedBytes != null
647 ? Convert.ToBase64String(protectedBytes)
648 : Convert.ToBase64String(newNonce);
649
r201
650 PlayerPrefs.SetString(key, toStore);
r198
651 PlayerPrefs.Save();
r232
652 if (protectedBytes == null) DevLog("New nonce saved without OS protection. [Recovery: Fallback to PlayerPrefs]", DevLogLevel.Warning);
653 else DevLog("New nonce saved with OS protection.", DevLogLevel.Trace);
r198
654 }
r232
655 catch (Exception ex) { DevLog("Save new session nonce failed. [Defense: Use in-memory nonce]", DevLogLevel.Critical, ex); }
656 return newNonce;
r198
657 }
658
r182
659 [MethodImpl(MethodImplOptions.NoInlining)]
660 private byte[] DumbMixA(byte[] input)
661 {
r201
662 if (input == null) return new byte[0];
r182
663 byte[] outb = new byte[input.Length];
664 for (int i = 0; i < input.Length; i++)
665 {
666 int v = input[i];
667 v = ((v << 3) | (v >> 5)) & 0xFF;
668 v ^= (i * 37) & 0xFF;
669 outb[i] = (byte)v;
670 }
671 return outb;
672 }
673
674 [MethodImpl(MethodImplOptions.NoInlining)]
675 private byte[] DumbMixB(byte[] input, int seed)
676 {
r201
677 if (input == null) return new byte[0];
r182
678 byte[] outb = new byte[input.Length];
679 for (int i = 0; i < input.Length; i++)
680 {
681 int v = input[i] ^ (seed >> (i % 8));
682 v = (v * 13 + (i * 7)) & 0xFF;
683 v = (v ^ 0x5A) & 0xFF;
684 outb[i] = (byte)v;
685 }
686 return outb;
687 }
688
689 [MethodImpl(MethodImplOptions.NoInlining)]
690 private byte[] DumbMixC(byte[] input, byte[] nonce)
691 {
r201
692 if (input == null) return new byte[0];
693 if (nonce == null || nonce.Length == 0) return (byte[])input.Clone();
r182
694 byte[] outb = new byte[input.Length];
695 for (int i = 0; i < input.Length; i++)
696 {
697 int n = nonce[i % nonce.Length];
698 int v = input[i];
699 v = ((v + n) ^ (n >> (i % 4))) & 0xFF;
700 outb[i] = (byte)v;
701 }
702 return outb;
703 }
704
705 private byte[] GetSecretBytes()
706 {
707 byte[] k = new byte[obfPartA.Length + obfPartB.Length];
708 for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
709 for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
710 if (!EnableHardening || !EnableDummyMix)
711 {
712 byte[] simple = new byte[k.Length];
713 Buffer.BlockCopy(k, 0, simple, 0, k.Length);
714 Array.Clear(k, 0, k.Length);
715 return simple;
716 }
717 byte[] s1 = DumbMixA(k);
r201
718 int deviceSeed = 0;
719 try { deviceSeed = Application.identifier.GetHashCode() ^ (SystemInfo.deviceUniqueIdentifier?.GetHashCode() ?? 0); } catch { deviceSeed = Application.identifier.GetHashCode(); }
r198
720 byte[] s2 = DumbMixB(s1, deviceSeed);
r182
721 byte[] s3 = DumbMixC(s2, sessionNonce);
722 for (int r = 0; r < 2; r++)
723 {
724 byte[] t = DumbMixA(s3);
r198
725 byte[] u = DumbMixB(t, deviceSeed ^ r);
r182
726 for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]);
727 Array.Clear(t, 0, t.Length);
728 Array.Clear(u, 0, u.Length);
729 }
730 Array.Clear(s1, 0, s1.Length);
731 Array.Clear(s2, 0, s2.Length);
732 byte[] result = new byte[k.Length];
733 for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
734 Array.Clear(k, 0, k.Length);
735 Array.Clear(s3, 0, s3.Length);
736 return result;
737 }
738
739 private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce)
740 {
741 byte[] secret = GetSecretBytes();
742 string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
743 Array.Clear(secret, 0, secret.Length);
r232
744 try
r182
745 {
r232
746 using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
r182
747 {
r232
748 byte[] km = kdf.GetBytes(KeyMaterialLen);
749 if (EnableHardening && EnableRotation && rotationNonce != null)
750 {
751 for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
752 }
753 byte[] aesKey = new byte[AesKeyLen];
754 byte[] hmacKey = new byte[HmacKeyLen];
755 Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
756 Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
757 Array.Clear(km, 0, km.Length);
758 return (aesKey, hmacKey);
r182
759 }
760 }
r232
761 catch (Exception ex)
762 {
763 DevLog("Key derivation failed. [Defense: Throw]", DevLogLevel.Critical, ex);
764 throw new InvalidOperationException("Key derivation failed, cannot proceed with crypto operations.", ex);
765 }
r182
766 }
767
r201
768 public Task SaveAsync<T>(T data) where T : class
769 {
r232
770 if (!UseAsyncIO)
771 {
772 string json = JsonUtility.ToJson(data);
773 SaveInternalFromJson(json);
774 return Task.CompletedTask;
775 }
776
r201
777 string json = JsonUtility.ToJson(data);
778 return Task.Run(() => SaveInternalFromJson(json));
779 }
780
781 public async Task<T> LoadAsync<T>() where T : class
782 {
r232
783 string json = null;
784
785 if (!UseAsyncIO) json = LoadInternalGetJson();
786 else json = await Task.Run(() => LoadInternalGetJson());
787
r201
788 if (json == null) return default;
789 T obj = null;
r182
790 try
791 {
r201
792 obj = JsonUtility.FromJson<T>(json);
793 }
r232
794 catch (ArgumentException ex)
795 {
796 DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
797 return default;
798 }
r201
799 catch (Exception ex)
800 {
r232
801 DevLog($"JSON deserialization failed for type {typeof(T).Name}. [Defense: Return Default]", DevLogLevel.Error, ex);
r201
802 return default;
803 }
804 return obj;
805 }
806
807 private void SaveInternalFromJson(string json)
808 {
r232
809 if (IsTamperedOrDebug(out string reason))
r201
810 {
r232
811 DevLog($"Save aborted: Tamper/Debug detected - {reason}. [Defense: Abort Save]", DevLogLevel.Warning);
812 return;
r201
813 }
814 perfSave.Restart();
815 try
816 {
r182
817 byte[] salt = GenerateRandomBytes(SaltLenDefault);
818 byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
819 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
820 byte[] aesKey = keys.aesKey;
821 byte[] hmacKey = keys.hmacKey;
822 byte[] plain = Encoding.UTF8.GetBytes(json);
823 byte[] iv;
824 byte[] cipher;
r232
825
826 try
r182
827 {
r232
828 using (Aes aes = Aes.Create())
829 {
830 aes.KeySize = 256;
831 aes.Mode = CipherMode.CBC;
832 aes.Padding = PaddingMode.PKCS7;
833 aes.Key = aesKey;
834 aes.GenerateIV();
835 iv = aes.IV;
836 using (var enc = aes.CreateEncryptor(aes.Key, iv))
837 cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
838 }
r182
839 }
r232
840 catch (CryptographicException ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
841 catch (Exception ex) { DevLog("AES Encryption failed. [Defense: Abort Save]", DevLogLevel.Critical, ex); return; }
842
r201
843 long ticks = DateTime.UtcNow.Ticks;
844 byte[] ticksBytes = BitConverter.GetBytes(ticks);
845 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
846 int pos = 0;
847 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
848 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
849 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
850 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
851 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r182
852 byte[] hmac;
r201
853 using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput);
854 byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey));
r232
855
856 byte[] outBytes;
r182
857 using (MemoryStream ms = new MemoryStream())
r201
858 using (BinaryWriter bw = new BinaryWriter(ms))
r182
859 {
r201
860 bw.Write(magicBytes);
861 bw.Write(CurrentVersion);
862 bw.Write((byte)salt.Length);
863 bw.Write(salt);
864 bw.Write((byte)(EnableRotation ? rotation.Length : 0));
865 if (EnableRotation) bw.Write(rotation);
866 bw.Write(iv);
r182
867 byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
868 if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
r201
869 bw.Write(cipherLenBytes);
870 bw.Write(cipher);
871 bw.Write(hmac);
872 bw.Write(ticksBytes);
r232
873 outBytes = ms.ToArray();
r182
874 }
r232
875
876 string bak = filePath + ".bak";
877 try
878 {
879 if (File.Exists(filePath)) File.Copy(filePath, bak, true);
880 }
881 catch (Exception ex) { DevLog($"Backup creation failed. [Recovery: Continue with Main Write]", DevLogLevel.Warning, ex); }
882
883 try
884 {
885 File.WriteAllBytes(filePath, outBytes);
886 FileInfo fi = new FileInfo(filePath);
887 DevLog($"Save successful: {filePath} ({fi.Length} bytes)", DevLogLevel.Info);
888 }
889 catch (IOException ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
890 catch (Exception ex) { DevLog($"File write failed to {filePath}. [Defense: Throw]", DevLogLevel.Critical, ex); throw; }
891
892 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(plain); ClearBytes(iv); ClearBytes(cipher);
893 ClearBytes(hmacInput); ClearBytes(hmac); ClearBytes(salt); ClearBytes(rotation);
894
r201
895 perfSave.Stop();
896 saveCount++;
897 totalSaveMs += perfSave.ElapsedMilliseconds;
r232
898 DevLog($"Save completed in {perfSave.ElapsedMilliseconds} ms", DevLogLevel.Info);
r182
899 }
r232
900 catch (Exception ex) { DevLog("Save operation failed (Critical Catch).", DevLogLevel.Critical, ex); }
r182
901 }
902
r201
903 private string LoadInternalGetJson()
r182
904 {
r232
905 if (IsTamperedOrDebug(out string reason))
r201
906 {
r232
907 DevLog($"Load aborted: Tamper/Debug detected - {reason}. [Defense: Abort Load]", DevLogLevel.Warning);
908 return null;
r201
909 }
r232
910
r201
911 perfLoad.Restart();
r232
912 string json = TryLoadFile(filePath, false);
913
914 if (json == null)
r201
915 {
916 string bak = filePath + ".bak";
917 if (File.Exists(bak))
918 {
r232
919 DevLog("Main file load failed. Attempting to load from backup. [Recovery: Try Backup]", DevLogLevel.Warning);
920 json = TryLoadFile(bak, true);
921
922 if (json != null)
r201
923 {
r232
924 try
925 {
926 File.Copy(bak, filePath, true);
927 DevLog("Main file restored from backup. [Recovery: Success]", DevLogLevel.Trace);
928 }
929 catch (IOException ex)
930 {
931 DevLog("Failed to restore main file from backup. [Recovery: Continue with JSON]", DevLogLevel.Warning, ex);
932 }
r201
933 }
r232
934 else
r201
935 {
r232
936 DevLog("Backup file also failed to load. [Defense: Return Null]", DevLogLevel.Error);
r201
937 }
938 }
r232
939 else
940 {
941 DevLog("No main file or backup found to load. [Defense: Return Null]", DevLogLevel.Info);
942 }
r201
943 }
r232
944
945 perfLoad.Stop();
946 if (json != null)
947 {
948 loadCount++;
949 totalLoadMs += perfLoad.ElapsedMilliseconds;
950 DevLog($"Load successful. Load time: {perfLoad.ElapsedMilliseconds} ms", DevLogLevel.Info);
951 }
952
953 return json;
954 }
955
956 private string TryLoadFile(string path, bool isBackup)
957 {
958 if (!File.Exists(path)) return null;
959 byte[] total = null;
960
r201
961 try
962 {
r232
963 total = File.ReadAllBytes(path);
964 }
965 catch (IOException ex) { DevLog($"File read failed for {path}. [Defense: Reject File]", DevLogLevel.Warning, ex); return null; }
966
967 try
968 {
r182
969 using (MemoryStream ms = new MemoryStream(total))
970 using (BinaryReader br = new BinaryReader(ms))
971 {
972 byte[] magic = br.ReadBytes(4);
r201
973 string mag = Encoding.ASCII.GetString(magic);
974 string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
r232
975
976 if (mag != expectedMagic) { DevLog($"Magic mismatch in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
977
r182
978 byte ver = br.ReadByte();
r232
979 if (ver != CurrentVersion) { DevLog($"Version mismatch in {path}: File Version {ver} != Current Version {CurrentVersion}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
980
r182
981 int saltLen = br.ReadByte();
r232
982 if (saltLen <= 0 || saltLen > 64) { DevLog($"Bad salt length ({saltLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
r182
983 byte[] salt = br.ReadBytes(saltLen);
r232
984
r182
985 int rotationLen = br.ReadByte();
986 byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
r232
987
r182
988 byte[] iv = br.ReadBytes(IvLen);
r232
989
r182
990 int cipherLen = br.ReadInt32();
991 if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
r232
992 if (cipherLen <= 0 || cipherLen > total.Length) { DevLog($"Bad cipher length ({cipherLen}) in {path}. [Defense: Reject Load]", DevLogLevel.Warning); return null; }
r182
993 byte[] cipher = br.ReadBytes(cipherLen);
r232
994
r182
995 byte[] hmac = br.ReadBytes(HmacLen);
r201
996 byte[] ticksBytes = br.ReadBytes(8);
r232
997
r201
998 long ticks = BitConverter.ToInt64(ticksBytes, 0);
r232
999
r201
1000 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
1001 int pos = 0;
1002 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
1003 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
1004 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
1005 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
1006 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r232
1007
r182
1008 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
1009 byte[] aesKey = keys.aesKey;
1010 byte[] hmacKey = keys.hmacKey;
r232
1011 byte[] expectedHmac;
1012 using (var h = new HMACSHA256(hmacKey)) expectedHmac = h.ComputeHash(hmacInput);
1013
1014 if (!CryptographicOperations.FixedTimeEquals(expectedHmac, hmac))
r182
1015 {
r232
1016 DevLog($"HMAC mismatch in {path} (data tamper). [Defense: Reject Load and Clear Keys]", DevLogLevel.Critical);
1017 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1018 return null;
r182
1019 }
r232
1020
r201
1021 if (EnableTimeValidation && ticks > 0)
1022 {
1023 long nowTicks = DateTime.UtcNow.Ticks;
1024 long diffSec = Math.Abs(nowTicks - ticks) / TimeSpan.TicksPerSecond;
1025 if (diffSec > MaxAllowedTimeJumpSeconds)
1026 {
r232
1027 DevLog($"Time jump detected: {diffSec} seconds > Max ({MaxAllowedTimeJumpSeconds}). [Defense: Reject Load]", DevLogLevel.Warning);
1028 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1029 return null;
1030 }
1031 }
r232
1032
r182
1033 byte[] plain;
r201
1034 try
1035 {
r232
1036 using (Aes aes = Aes.Create())
r201
1037 {
r232
1038 aes.KeySize = 256;
1039 aes.Mode = CipherMode.CBC;
1040 aes.Padding = PaddingMode.PKCS7;
1041 aes.Key = aesKey;
1042 aes.IV = iv;
1043 using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
1044 plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
r201
1045 }
1046 }
r232
1047 catch (CryptographicException ex)
r201
1048 {
r232
1049 DevLog($"AES Decryption failed in {path}. [Defense: Reject Load]", DevLogLevel.Warning, ex);
1050 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac);
r201
1051 return null;
1052 }
r232
1053
1054 string json = Encoding.UTF8.GetString(plain);
1055
1056 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expectedHmac); ClearBytes(plain);
1057 ClearBytes(iv); ClearBytes(cipher); ClearBytes(salt); ClearBytes(rotation);
1058
1059 return json;
r201
1060 }
1061 }
r232
1062 catch (EndOfStreamException ex) { DevLog($"File structure truncated in {path}. [Defense: Reject File]", DevLogLevel.Error, ex); return null; }
1063 catch (Exception ex) { DevLog($"Load processing failed (General Error) for {path}. [Defense: Reject File]", DevLogLevel.Critical, ex); return null; }
r182
1064 }
1065
r232
1066 public SecureMetrics GetDiagnosticMetrics()
r182
1067 {
r232
1068 string reason = "";
1069 bool isTampered = IsTamperedOrDebug(out reason);
1070
1071 return new SecureMetrics
r182
1072 {
r232
1073 TotalSaveTimeMs = totalSaveMs,
1074 TotalLoadTimeMs = totalLoadMs,
1075 SaveCount = saveCount,
1076 LoadCount = loadCount,
1077 SessionNonceStatus = nonceLoadedSecurely ? "SECURELY_LOADED" : "FALLBACK_USED",
1078 IsTamperingForced = ForceTamperingForTest,
1079 IsEnvironmentTampered = isTampered
1080 };
r182
1081 }
1082
r201
1083 private bool IsTamperedOrDebug(out string reason)
r182
1084 {
r201
1085 reason = "";
r232
1086
1087 if (ForceTamperingForTest) { reason = "Forced via Inspector/Dev Tools"; return true; }
r182
1088 if (!EnableHardening) return false;
r232
1089
r182
1090 try
1091 {
1092 if (EnableAntiDebugChecks)
1093 {
r232
1094 if (System.Diagnostics.Debugger.IsAttached) { reason = "Debugger Attached"; return true; }
1095 string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower();
1096 if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy")) { reason = $"ProcessName suspicious: {proc}"; return true; }
r182
1097 }
1098 }
r232
1099 catch { }
1100
r182
1101 try
1102 {
1103 if (EnableRootDetection)
1104 {
1105 string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
r232
1106 foreach (var p in suspectPaths) if (File.Exists(p)) { reason = $"Root path found: {p}"; return true; }
r182
1107 }
1108 }
r232
1109 catch { }
1110
r182
1111 return false;
1112 }
r201
1113
1114 private bool VerifyAssemblyHash(string expectedHex)
1115 {
1116 try
1117 {
1118 Assembly asm = typeof(SecureManager).Assembly;
1119 string loc = asm.Location;
1120 if (string.IsNullOrEmpty(loc)) return true;
1121 byte[] bytes = File.ReadAllBytes(loc);
1122 using (var sha = SHA256.Create())
1123 {
1124 byte[] h = sha.ComputeHash(bytes);
1125 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
1126 return hex == expectedHex.ToLowerInvariant();
1127 }
1128 }
1129 catch { return true; }
1130 }
1131
1132 private string CalculateAssemblyHashPartial(out bool success)
1133 {
1134 success = false;
1135 try
1136 {
1137 Assembly asm = typeof(SecureManager).Assembly;
1138 string loc = asm.Location;
r232
1139 if (string.IsNullOrEmpty(loc)) return "";
r201
1140 byte[] bytes = File.ReadAllBytes(loc);
1141 using (var sha = SHA256.Create())
1142 {
1143 byte[] h = sha.ComputeHash(bytes);
1144 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
1145 success = true;
1146 return hex;
1147 }
1148 }
r232
1149 catch { return ""; }
r201
1150 }
1151
r232
1152 private void DevLog(string message, DevLogLevel level = DevLogLevel.Info, Exception ex = null, [CallerMemberName] string methodName = "")
r201
1153 {
r232
1154 if (!EnableDeveloperLogging || level < DeveloperLogLevel) return;
1155
1156 string prefix = $"[SM:{level}][{methodName}] ";
r201
1157 string line = prefix + message;
r232
1158
1159 if (ex != null)
1160 {
1161 line += $" | EXCEPTION_TYPE: {ex.GetType().Name} | EXCEPTION_MSG: {ex.Message}";
1162 if (level >= DevLogLevel.Error) line += $"\n--- STACK TRACE ---\n{ex.StackTrace}";
1163 }
1164
r201
1165 try
1166 {
r232
1167 if (level == DevLogLevel.Critical) UnityEngine.Debug.LogError("[CRITICAL] " + line);
1168 else if (level == DevLogLevel.Error) UnityEngine.Debug.LogError(line);
1169 else if (level == DevLogLevel.Warning) UnityEngine.Debug.LogWarning(line);
1170 else UnityEngine.Debug.Log(line);
r201
1171 }
1172 catch { }
r232
1173
r201
1174 try
1175 {
1176 File.AppendAllText(devLogFilePath, DateTime.UtcNow.ToString("o") + " " + line + Environment.NewLine);
1177 }
1178 catch { }
1179 }
1180
r232
1181 private static byte[] GenerateRandomBytes(int len)
1182 {
1183 byte[] b = new byte[len];
1184 using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
1185 return b;
1186 }
1187
1188 private static void ClearBytes(byte[] b)
1189 {
1190 if (b == null) return;
1191 Array.Clear(b, 0, b.Length);
1192 }
1193
1194 private string ResolveFileName(string baseName)
1195 {
1196 if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
1197 byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
1198 using (var sha = SHA256.Create())
1199 {
1200 byte[] hash = sha.ComputeHash(nameBytes);
1201 string b64 = Convert.ToBase64String(hash);
1202 string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
1203 ClearBytes(hash);
1204 return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
1205 }
1206 }
1207
r201
1208 private byte[] TryProtectedProtect(byte[] data)
1209 {
1210 try
1211 {
1212#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
1213 return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
1214#else
1215 return null;
1216#endif
1217 }
1218 catch { return null; }
1219 }
1220
1221 private byte[] TryProtectedUnprotect(byte[] data)
1222 {
1223 try
1224 {
1225#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
1226 return ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser);
1227#else
1228 return null;
1229#endif
1230 }
1231 catch { return null; }
1232 }
r198
1233}
1234}}}
r231
1235====# 기능 #====
r204
1236 * 싱글톤 인스턴스 관리
1237 * 초기화
1238 * 세션 Nonce 생성
1239 * 파일 경로 생성
1240 * Warmup 호출
1241
1242 * 무작위 바이트 생성
1243 * DumbMixA/B/C
1244 * SecretBytes 생성
1245
1246 * 키 파생 (DeriveKeysRotating)
1247 * AES 키/HMAC 키 분리
1248 * Rotation 적용 (옵션)
1249
1250 * Save<T>
1251 * JSON 직렬화
1252 * 난수 Salt 생성
1253 * Rotation 바이트 생성
1254 * AES 키/HMAC 키 생성
1255 * AES 암호화 수행
1256 * HMAC 생성
1257 * 메모리 블롭 생성
1258 * 파일 저장
1259 * 메모리 정리
1260
1261 * Load<T>
1262 * 파일 존재 확인
1263 * Base64 디코딩
1264 * Magic/Version 체크
1265 * Salt/Rotation/IV/Cipher/HMAC 읽기
1266 * 키 파생
1267 * HMAC 검증
1268 * AES 복호화 수행
1269 * JSON 역직렬화
1270 * 메모리 정리
1271
1272 * 파일명 난독화 및 경로
1273 * ResolveFileName(baseName)
1274 * 최종 경로 반환(filePath)
1275
1276 * 개발자 로깅 (EnableDeveloperLogging)
1277 * 로그 레벨 관리
1278 * DevLog 함수: 콘솔 + 파일 기록
1279 * 초기화 환경 로그
1280 * 파일 경로, 백업, 파일 크기 로그
1281 * Save/Load 시간 측정 및 평균
1282 * 키 파생 옵션 로그
1283 * 보안 실패 상세 로그
1284 * 예외 메시지/스택트레이스 기록
1285 * 로그 필터링
1286
1287 * 변조·디버깅 탐지
1288 * Debugger.IsAttached
1289 * 프로세스 이름 확인
1290 * 루팅 경로 검사
1291 * 탐지 시 Save/Load 거부 및 로그 출력
1292
1293 * 어셈블리 무결성 (옵션)
1294 * ExpectedAssemblyHash 비교
1295 * DevLog 해시 일부 출력
1296
1297 * 시간 위변조 검증
1298 * Save 시 UTC ticks 저장
1299 * Load 시 ticks 비교
1300 * 시간 점프 감지 및 로그 출력
1301
1302 * 플랫폼 보호 (Windows DPAPI)
1303 * TryProtectedProtect / TryProtectedUnprotect
1304 * Windows 환경만 적용
1305
1306 * 성능/안정성 보조
1307 * Save/Load 시간 측정 및 평균
1308 * I/O 예외 시 백업 로드 시도
1309
1310 * 유틸리티 헬퍼
1311 * GenerateRandomBytes(len)
1312 * ClearBytes(byte[])
1313 * Obf.Decode
1314 * CalculateAssemblyHashPartial(out bool)
1315
1316 * 파일 버전 및 마이그레이션 자리
1317 * Version 바이트 관리
1318 * MigrateData 가능
1319
r231
1320===# 예전 코드 실행 코드 #===
r183
1321{{{#!syntax csharp
1322using UnityEngine;
1323
r185
1324/// <summary>
1325/// SecureManager의 Save 및 Load 메서드를 시연하는 예시 스크립트입니다.
1326/// </summary>
r183
1327public class ExampleUsage : MonoBehaviour
1328{
1329 private readonly string testPlayerName = "Gemini_Test_User";
1330 private readonly int initialScore = 1000;
1331 private readonly int newScore = 5000;
1332
1333 void Start()
1334 {
r185
1335 // SecureManager가 씬에 존재하는지 확인합니다.
r183
1336 if (SecureManager.Instance == null)
1337 {
1338 Debug.LogError("SecureManager 인스턴스를 찾을 수 없습니다. SecureManager.cs를 GameObject에 추가했는지 확인해주세요.");
1339 return;
1340 }
r185
1341
1342 // 1. 데이터 저장 테스트
r183
1343 SaveTestData();
1344
r185
1345 // 2. 데이터 로드 테스트
r183
1346 LoadTestData();
1347
r185
1348 // 3. 데이터 업데이트 및 재저장
r183
1349 UpdateAndSaveTestData();
r185
1350
1351 // 4. 업데이트된 데이터 로드 확인
r183
1352 LoadTestData();
1353 }
1354
1355 void SaveTestData()
1356 {
1357 Debug.Log("--- 1. 초기 데이터 저장 시도 ---");
1358 PlayerData dataToSave = new PlayerData
1359 {
1360 playerName = testPlayerName,
1361 score = initialScore
1362 };
1363
1364 SecureManager.Instance.Save(dataToSave);
1365 Debug.Log($"초기 데이터 저장 완료: Player={dataToSave.playerName}, Score={dataToSave.score}");
1366 }
1367
1368 void LoadTestData()
1369 {
1370 Debug.Log("--- 2. 데이터 로드 시도 ---");
1371 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
1372
1373 if (loadedData != null)
1374 {
1375 Debug.Log($"데이터 로드 성공: Player={loadedData.playerName}, Score={loadedData.score}");
1376 }
1377 else
1378 {
1379 Debug.LogWarning("데이터 로드 실패 (파일 없음, 오류 발생, 혹은 디버거 감지).");
1380 }
1381 }
1382
1383 void UpdateAndSaveTestData()
1384 {
1385 Debug.Log("--- 3. 데이터 업데이트 및 재저장 시도 ---");
1386 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
1387
1388 if (loadedData != null)
1389 {
1390 loadedData.score = newScore;
1391 SecureManager.Instance.Save(loadedData);
1392 Debug.Log($"데이터 업데이트 및 저장 완료: New Score={loadedData.score}");
1393 }
1394 }
1395
1396 void OnDestroy()
1397 {
r185
1398 // 애플리케이션 종료 시 디버거 체크가 활성화된 경우 에러 방지
r183
1399 if (SecureManager.Instance != null && SecureManager.Instance.EnableAntiDebugChecks)
1400 {
r187
1401 SecureManager.Instance.EnableAntiDebugChecks = false;
r183
1402 Debug.Log("SecureManager: Anti-Debug Checks가 활성화되어 있습니다.");
1403 }
1404 }
r182
1405} }}}