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