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