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