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