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