r200 vs r201
......
5656
GZipStream을 이용해서 저장 용량을 50% 줄여보기.
5757
5858
59
=== 본코드 ===
59
=== 본 코드 ===
6060
{{{#!syntax csharp
6161
using System;
6262
using System.IO;
63
using System.Text;
6364
using System.Security.Cryptography;
64
using System.Text;
65
using System.Threading.Tasks;
66
using System.Reflection;
6567
using UnityEngine;
68
using System.Diagnostics;
6669
using System.Runtime.CompilerServices;
6770
6871
[Serializable]
......
7275
public int score;
7376
}
7477
78
public enum DevLogLevel { Trace = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 }
79
7580
public class SecureManager : MonoBehaviour
7681
{
7782
public static SecureManager Instance { get; private set; }
......
7984
public bool EnableHardening = true;
8085
public bool EnableAntiDebugChecks = true;
8186
public bool EnableRootDetection = true;
82
public bool EnableDummyMix = false;
87
public bool EnableDummyMix = true;
8388
public bool EnableFileNameObfuscation = true;
8489
public bool EnableRotation = true;
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 = "";
8597
86
private const string Magic = "SJMP";
87
private const byte Version = 1;
88
private const int SaltLenDefault = 12;
98
private const int SaltLenDefault = 16;
8999
private const int IvLen = 16;
90100
private const int HmacLen = 32;
91101
private const int KeyMaterialLen = 64;
92102
private const int AesKeyLen = 32;
93103
private const int HmacKeyLen = 32;
94104
private const int Pbkdf2Iterations = 20000;
105
95106
private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
96107
private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
97108
private const byte Mask = 0xAA;
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
98113
private string filePath;
99114
private byte[] sessionNonce;
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;
100122
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
101135
void Awake()
102136
{
103137
if (Instance != null && Instance != this)
......
107141
}
108142
Instance = this;
109143
DontDestroyOnLoad(gameObject);
110
111144
sessionNonce = LoadOrCreateSessionNonce();
112145
filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
113
Warmup();
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();
114175
}
115176
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
116182
private byte[] LoadOrCreateSessionNonce()
117183
{
118
const string key = "SecureManager_SessionNonce_v1";
184
const string key = "SecureManager_SessionNonce_v4";
119185
try
120186
{
121187
if (PlayerPrefs.HasKey(key))
122188
{
123189
string b64 = PlayerPrefs.GetString(key);
124
byte[] stored = Convert.FromBase64String(b64);
125
if (stored != null && stored.Length > 0) return stored;
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
}
126201
}
127202
}
128
catch { }
129
203
catch (Exception ex)
204
{
205
DevLog("Load session nonce failed: " + ex.Message, DevLogLevel.Warning);
206
}
130207
byte[] nonce = GenerateRandomBytes(16);
131208
try
132209
{
133
PlayerPrefs.SetString(key, Convert.ToBase64String(nonce));
210
byte[] protectedBytes = TryProtectedProtect(nonce);
211
string toStore = protectedBytes != null ? Convert.ToBase64String(protectedBytes) : Convert.ToBase64String(nonce);
212
PlayerPrefs.SetString(key, toStore);
134213
PlayerPrefs.Save();
135214
}
136
catch { }
215
catch (Exception ex)
216
{
217
DevLog("Save session nonce failed: " + ex.Message, DevLogLevel.Warning);
218
}
137219
return nonce;
138220
}
139221
140
private void Warmup()
141
{
142
try { _ = GetSecretBytes(); } catch { }
143
}
144
145222
[MethodImpl(MethodImplOptions.NoInlining)]
146223
private byte[] DumbMixA(byte[] input)
147224
{
225
if (input == null) return new byte[0];
148226
byte[] outb = new byte[input.Length];
149227
for (int i = 0; i < input.Length; i++)
150228
{
......
159237
[MethodImpl(MethodImplOptions.NoInlining)]
160238
private byte[] DumbMixB(byte[] input, int seed)
161239
{
240
if (input == null) return new byte[0];
162241
byte[] outb = new byte[input.Length];
163242
for (int i = 0; i < input.Length; i++)
164243
{
......
173252
[MethodImpl(MethodImplOptions.NoInlining)]
174253
private byte[] DumbMixC(byte[] input, byte[] nonce)
175254
{
255
if (input == null) return new byte[0];
256
if (nonce == null || nonce.Length == 0) return (byte[])input.Clone();
176257
byte[] outb = new byte[input.Length];
177258
for (int i = 0; i < input.Length; i++)
178259
{
......
189270
byte[] k = new byte[obfPartA.Length + obfPartB.Length];
190271
for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
191272
for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
192
193273
if (!EnableHardening || !EnableDummyMix)
194274
{
195275
byte[] simple = new byte[k.Length];
......
197277
Array.Clear(k, 0, k.Length);
198278
return simple;
199279
}
200
201280
byte[] s1 = DumbMixA(k);
202
203
204
int deviceSeed = Application.identifier.GetHashCode() ^ SystemInfo.deviceUniqueIdentifier.GetHashCode();
205
281
int deviceSeed = 0;
282
try { deviceSeed = Application.identifier.GetHashCode() ^ (SystemInfo.deviceUniqueIdentifier?.GetHashCode() ?? 0); } catch { deviceSeed = Application.identifier.GetHashCode(); }
206283
byte[] s2 = DumbMixB(s1, deviceSeed);
207284
byte[] s3 = DumbMixC(s2, sessionNonce);
208285
for (int r = 0; r < 2; r++)
......
215292
}
216293
Array.Clear(s1, 0, s1.Length);
217294
Array.Clear(s2, 0, s2.Length);
218
219295
byte[] result = new byte[k.Length];
220296
for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
221297
Array.Clear(k, 0, k.Length);
......
228304
byte[] secret = GetSecretBytes();
229305
string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
230306
Array.Clear(secret, 0, secret.Length);
231
232307
using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
233308
{
234309
byte[] km = kdf.GetBytes(KeyMaterialLen);
......
241316
Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
242317
Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
243318
Array.Clear(km, 0, km.Length);
319
DevLog($"DeriveKeysRotating called. EnableDummyMix={EnableDummyMix}, EnableRotation={EnableRotation}", DevLogLevel.Trace);
244320
return (aesKey, hmacKey);
245321
}
246322
}
247323
248
public void Save<T>(T data)
324
public void Save<T>(T data) where T : class
249325
{
250
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return;
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;
251347
try
252348
{
253
string json = JsonUtility.ToJson(data);
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
{
254380
byte[] salt = GenerateRandomBytes(SaltLenDefault);
255381
byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
256382
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
......
270396
using (var enc = aes.CreateEncryptor(aes.Key, iv))
271397
cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
272398
}
273
byte[] ivCipher = new byte[iv.Length + cipher.Length];
274
Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
275
Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
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);
276408
byte[] hmac;
277
using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(ivCipher);
409
using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(hmacInput);
410
byte[] magicBytes = Encoding.ASCII.GetBytes(Obf.Decode(ObfMagicBytes, ObfMagicKey));
278411
using (MemoryStream ms = new MemoryStream())
412
using (BinaryWriter bw = new BinaryWriter(ms))
279413
{
280
ms.Write(Encoding.ASCII.GetBytes(Magic), 0, 4);
281
ms.WriteByte(Version);
282
ms.WriteByte((byte)salt.Length);
283
ms.Write(salt, 0, salt.Length);
284
ms.WriteByte(EnableRotation ? (byte)rotation.Length : (byte)0);
285
if (EnableRotation) ms.Write(rotation, 0, rotation.Length);
286
ms.Write(iv, 0, iv.Length);
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);
287421
byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
288422
if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
289
ms.Write(cipherLenBytes, 0, 4);
290
ms.Write(cipher, 0, cipher.Length);
291
ms.Write(hmac, 0, hmac.Length);
292
string outBlob = Convert.ToBase64String(ms.ToArray());
293
File.WriteAllText(filePath, outBlob);
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
}
294452
}
295453
ClearBytes(aesKey);
296454
ClearBytes(hmacKey);
297455
ClearBytes(plain);
298456
ClearBytes(iv);
299457
ClearBytes(cipher);
300
ClearBytes(ivCipher);
458
ClearBytes(hmacInput);
301459
ClearBytes(hmac);
302460
ClearBytes(salt);
303461
ClearBytes(rotation);
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);
304466
}
305
catch { }
467
catch (Exception ex)
468
{
469
DevLog("Save exception: " + ex.ToString(), DevLogLevel.Error);
470
}
306471
}
307472
308
public T Load<T>()
473
private string LoadInternalGetJson()
309474
{
310
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return default;
311
if (!File.Exists(filePath)) return default;
312
string blob = File.ReadAllText(filePath);
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
}
313490
byte[] total;
314
try { total = Convert.FromBase64String(blob); } catch { return default; }
315491
try
316492
{
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
{
317516
using (MemoryStream ms = new MemoryStream(total))
318517
using (BinaryReader br = new BinaryReader(ms))
319518
{
320519
byte[] magic = br.ReadBytes(4);
321
if (Encoding.ASCII.GetString(magic) != Magic) return default;
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
}
322527
byte ver = br.ReadByte();
323
if (ver != Version) return default;
528
if (ver != CurrentVersion)
529
{
530
DevLog($"Version mismatch: {ver} != {CurrentVersion}", DevLogLevel.Warning);
531
return null;
532
}
324533
int saltLen = br.ReadByte();
325
if (saltLen <= 0 || saltLen > 64) return default;
534
if (saltLen <= 0 || saltLen > 64) { DevLog("Bad salt length.", DevLogLevel.Warning); return null; }
326535
byte[] salt = br.ReadBytes(saltLen);
327536
int rotationLen = br.ReadByte();
328537
byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
329538
byte[] iv = br.ReadBytes(IvLen);
330539
int cipherLen = br.ReadInt32();
331540
if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
332
if (cipherLen <= 0 || cipherLen > total.Length) return default;
541
if (cipherLen <= 0 || cipherLen > total.Length) { DevLog("Bad cipher length.", DevLogLevel.Warning); return null; }
333542
byte[] cipher = br.ReadBytes(cipherLen);
334543
byte[] hmac = br.ReadBytes(HmacLen);
335
byte[] ivCipher = new byte[iv.Length + cipher.Length];
336
Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
337
Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
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);
338553
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
339554
byte[] aesKey = keys.aesKey;
340555
byte[] hmacKey = keys.hmacKey;
341556
byte[] expected;
342
using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(ivCipher);
557
using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(hmacInput);
343558
bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
344559
if (!ok)
345560
{
561
DevLog("HMAC mismatch - possible tamper or key mismatch.", DevLogLevel.Warning);
346562
ClearBytes(aesKey);
347563
ClearBytes(hmacKey);
348564
ClearBytes(expected);
349
return default;
565
return null;
350566
}
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
}
351581
byte[] plain;
352582
using (Aes aes = Aes.Create())
353583
{
......
368598
ClearBytes(cipher);
369599
ClearBytes(salt);
370600
ClearBytes(rotation);
371
return JsonUtility.FromJson<T>(json);
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;
372606
}
373607
}
374
catch { return default; }
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
}
375687
}
376688
377689
private static byte[] GenerateRandomBytes(int len)
......
401713
}
402714
}
403715
404
private bool IsTamperedOrDebug()
716
private bool IsTamperedOrDebug(out string reason)
405717
{
718
reason = "";
406719
if (!EnableHardening) return false;
407720
try
408721
{
409722
if (EnableAntiDebugChecks)
410723
{
411
if (System.Diagnostics.Debugger.IsAttached) return true;
412
if (System.Diagnostics.Process.GetCurrentProcess().ProcessName.ToLower().Contains("debug")) return true;
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
}
413742
}
414743
}
415
catch { }
744
catch (Exception ex) { DevLog("AntiDebugChecks failed: " + ex.Message, DevLogLevel.Trace); }
416745
try
417746
{
418747
if (EnableRootDetection)
419748
{
420749
string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
421
foreach (var p in suspectPaths) if (File.Exists(p)) return true;
750
foreach (var p in suspectPaths) if (File.Exists(p))
751
{
752
reason = $"Root path found: {p}";
753
return true;
754
}
422755
}
423756
}
424
catch { }
757
catch (Exception ex) { DevLog("Root detection failed: " + ex.Message, DevLogLevel.Trace); }
425758
return false;
426759
}
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
}
427844
}
428845
}}}
429846
=== 실행 코드 ===
......