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