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