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