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