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