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