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