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