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