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