r205
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 키기 귀찮아서 나중에 고치려고 여기가 적는게 맞습니다.
r201
58=== 본 코드 ===
r182
59{{{#!syntax csharp
60using System;
61using System.IO;
r201
62using System.Text;
r182
63using System.Security.Cryptography;
r201
64using System.Threading.Tasks;
65using System.Reflection;
r182
66using UnityEngine;
r201
67using System.Diagnostics;
r182
68using System.Runtime.CompilerServices;
69
70[Serializable]
71public class PlayerData
72{
73 public string playerName;
74 public int score;
75}
76
r201
77public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 }
78
r182
79public class SecureManager : MonoBehaviour
80{
81 public static SecureManager Instance { get; private set; }
82
83 public bool EnableHardening = true;
84 public bool EnableAntiDebugChecks = true;
85 public bool EnableRootDetection = true;
r201
86 public bool EnableDummyMix = true;
r197
87 public bool EnableFileNameObfuscation = true;
r182
88 public bool EnableRotation = true;
r201
89 public bool EnableDeveloperLogging = true;
90 public DevLogLevel DeveloperLogLevel = DevLogLevel.Info;
91 public bool EnableTimeValidation = true;
92 public long MaxAllowedTimeJumpSeconds = 3600 * 6;
93 public bool UseAsyncIO = true;
94 public byte CurrentVersion = 1;
95 public string ExpectedAssemblyHashHex = "";
r182
96
r201
97 private const int SaltLenDefault = 16;
r182
98 private const int IvLen = 16;
99 private const int HmacLen = 32;
100 private const int KeyMaterialLen = 64;
101 private const int AesKeyLen = 32;
102 private const int HmacKeyLen = 32;
103 private const int Pbkdf2Iterations = 20000;
r201
104
r182
105 private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
106 private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
107 private const byte Mask = 0xAA;
r201
108
109 private static readonly byte[] ObfMagicBytes = new byte[] { (byte)('S' ^ 0x5A ^ 0), (byte)('J' ^ 0x5A ^ 1), (byte)('M' ^ 0x5A ^ 2), (byte)('P' ^ 0x5A ^ 3) };
110 private const byte ObfMagicKey = 0x5A;
111
r182
112 private string filePath;
113 private byte[] sessionNonce;
r201
114 private Stopwatch perfSave = new Stopwatch();
115 private Stopwatch perfLoad = new Stopwatch();
116 private long totalSaveMs = 0;
117 private long totalLoadMs = 0;
118 private int saveCount = 0;
119 private int loadCount = 0;
120 private string devLogFilePath;
r182
121
r201
122 private static class Obf
123 {
124 public static string Decode(byte[] cipher, byte key)
125 {
126 byte[] b = new byte[cipher.Length];
127 for (int i = 0; i < b.Length; i++) b[i] = (byte)(cipher[i] ^ key ^ (i & 0xFF));
128 string s = Encoding.UTF8.GetString(b);
129 Array.Clear(b, 0, b.Length);
130 return s;
131 }
132 }
133
r182
134 void Awake()
135 {
136 if (Instance != null && Instance != this)
137 {
138 Destroy(gameObject);
139 return;
140 }
141 Instance = this;
142 DontDestroyOnLoad(gameObject);
r198
143 sessionNonce = LoadOrCreateSessionNonce();
r182
144 filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
r201
145 devLogFilePath = Path.Combine(Application.persistentDataPath, "securemanager_devlog.txt");
146 if (!string.IsNullOrEmpty(ExpectedAssemblyHashHex))
147 {
148 try
149 {
150 string calc = CalculateAssemblyHashPartial(out bool ok);
151 if (!ok)
152 {
153 DevLog("Assembly hash verification not applicable on this platform.", DevLogLevel.Warning);
154 }
155 else
156 {
157 string exp = ExpectedAssemblyHashHex.ToLowerInvariant();
158 string calcLow = calc.ToLowerInvariant();
159 if (exp.Length >= 8 && calcLow.Length >= 8)
160 DevLog($"Assembly hash compare: Expected={exp.Substring(0, Math.Min(8, exp.Length))}..., Calculated={calcLow.Substring(0, 8)}...", DevLogLevel.Info);
161 if (!VerifyAssemblyHash(ExpectedAssemblyHashHex))
162 {
163 DevLog($"Assembly hash mismatch: Expected={ExpectedAssemblyHashHex.Substring(0, Math.Min(8, ExpectedAssemblyHashHex.Length))}..., Calculated={calc.Substring(0, Math.Min(8, calc.Length))}...", DevLogLevel.Warning);
164 }
165 }
166 }
167 catch (Exception ex)
168 {
169 DevLog("Assembly verify exception: " + ex.ToString(), DevLogLevel.Warning);
170 }
171 }
172 DevLog("Final data path: " + filePath, DevLogLevel.Info);
173 DevLogEnvironmentSnapshot();
r182
174 }
175
r201
176 private void DevLogEnvironmentSnapshot()
177 {
178 DevLog($"Platform={Application.platform}, OS={SystemInfo.operatingSystem}, Device={SystemInfo.deviceModel}, CPU={SystemInfo.processorType}, Memory={SystemInfo.systemMemorySize}MB", DevLogLevel.Info);
179 }
180
r198
181 private byte[] LoadOrCreateSessionNonce()
182 {
r201
183 const string key = "SecureManager_SessionNonce_v4";
r198
184 try
185 {
186 if (PlayerPrefs.HasKey(key))
187 {
188 string b64 = PlayerPrefs.GetString(key);
r201
189 if (!string.IsNullOrEmpty(b64))
190 {
191 try
192 {
193 byte[] enc = Convert.FromBase64String(b64);
194 byte[] dec = TryProtectedUnprotect(enc);
195 if (dec != null && dec.Length > 0) return dec;
196 return enc;
197 }
198 catch { }
199 }
r198
200 }
201 }
r201
202 catch (Exception ex)
203 {
204 DevLog("Load session nonce failed: " + ex.Message, DevLogLevel.Warning);
205 }
r198
206 byte[] nonce = GenerateRandomBytes(16);
207 try
208 {
r201
209 byte[] protectedBytes = TryProtectedProtect(nonce);
210 string toStore = protectedBytes != null ? Convert.ToBase64String(protectedBytes) : Convert.ToBase64String(nonce);
211 PlayerPrefs.SetString(key, toStore);
r198
212 PlayerPrefs.Save();
213 }
r201
214 catch (Exception ex)
215 {
216 DevLog("Save session nonce failed: " + ex.Message, DevLogLevel.Warning);
217 }
r198
218 return nonce;
219 }
220
r182
221 [MethodImpl(MethodImplOptions.NoInlining)]
222 private byte[] DumbMixA(byte[] input)
223 {
r201
224 if (input == null) return new byte[0];
r182
225 byte[] outb = new byte[input.Length];
226 for (int i = 0; i < input.Length; i++)
227 {
228 int v = input[i];
229 v = ((v << 3) | (v >> 5)) & 0xFF;
230 v ^= (i * 37) & 0xFF;
231 outb[i] = (byte)v;
232 }
233 return outb;
234 }
235
236 [MethodImpl(MethodImplOptions.NoInlining)]
237 private byte[] DumbMixB(byte[] input, int seed)
238 {
r201
239 if (input == null) return new byte[0];
r182
240 byte[] outb = new byte[input.Length];
241 for (int i = 0; i < input.Length; i++)
242 {
243 int v = input[i] ^ (seed >> (i % 8));
244 v = (v * 13 + (i * 7)) & 0xFF;
245 v = (v ^ 0x5A) & 0xFF;
246 outb[i] = (byte)v;
247 }
248 return outb;
249 }
250
251 [MethodImpl(MethodImplOptions.NoInlining)]
252 private byte[] DumbMixC(byte[] input, byte[] nonce)
253 {
r201
254 if (input == null) return new byte[0];
255 if (nonce == null || nonce.Length == 0) return (byte[])input.Clone();
r182
256 byte[] outb = new byte[input.Length];
257 for (int i = 0; i < input.Length; i++)
258 {
259 int n = nonce[i % nonce.Length];
260 int v = input[i];
261 v = ((v + n) ^ (n >> (i % 4))) & 0xFF;
262 outb[i] = (byte)v;
263 }
264 return outb;
265 }
266
267 private byte[] GetSecretBytes()
268 {
269 byte[] k = new byte[obfPartA.Length + obfPartB.Length];
270 for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
271 for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
272 if (!EnableHardening || !EnableDummyMix)
273 {
274 byte[] simple = new byte[k.Length];
275 Buffer.BlockCopy(k, 0, simple, 0, k.Length);
276 Array.Clear(k, 0, k.Length);
277 return simple;
278 }
279 byte[] s1 = DumbMixA(k);
r201
280 int deviceSeed = 0;
281 try { deviceSeed = Application.identifier.GetHashCode() ^ (SystemInfo.deviceUniqueIdentifier?.GetHashCode() ?? 0); } catch { deviceSeed = Application.identifier.GetHashCode(); }
r198
282 byte[] s2 = DumbMixB(s1, deviceSeed);
r182
283 byte[] s3 = DumbMixC(s2, sessionNonce);
284 for (int r = 0; r < 2; r++)
285 {
286 byte[] t = DumbMixA(s3);
r198
287 byte[] u = DumbMixB(t, deviceSeed ^ r);
r182
288 for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]);
289 Array.Clear(t, 0, t.Length);
290 Array.Clear(u, 0, u.Length);
291 }
292 Array.Clear(s1, 0, s1.Length);
293 Array.Clear(s2, 0, s2.Length);
294 byte[] result = new byte[k.Length];
295 for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
296 Array.Clear(k, 0, k.Length);
297 Array.Clear(s3, 0, s3.Length);
298 return result;
299 }
300
301 private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce)
302 {
303 byte[] secret = GetSecretBytes();
304 string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
305 Array.Clear(secret, 0, secret.Length);
306 using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
307 {
308 byte[] km = kdf.GetBytes(KeyMaterialLen);
309 if (EnableHardening && EnableRotation && rotationNonce != null)
310 {
311 for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
312 }
313 byte[] aesKey = new byte[AesKeyLen];
314 byte[] hmacKey = new byte[HmacKeyLen];
315 Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
316 Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
317 Array.Clear(km, 0, km.Length);
r201
318 DevLog($"DeriveKeysRotating called. EnableDummyMix={EnableDummyMix}, EnableRotation={EnableRotation}", DevLogLevel.Trace);
r182
319 return (aesKey, hmacKey);
320 }
321 }
322
r201
323 public void Save<T>(T data) where T : class
r182
324 {
r201
325 if (UseAsyncIO) SaveAsync(data).GetAwaiter().GetResult();
326 else SaveInternal(data);
327 }
328
329 public T Load<T>() where T : class
330 {
331 if (UseAsyncIO) return LoadAsync<T>().GetAwaiter().GetResult();
332 return LoadInternal<T>();
333 }
334
335 public Task SaveAsync<T>(T data) where T : class
336 {
337 string json = JsonUtility.ToJson(data);
338 return Task.Run(() => SaveInternalFromJson(json));
339 }
340
341 public async Task<T> LoadAsync<T>() where T : class
342 {
343 string json = await Task.Run(() => LoadInternalGetJson());
344 if (json == null) return default;
345 T obj = null;
r182
346 try
347 {
r201
348 obj = JsonUtility.FromJson<T>(json);
349 DevLog("JSON deserialized on main thread.", DevLogLevel.Trace);
350 }
351 catch (Exception ex)
352 {
353 DevLog("JsonUtility.FromJson failed: " + ex.ToString(), DevLogLevel.Error);
354 return default;
355 }
356 return obj;
357 }
358
359 private void SaveInternal<T>(T data) where T : class
360 {
361 string json = JsonUtility.ToJson(data);
362 SaveInternalFromJson(json);
363 }
364
365 private void SaveInternalFromJson(string json)
366 {
367 if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection))
368 {
369 string reason;
370 if (IsTamperedOrDebug(out reason))
371 {
372 DevLog($"Save aborted: Tamper/Debug detected - {reason}", DevLogLevel.Warning);
373 return;
374 }
375 }
376 perfSave.Restart();
377 try
378 {
r182
379 byte[] salt = GenerateRandomBytes(SaltLenDefault);
380 byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
381 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
382 byte[] aesKey = keys.aesKey;
383 byte[] hmacKey = keys.hmacKey;
384 byte[] plain = Encoding.UTF8.GetBytes(json);
385 byte[] iv;
386 byte[] cipher;
387 using (Aes aes = Aes.Create())
388 {
389 aes.KeySize = 256;
390 aes.Mode = CipherMode.CBC;
391 aes.Padding = PaddingMode.PKCS7;
392 aes.Key = aesKey;
393 aes.GenerateIV();
394 iv = aes.IV;
395 using (var enc = aes.CreateEncryptor(aes.Key, iv))
396 cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
397 }
r201
398 long ticks = DateTime.UtcNow.Ticks;
399 byte[] ticksBytes = BitConverter.GetBytes(ticks);
400 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
401 int pos = 0;
402 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
403 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
404 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
405 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
406 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r182
407 byte[] hmac;
r201
408 using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput);
409 byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey));
r182
410 using (MemoryStream ms = new MemoryStream())
r201
411 using (BinaryWriter bw = new BinaryWriter(ms))
r182
412 {
r201
413 bw.Write(magicBytes);
414 bw.Write(CurrentVersion);
415 bw.Write((byte)salt.Length);
416 bw.Write(salt);
417 bw.Write((byte)(EnableRotation ? rotation.Length : 0));
418 if (EnableRotation) bw.Write(rotation);
419 bw.Write(iv);
r182
420 byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
421 if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
r201
422 bw.Write(cipherLenBytes);
423 bw.Write(cipher);
424 bw.Write(hmac);
425 bw.Write(ticksBytes);
426 byte[] outBytes = ms.ToArray();
427 string bak = filePath + ".bak";
428 try
429 {
430 if (File.Exists(filePath))
431 {
432 File.Copy(filePath, bak, true);
433 DevLog("Backup created: " + bak, DevLogLevel.Info);
434 }
435 }
436 catch (Exception ex)
437 {
438 DevLog("Backup creation failed: " + ex.Message, DevLogLevel.Warning);
439 }
440 try
441 {
442 File.WriteAllBytes(filePath, outBytes);
443 FileInfo fi = new FileInfo(filePath);
444 DevLog($"Save complete: {filePath} ({fi.Length} bytes)", DevLogLevel.Info);
445 }
446 catch (Exception ex)
447 {
448 DevLog("File write failed: " + ex.ToString(), DevLogLevel.Error);
449 throw;
450 }
r182
451 }
452 ClearBytes(aesKey);
453 ClearBytes(hmacKey);
454 ClearBytes(plain);
455 ClearBytes(iv);
456 ClearBytes(cipher);
r201
457 ClearBytes(hmacInput);
r182
458 ClearBytes(hmac);
459 ClearBytes(salt);
460 ClearBytes(rotation);
r201
461 perfSave.Stop();
462 saveCount++;
463 totalSaveMs += perfSave.ElapsedMilliseconds;
464 DevLog($"Save time: {perfSave.ElapsedMilliseconds} ms (avg {totalSaveMs / (double)saveCount:F1} ms over {saveCount} saves)", DevLogLevel.Info);
r182
465 }
r201
466 catch (Exception ex)
467 {
468 DevLog("Save exception: " + ex.ToString(), DevLogLevel.Error);
469 }
r182
470 }
471
r201
472 private string LoadInternalGetJson()
r182
473 {
r201
474 if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection))
475 {
476 string reason;
477 if (IsTamperedOrDebug(out reason))
478 {
479 DevLog($"Load aborted: Tamper/Debug detected - {reason}", DevLogLevel.Warning);
480 return null;
481 }
482 }
483 perfLoad.Restart();
484 if (!File.Exists(filePath))
485 {
486 DevLog("No file to load: " + filePath, DevLogLevel.Info);
487 return null;
488 }
r182
489 byte[] total;
490 try
491 {
r201
492 total = File.ReadAllBytes(filePath);
493 }
494 catch (Exception ex)
495 {
496 DevLog("Read failed: " + ex.Message, DevLogLevel.Warning);
497 string bak = filePath + ".bak";
498 if (File.Exists(bak))
499 {
500 try
501 {
502 total = File.ReadAllBytes(bak);
503 DevLog("Loaded from backup: " + bak, DevLogLevel.Info);
504 }
505 catch (Exception ex2)
506 {
507 DevLog("Backup read failed: " + ex2.Message, DevLogLevel.Error);
508 return null;
509 }
510 }
511 else return null;
512 }
513 try
514 {
r182
515 using (MemoryStream ms = new MemoryStream(total))
516 using (BinaryReader br = new BinaryReader(ms))
517 {
518 byte[] magic = br.ReadBytes(4);
r201
519 string mag = Encoding.ASCII.GetString(magic);
520 string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
521 if (mag != expectedMagic)
522 {
523 DevLog("Magic mismatch.", DevLogLevel.Warning);
524 return null;
525 }
r182
526 byte ver = br.ReadByte();
r201
527 if (ver != CurrentVersion)
528 {
529 DevLog($"Version mismatch: {ver} != {CurrentVersion}", DevLogLevel.Warning);
530 return null;
531 }
r182
532 int saltLen = br.ReadByte();
r201
533 if (saltLen <= 0 || saltLen > 64) { DevLog("Bad salt length.", DevLogLevel.Warning); return null; }
r182
534 byte[] salt = br.ReadBytes(saltLen);
535 int rotationLen = br.ReadByte();
536 byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
537 byte[] iv = br.ReadBytes(IvLen);
538 int cipherLen = br.ReadInt32();
539 if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
r201
540 if (cipherLen <= 0 || cipherLen > total.Length) { DevLog("Bad cipher length.", DevLogLevel.Warning); return null; }
r182
541 byte[] cipher = br.ReadBytes(cipherLen);
542 byte[] hmac = br.ReadBytes(HmacLen);
r201
543 byte[] ticksBytes = br.ReadBytes(8);
544 long ticks = BitConverter.ToInt64(ticksBytes, 0);
545 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
546 int pos = 0;
547 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
548 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
549 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
550 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
551 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
r182
552 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
553 byte[] aesKey = keys.aesKey;
554 byte[] hmacKey = keys.hmacKey;
555 byte[] expected;
r201
556 using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(hmacInput);
r182
557 bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
558 if (!ok)
559 {
r201
560 DevLog("HMAC mismatch - possible tamper or key mismatch.", DevLogLevel.Warning);
r182
561 ClearBytes(aesKey);
562 ClearBytes(hmacKey);
563 ClearBytes(expected);
r201
564 return null;
r182
565 }
r201
566 if (EnableTimeValidation && ticks > 0)
567 {
568 long nowTicks = DateTime.UtcNow.Ticks;
569 long diffSec = Math.Abs(nowTicks - ticks) / TimeSpan.TicksPerSecond;
570 DevLog($"Saved timestamp: {new DateTime(ticks).ToString("o")}, Current UTC: {DateTime.UtcNow.ToString("o")}, diffSec={diffSec}", DevLogLevel.Info);
571 if (diffSec > MaxAllowedTimeJumpSeconds)
572 {
573 DevLog($"Time jump detected: {diffSec} seconds -> rejecting load.", DevLogLevel.Warning);
574 ClearBytes(aesKey);
575 ClearBytes(hmacKey);
576 ClearBytes(expected);
577 return null;
578 }
579 }
r182
580 byte[] plain;
581 using (Aes aes = Aes.Create())
582 {
583 aes.KeySize = 256;
584 aes.Mode = CipherMode.CBC;
585 aes.Padding = PaddingMode.PKCS7;
586 aes.Key = aesKey;
587 aes.IV = iv;
588 using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
589 plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
590 }
591 string json = Encoding.UTF8.GetString(plain);
592 ClearBytes(aesKey);
593 ClearBytes(hmacKey);
594 ClearBytes(expected);
595 ClearBytes(plain);
596 ClearBytes(iv);
597 ClearBytes(cipher);
598 ClearBytes(salt);
599 ClearBytes(rotation);
r201
600 perfLoad.Stop();
601 loadCount++;
602 totalLoadMs += perfLoad.ElapsedMilliseconds;
603 DevLog($"Load complete: {filePath} ({total.Length} bytes). Load time: {perfLoad.ElapsedMilliseconds} ms (avg {totalLoadMs / (double)loadCount:F1} ms over {loadCount} loads)", DevLogLevel.Info);
604 return json;
r182
605 }
606 }
r201
607 catch (Exception ex)
608 {
609 DevLog("Load exception: " + ex.ToString(), DevLogLevel.Error);
610 string bak = filePath + ".bak";
611 if (File.Exists(bak))
612 {
613 DevLog("Attempting to load from backup.", DevLogLevel.Info);
614 try
615 {
616 byte[] bakTotal = File.ReadAllBytes(bak);
617 using (MemoryStream ms = new MemoryStream(bakTotal))
618 using (BinaryReader br = new BinaryReader(ms))
619 {
620 byte[] magic = br.ReadBytes(4);
621 string mag = Encoding.ASCII.GetString(magic);
622 string expectedMagic = Obf.Decode(ObfMagicBytes, ObfMagicKey);
623 if (mag != expectedMagic) { DevLog("Backup magic mismatch.", DevLogLevel.Warning); return null; }
624 byte ver = br.ReadByte();
625 if (ver != CurrentVersion) { DevLog("Backup version mismatch.", DevLogLevel.Warning); return null; }
626 int saltLen = br.ReadByte();
627 if (saltLen <= 0 || saltLen > 64) { DevLog("Backup bad salt length.", DevLogLevel.Warning); return null; }
628 byte[] salt = br.ReadBytes(saltLen);
629 int rotationLen = br.ReadByte();
630 byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
631 byte[] iv = br.ReadBytes(IvLen);
632 int cipherLen = br.ReadInt32();
633 if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
634 if (cipherLen <= 0 || cipherLen > bakTotal.Length) { DevLog("Backup bad cipher length.", DevLogLevel.Warning); return null; }
635 byte[] cipher = br.ReadBytes(cipherLen);
636 byte[] hmac = br.ReadBytes(HmacLen);
637 byte[] ticksBytes = br.ReadBytes(8);
638 byte[] hmacInput = new byte[salt.Length + rotation.Length + iv.Length + cipher.Length + ticksBytes.Length];
639 int pos = 0;
640 Buffer.BlockCopy(salt, 0, hmacInput, pos, salt.Length); pos += salt.Length;
641 Buffer.BlockCopy(rotation, 0, hmacInput, pos, rotation.Length); pos += rotation.Length;
642 Buffer.BlockCopy(iv, 0, hmacInput, pos, iv.Length); pos += iv.Length;
643 Buffer.BlockCopy(cipher, 0, hmacInput, pos, cipher.Length); pos += cipher.Length;
644 Buffer.BlockCopy(ticksBytes, 0, hmacInput, pos, ticksBytes.Length);
645 var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
646 byte[] aesKey = keys.aesKey;
647 byte[] hmacKey = keys.hmacKey;
648 byte[] expected;
649 using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(hmacInput);
650 bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
651 if (!ok) { ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); DevLog("Backup HMAC mismatch.", DevLogLevel.Warning); return null; }
652 byte[] plain;
653 using (Aes aes = Aes.Create())
654 {
655 aes.KeySize = 256;
656 aes.Mode = CipherMode.CBC;
657 aes.Padding = PaddingMode.PKCS7;
658 aes.Key = aesKey;
659 aes.IV = iv;
660 using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
661 plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
662 }
663 string json = Encoding.UTF8.GetString(plain);
664 DevLog("Backup load successful.", DevLogLevel.Info);
665 ClearBytes(aesKey); ClearBytes(hmacKey); ClearBytes(expected); ClearBytes(plain);
666 try
667 {
668 File.Copy(bak, filePath, true);
669 DevLog("Main file restored from backup.", DevLogLevel.Info);
670 }
671 catch (Exception ex2)
672 {
673 DevLog("Failed to restore main from backup: " + ex2.Message, DevLogLevel.Warning);
674 }
675 return json;
676 }
677 }
678 catch (Exception ex2)
679 {
680 DevLog("Backup load failed: " + ex2.Message, DevLogLevel.Error);
681 return null;
682 }
683 }
684 return null;
685 }
r182
686 }
687
688 private static byte[] GenerateRandomBytes(int len)
689 {
690 byte[] b = new byte[len];
691 using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
692 return b;
693 }
694
695 private static void ClearBytes(byte[] b)
696 {
697 if (b == null) return;
698 Array.Clear(b, 0, b.Length);
699 }
700
701 private string ResolveFileName(string baseName)
702 {
703 if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
704 byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
705 using (var sha = SHA256.Create())
706 {
707 byte[] hash = sha.ComputeHash(nameBytes);
708 string b64 = Convert.ToBase64String(hash);
709 string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
710 ClearBytes(hash);
711 return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
712 }
713 }
714
r201
715 private bool IsTamperedOrDebug(out string reason)
r182
716 {
r201
717 reason = "";
r182
718 if (!EnableHardening) return false;
719 try
720 {
721 if (EnableAntiDebugChecks)
722 {
r201
723 if (System.Diagnostics.Debugger.IsAttached)
724 {
725 reason = "Debugger Attached";
726 return true;
727 }
728 try
729 {
730 string proc = System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower();
731 if (proc.Contains("debug") || proc.Contains("devenv") || proc.Contains("mono") || proc.Contains("dnspy"))
732 {
733 reason = $"ProcessName suspicious: {proc}";
734 return true;
735 }
736 }
737 catch (Exception ex)
738 {
739 DevLog("Process name check failed: " + ex.Message, DevLogLevel.Trace);
740 }
r182
741 }
742 }
r201
743 catch (Exception ex) { DevLog("AntiDebugChecks failed: " + ex.Message, DevLogLevel.Trace); }
r182
744 try
745 {
746 if (EnableRootDetection)
747 {
748 string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
r201
749 foreach (var p in suspectPaths) if (File.Exists(p))
750 {
751 reason = $"Root path found: {p}";
752 return true;
753 }
r182
754 }
755 }
r201
756 catch (Exception ex) { DevLog("Root detection failed: " + ex.Message, DevLogLevel.Trace); }
r182
757 return false;
758 }
r201
759
760 private bool VerifyAssemblyHash(string expectedHex)
761 {
762 try
763 {
764 Assembly asm = typeof(SecureManager).Assembly;
765 string loc = asm.Location;
766 if (string.IsNullOrEmpty(loc)) return true;
767 byte[] bytes = File.ReadAllBytes(loc);
768 using (var sha = SHA256.Create())
769 {
770 byte[] h = sha.ComputeHash(bytes);
771 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
772 return hex == expectedHex.ToLowerInvariant();
773 }
774 }
775 catch { return true; }
776 }
777
778 private string CalculateAssemblyHashPartial(out bool success)
779 {
780 success = false;
781 try
782 {
783 Assembly asm = typeof(SecureManager).Assembly;
784 string loc = asm.Location;
785 if (string.IsNullOrEmpty(loc)) { success = false; return ""; }
786 byte[] bytes = File.ReadAllBytes(loc);
787 using (var sha = SHA256.Create())
788 {
789 byte[] h = sha.ComputeHash(bytes);
790 string hex = BitConverter.ToString(h).Replace("-", "").ToLowerInvariant();
791 success = true;
792 return hex;
793 }
794 }
795 catch { success = false; return ""; }
796 }
797
798 private void DevLog(string message, DevLogLevel level = DevLogLevel.Info)
799 {
800 if (!EnableDeveloperLogging) return;
801 if (level < DeveloperLogLevel) return;
802 string prefix = $"[SM:{level}] ";
803 string line = prefix + message;
804 try
805 {
806 if (level == DevLogLevel.Error || level == DevLogLevel.Critical) Debug.LogError(line);
807 else if (level == DevLogLevel.Warning) Debug.LogWarning(line);
808 else Debug.Log(line);
809 }
810 catch { }
811 try
812 {
813 File.AppendAllText(devLogFilePath, DateTime.UtcNow.ToString("o") + " " + line + Environment.NewLine);
814 }
815 catch { }
816 }
817
818 private byte[] TryProtectedProtect(byte[] data)
819 {
820 try
821 {
822#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
823 return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
824#else
825 return null;
826#endif
827 }
828 catch { return null; }
829 }
830
831 private byte[] TryProtectedUnprotect(byte[] data)
832 {
833 try
834 {
835#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
836 return ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser);
837#else
838 return null;
839#endif
840 }
841 catch { return null; }
842 }
r198
843}
844}}}
r204
845==== 기능 ====
846 * 싱글톤 인스턴스 관리
847 * 초기화
848 * 세션 Nonce 생성
849 * 파일 경로 생성
850 * Warmup 호출
851
852 * 무작위 바이트 생성
853 * DumbMixA/B/C
854 * SecretBytes 생성
855
856 * 키 파생 (DeriveKeysRotating)
857 * AES 키/HMAC 키 분리
858 * Rotation 적용 (옵션)
859
860 * Save<T>
861 * JSON 직렬화
862 * 난수 Salt 생성
863 * Rotation 바이트 생성
864 * AES 키/HMAC 키 생성
865 * AES 암호화 수행
866 * HMAC 생성
867 * 메모리 블롭 생성
868 * 파일 저장
869 * 메모리 정리
870
871 * Load<T>
872 * 파일 존재 확인
873 * Base64 디코딩
874 * Magic/Version 체크
875 * Salt/Rotation/IV/Cipher/HMAC 읽기
876 * 키 파생
877 * HMAC 검증
878 * AES 복호화 수행
879 * JSON 역직렬화
880 * 메모리 정리
881
882 * 파일명 난독화 및 경로
883 * ResolveFileName(baseName)
884 * 최종 경로 반환(filePath)
885
886 * 개발자 로깅 (EnableDeveloperLogging)
887 * 로그 레벨 관리
888 * DevLog 함수: 콘솔 + 파일 기록
889 * 초기화 환경 로그
890 * 파일 경로, 백업, 파일 크기 로그
891 * Save/Load 시간 측정 및 평균
892 * 키 파생 옵션 로그
893 * 보안 실패 상세 로그
894 * 예외 메시지/스택트레이스 기록
895 * 로그 필터링
896
897 * 변조·디버깅 탐지
898 * Debugger.IsAttached
899 * 프로세스 이름 확인
900 * 루팅 경로 검사
901 * 탐지 시 Save/Load 거부 및 로그 출력
902
903 * 어셈블리 무결성 (옵션)
904 * ExpectedAssemblyHash 비교
905 * DevLog 해시 일부 출력
906
907 * 시간 위변조 검증
908 * Save 시 UTC ticks 저장
909 * Load 시 ticks 비교
910 * 시간 점프 감지 및 로그 출력
911
912 * 플랫폼 보호 (Windows DPAPI)
913 * TryProtectedProtect / TryProtectedUnprotect
914 * Windows 환경만 적용
915
916 * 성능/안정성 보조
917 * Save/Load 시간 측정 및 평균
918 * I/O 예외 시 백업 로드 시도
919
920 * 유틸리티 헬퍼
921 * GenerateRandomBytes(len)
922 * ClearBytes(byte[])
923 * Obf.Decode
924 * CalculateAssemblyHashPartial(out bool)
925
926 * 파일 버전 및 마이그레이션 자리
927 * Version 바이트 관리
928 * MigrateData 가능
929
r185
930=== 실행 코드 ===
r183
931{{{#!syntax csharp
932using UnityEngine;
933
r185
934/// <summary>
935/// SecureManager의 Save 및 Load 메서드를 시연하는 예시 스크립트입니다.
936/// </summary>
r183
937public class ExampleUsage : MonoBehaviour
938{
939 private readonly string testPlayerName = "Gemini_Test_User";
940 private readonly int initialScore = 1000;
941 private readonly int newScore = 5000;
942
943 void Start()
944 {
r185
945 // SecureManager가 씬에 존재하는지 확인합니다.
r183
946 if (SecureManager.Instance == null)
947 {
948 Debug.LogError("SecureManager 인스턴스를 찾을 수 없습니다. SecureManager.cs를 GameObject에 추가했는지 확인해주세요.");
949 return;
950 }
r185
951
952 // 1. 데이터 저장 테스트
r183
953 SaveTestData();
954
r185
955 // 2. 데이터 로드 테스트
r183
956 LoadTestData();
957
r185
958 // 3. 데이터 업데이트 및 재저장
r183
959 UpdateAndSaveTestData();
r185
960
961 // 4. 업데이트된 데이터 로드 확인
r183
962 LoadTestData();
963 }
964
965 void SaveTestData()
966 {
967 Debug.Log("--- 1. 초기 데이터 저장 시도 ---");
968 PlayerData dataToSave = new PlayerData
969 {
970 playerName = testPlayerName,
971 score = initialScore
972 };
973
974 SecureManager.Instance.Save(dataToSave);
975 Debug.Log($"초기 데이터 저장 완료: Player={dataToSave.playerName}, Score={dataToSave.score}");
976 }
977
978 void LoadTestData()
979 {
980 Debug.Log("--- 2. 데이터 로드 시도 ---");
981 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
982
983 if (loadedData != null)
984 {
985 Debug.Log($"데이터 로드 성공: Player={loadedData.playerName}, Score={loadedData.score}");
986 }
987 else
988 {
989 Debug.LogWarning("데이터 로드 실패 (파일 없음, 오류 발생, 혹은 디버거 감지).");
990 }
991 }
992
993 void UpdateAndSaveTestData()
994 {
995 Debug.Log("--- 3. 데이터 업데이트 및 재저장 시도 ---");
996 PlayerData loadedData = SecureManager.Instance.Load<PlayerData>();
997
998 if (loadedData != null)
999 {
1000 loadedData.score = newScore;
1001 SecureManager.Instance.Save(loadedData);
1002 Debug.Log($"데이터 업데이트 및 저장 완료: New Score={loadedData.score}");
1003 }
1004 }
1005
1006 void OnDestroy()
1007 {
r185
1008 // 애플리케이션 종료 시 디버거 체크가 활성화된 경우 에러 방지
r183
1009 if (SecureManager.Instance != null && SecureManager.Instance.EnableAntiDebugChecks)
1010 {
r187
1011 SecureManager.Instance.EnableAntiDebugChecks = false;
r183
1012 Debug.Log("SecureManager: Anti-Debug Checks가 활성화되어 있습니다.");
1013 }
1014 }
r182
1015} }}}