r177 vs r182
......
5555
5656
[[/연습장|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)"
5757
'''연습장 바로가기'''}}}]]
58
59
{{{#!syntax csharp
60
using System;
61
using System.IO;
62
using System.Security.Cryptography;
63
using System.Text;
64
using UnityEngine;
65
using System.Diagnostics;
66
using System.Runtime.CompilerServices;
67
68
[Serializable]
69
public class PlayerData
70
{
71
public string playerName;
72
public int score;
73
}
74
75
public class SecureManager : MonoBehaviour
76
{
77
public static SecureManager Instance { get; private set; }
78
79
public bool EnableHardening = true;
80
public bool EnableAntiDebugChecks = true;
81
public bool EnableRootDetection = true;
82
public bool EnableDummyMix = true;
83
public bool EnableFileNameObfuscation = true;
84
public bool EnableRotation = true;
85
86
private const string Magic = "SJMP";
87
private const byte Version = 1;
88
private const int SaltLenDefault = 12;
89
private const int IvLen = 16;
90
private const int HmacLen = 32;
91
private const int KeyMaterialLen = 64;
92
private const int AesKeyLen = 32;
93
private const int HmacKeyLen = 32;
94
private const int Pbkdf2Iterations = 20000;
95
private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C };
96
private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE };
97
private const byte Mask = 0xAA;
98
private string filePath;
99
private byte[] sessionNonce;
100
101
void Awake()
102
{
103
if (Instance != null && Instance != this)
104
{
105
Destroy(gameObject);
106
return;
107
}
108
Instance = this;
109
DontDestroyOnLoad(gameObject);
110
111
sessionNonce = GenerateRandomBytes(16);
112
filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData"));
113
Warmup();
114
}
115
116
private void Warmup()
117
{
118
try { _ = GetSecretBytes(); } catch { }
119
}
120
121
[MethodImpl(MethodImplOptions.NoInlining)]
122
private byte[] DumbMixA(byte[] input)
123
{
124
byte[] outb = new byte[input.Length];
125
for (int i = 0; i < input.Length; i++)
126
{
127
int v = input[i];
128
v = ((v << 3) | (v >> 5)) & 0xFF;
129
v ^= (i * 37) & 0xFF;
130
outb[i] = (byte)v;
131
}
132
return outb;
133
}
134
135
[MethodImpl(MethodImplOptions.NoInlining)]
136
private byte[] DumbMixB(byte[] input, int seed)
137
{
138
byte[] outb = new byte[input.Length];
139
for (int i = 0; i < input.Length; i++)
140
{
141
int v = input[i] ^ (seed >> (i % 8));
142
v = (v * 13 + (i * 7)) & 0xFF;
143
v = (v ^ 0x5A) & 0xFF;
144
outb[i] = (byte)v;
145
}
146
return outb;
147
}
148
149
[MethodImpl(MethodImplOptions.NoInlining)]
150
private byte[] DumbMixC(byte[] input, byte[] nonce)
151
{
152
byte[] outb = new byte[input.Length];
153
for (int i = 0; i < input.Length; i++)
154
{
155
int n = nonce[i % nonce.Length];
156
int v = input[i];
157
v = ((v + n) ^ (n >> (i % 4))) & 0xFF;
158
outb[i] = (byte)v;
159
}
160
return outb;
161
}
162
163
private byte[] GetSecretBytes()
164
{
165
byte[] k = new byte[obfPartA.Length + obfPartB.Length];
166
for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask);
167
for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask);
168
169
if (!EnableHardening || !EnableDummyMix)
170
{
171
byte[] simple = new byte[k.Length];
172
Buffer.BlockCopy(k, 0, simple, 0, k.Length);
173
Array.Clear(k, 0, k.Length);
174
return simple;
175
}
176
177
byte[] s1 = DumbMixA(k);
178
int dynamicSeed = (int)(DateTime.UtcNow.Ticks ^ Application.identifier.GetHashCode() ^ Environment.TickCount);
179
byte[] s2 = DumbMixB(s1, dynamicSeed);
180
byte[] s3 = DumbMixC(s2, sessionNonce);
181
for (int r = 0; r < 2; r++)
182
{
183
byte[] t = DumbMixA(s3);
184
byte[] u = DumbMixB(t, dynamicSeed ^ r);
185
for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]);
186
Array.Clear(t, 0, t.Length);
187
Array.Clear(u, 0, u.Length);
188
}
189
Array.Clear(s1, 0, s1.Length);
190
Array.Clear(s2, 0, s2.Length);
191
byte[] result = new byte[k.Length];
192
for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]);
193
Array.Clear(k, 0, k.Length);
194
Array.Clear(s3, 0, s3.Length);
195
return result;
196
}
197
198
private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce)
199
{
200
byte[] secret = GetSecretBytes();
201
string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier;
202
Array.Clear(secret, 0, secret.Length);
203
204
using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256))
205
{
206
byte[] km = kdf.GetBytes(KeyMaterialLen);
207
if (EnableHardening && EnableRotation && rotationNonce != null)
208
{
209
for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length];
210
}
211
byte[] aesKey = new byte[AesKeyLen];
212
byte[] hmacKey = new byte[HmacKeyLen];
213
Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen);
214
Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen);
215
Array.Clear(km, 0, km.Length);
216
return (aesKey, hmacKey);
217
}
218
}
219
220
public void Save<T>(T data)
221
{
222
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return;
223
try
224
{
225
string json = JsonUtility.ToJson(data);
226
byte[] salt = GenerateRandomBytes(SaltLenDefault);
227
byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16];
228
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
229
byte[] aesKey = keys.aesKey;
230
byte[] hmacKey = keys.hmacKey;
231
byte[] plain = Encoding.UTF8.GetBytes(json);
232
byte[] iv;
233
byte[] cipher;
234
using (Aes aes = Aes.Create())
235
{
236
aes.KeySize = 256;
237
aes.Mode = CipherMode.CBC;
238
aes.Padding = PaddingMode.PKCS7;
239
aes.Key = aesKey;
240
aes.GenerateIV();
241
iv = aes.IV;
242
using (var enc = aes.CreateEncryptor(aes.Key, iv))
243
cipher = enc.TransformFinalBlock(plain, 0, plain.Length);
244
}
245
byte[] ivCipher = new byte[iv.Length + cipher.Length];
246
Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
247
Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
248
byte[] hmac;
249
using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(ivCipher);
250
using (MemoryStream ms = new MemoryStream())
251
{
252
ms.Write(Encoding.ASCII.GetBytes(Magic), 0, 4);
253
ms.WriteByte(Version);
254
ms.WriteByte((byte)salt.Length);
255
ms.Write(salt, 0, salt.Length);
256
ms.WriteByte(EnableRotation ? (byte)rotation.Length : (byte)0);
257
if (EnableRotation) ms.Write(rotation, 0, rotation.Length);
258
ms.Write(iv, 0, iv.Length);
259
byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length);
260
if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes);
261
ms.Write(cipherLenBytes, 0, 4);
262
ms.Write(cipher, 0, cipher.Length);
263
ms.Write(hmac, 0, hmac.Length);
264
string outBlob = Convert.ToBase64String(ms.ToArray());
265
File.WriteAllText(filePath, outBlob);
266
}
267
ClearBytes(aesKey);
268
ClearBytes(hmacKey);
269
ClearBytes(plain);
270
ClearBytes(iv);
271
ClearBytes(cipher);
272
ClearBytes(ivCipher);
273
ClearBytes(hmac);
274
ClearBytes(salt);
275
ClearBytes(rotation);
276
}
277
catch { }
278
}
279
280
public T Load<T>()
281
{
282
if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return default;
283
if (!File.Exists(filePath)) return default;
284
string blob = File.ReadAllText(filePath);
285
byte[] total;
286
try { total = Convert.FromBase64String(blob); } catch { return default; }
287
try
288
{
289
using (MemoryStream ms = new MemoryStream(total))
290
using (BinaryReader br = new BinaryReader(ms))
291
{
292
byte[] magic = br.ReadBytes(4);
293
if (Encoding.ASCII.GetString(magic) != Magic) return default;
294
byte ver = br.ReadByte();
295
if (ver != Version) return default;
296
int saltLen = br.ReadByte();
297
if (saltLen <= 0 || saltLen > 64) return default;
298
byte[] salt = br.ReadBytes(saltLen);
299
int rotationLen = br.ReadByte();
300
byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16];
301
byte[] iv = br.ReadBytes(IvLen);
302
int cipherLen = br.ReadInt32();
303
if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen);
304
if (cipherLen <= 0 || cipherLen > total.Length) return default;
305
byte[] cipher = br.ReadBytes(cipherLen);
306
byte[] hmac = br.ReadBytes(HmacLen);
307
byte[] ivCipher = new byte[iv.Length + cipher.Length];
308
Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length);
309
Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length);
310
var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null));
311
byte[] aesKey = keys.aesKey;
312
byte[] hmacKey = keys.hmacKey;
313
byte[] expected;
314
using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(ivCipher);
315
bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac);
316
if (!ok)
317
{
318
ClearBytes(aesKey);
319
ClearBytes(hmacKey);
320
ClearBytes(expected);
321
return default;
322
}
323
byte[] plain;
324
using (Aes aes = Aes.Create())
325
{
326
aes.KeySize = 256;
327
aes.Mode = CipherMode.CBC;
328
aes.Padding = PaddingMode.PKCS7;
329
aes.Key = aesKey;
330
aes.IV = iv;
331
using (var dec = aes.CreateDecryptor(aes.Key, aes.IV))
332
plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
333
}
334
string json = Encoding.UTF8.GetString(plain);
335
ClearBytes(aesKey);
336
ClearBytes(hmacKey);
337
ClearBytes(expected);
338
ClearBytes(plain);
339
ClearBytes(iv);
340
ClearBytes(cipher);
341
ClearBytes(salt);
342
ClearBytes(rotation);
343
return JsonUtility.FromJson<T>(json);
344
}
345
}
346
catch { return default; }
347
}
348
349
private static byte[] GenerateRandomBytes(int len)
350
{
351
byte[] b = new byte[len];
352
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b);
353
return b;
354
}
355
356
private static void ClearBytes(byte[] b)
357
{
358
if (b == null) return;
359
Array.Clear(b, 0, b.Length);
360
}
361
362
private string ResolveFileName(string baseName)
363
{
364
if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat";
365
byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier);
366
using (var sha = SHA256.Create())
367
{
368
byte[] hash = sha.ComputeHash(nameBytes);
369
string b64 = Convert.ToBase64String(hash);
370
string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x');
371
ClearBytes(hash);
372
return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat";
373
}
374
}
375
376
private bool IsTamperedOrDebug()
377
{
378
if (!EnableHardening) return false;
379
try
380
{
381
if (EnableAntiDebugChecks)
382
{
383
if (Debug.isDebugBuild) return true;
384
if (Debugger.IsAttached) return true;
385
}
386
}
387
catch { }
388
try
389
{
390
if (EnableRootDetection)
391
{
392
string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" };
393
foreach (var p in suspectPaths) if (File.Exists(p)) return true;
394
}
395
}
396
catch { }
397
return false;
398
}
399
} }}}