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