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