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