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