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