sega 8a6d23b77d Реорг Scripts
Разделено на 4 модуля
- Menu
- Gameplay
- Settings
- Common
2026-07-15 10:49:55 +03:00

169 lines
6.4 KiB
C#

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class CryptoUtil
{
// Формат пакета (Base64):
// [1 байт Version][16 байт Salt][16 байт IV][N байт Cipher][32 байта HMAC]
private const byte Version = 1;
private const int SaltSize = 16;
private const int IvSize = 16;
private const int HmacSize = 32;
private const int Pbkdf2Iterations = 1000; // можно поднять до 200-300k, если ок по производительности
public static string EncryptJson(string json, string password)
{
if (json == null) throw new ArgumentNullException(nameof(json));
if (string.IsNullOrEmpty(password)) throw new ArgumentException("Password is empty.", nameof(password));
byte[] salt = RandomBytes(SaltSize);
byte[] iv = RandomBytes(IvSize);
// Получаем 64 байта: 32 на AES ключ + 32 на HMAC ключ
DeriveKeys(password, salt, out var encKey, out var macKey);
byte[] plainBytes = Encoding.UTF8.GetBytes(json);
byte[] cipherBytes = AesCbcEncrypt(plainBytes, encKey, iv);
// Собираем заголовок+данные (без HMAC)
byte[] headerAndCipher = new byte[1 + SaltSize + IvSize + cipherBytes.Length];
int offset = 0;
headerAndCipher[offset++] = Version;
Buffer.BlockCopy(salt, 0, headerAndCipher, offset, salt.Length); offset += salt.Length;
Buffer.BlockCopy(iv, 0, headerAndCipher, offset, iv.Length); offset += iv.Length;
Buffer.BlockCopy(cipherBytes, 0, headerAndCipher, offset, cipherBytes.Length);
// HMAC по всему, что выше (version+salt+iv+cipher)
byte[] hmac = ComputeHmac(macKey, headerAndCipher);
// Финальный пакет
byte[] package = new byte[headerAndCipher.Length + hmac.Length];
Buffer.BlockCopy(headerAndCipher, 0, package, 0, headerAndCipher.Length);
Buffer.BlockCopy(hmac, 0, package, headerAndCipher.Length, hmac.Length);
return Convert.ToBase64String(package);
}
public static string DecryptJson(string encryptedBase64, string password)
{
if (encryptedBase64 == null) throw new ArgumentNullException(nameof(encryptedBase64));
if (string.IsNullOrEmpty(password)) throw new ArgumentException("Password is empty.", nameof(password));
byte[] package;
try
{
package = Convert.FromBase64String(encryptedBase64);
}
catch (FormatException)
{
throw new CryptographicException("Input is not valid Base64.");
}
if (package.Length < 1 + SaltSize + IvSize + 1 + HmacSize)
throw new CryptographicException("Encrypted data is too short.");
int minHeader = 1 + SaltSize + IvSize;
int cipherLen = package.Length - minHeader - HmacSize;
if (cipherLen <= 0) throw new CryptographicException("Encrypted data has no ciphertext.");
byte version = package[0];
if (version != Version) throw new CryptographicException($"Unsupported version: {version}");
byte[] salt = new byte[SaltSize];
byte[] iv = new byte[IvSize];
byte[] cipher = new byte[cipherLen];
byte[] hmac = new byte[HmacSize];
Buffer.BlockCopy(package, 1, salt, 0, SaltSize);
Buffer.BlockCopy(package, 1 + SaltSize, iv, 0, IvSize);
Buffer.BlockCopy(package, minHeader, cipher, 0, cipherLen);
Buffer.BlockCopy(package, minHeader + cipherLen, hmac, 0, HmacSize);
DeriveKeys(password, salt, out var encKey, out var macKey);
// Проверка HMAC (важно: до расшифровки)
byte[] headerAndCipher = new byte[minHeader + cipherLen];
Buffer.BlockCopy(package, 0, headerAndCipher, 0, headerAndCipher.Length);
byte[] expectedHmac = ComputeHmac(macKey, headerAndCipher);
if (!FixedTimeEquals(hmac, expectedHmac))
throw new CryptographicException("Invalid password or data was tampered (HMAC mismatch).");
byte[] plainBytes = AesCbcDecrypt(cipher, encKey, iv);
return Encoding.UTF8.GetString(plainBytes);
}
// ===== helpers =====
private static void DeriveKeys(string password, byte[] salt, out byte[] encKey, out byte[] macKey)
{
using var kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256);
byte[] keyMaterial = kdf.GetBytes(64);
encKey = new byte[32];
macKey = new byte[32];
Buffer.BlockCopy(keyMaterial, 0, encKey, 0, 32);
Buffer.BlockCopy(keyMaterial, 32, macKey, 0, 32);
// чуть безопаснее: затереть временный буфер
Array.Clear(keyMaterial, 0, keyMaterial.Length);
}
private static byte[] AesCbcEncrypt(byte[] plain, byte[] key, byte[] iv)
{
using var aes = Aes.Create();
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
using var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(plain, 0, plain.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
private static byte[] AesCbcDecrypt(byte[] cipher, byte[] key, byte[] iv)
{
using var aes = Aes.Create();
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
using var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(cipher, 0, cipher.Length);
cs.FlushFinalBlock();
}
return ms.ToArray();
}
private static byte[] ComputeHmac(byte[] macKey, byte[] data)
{
using var h = new HMACSHA256(macKey);
return h.ComputeHash(data);
}
private static byte[] RandomBytes(int size)
{
var b = new byte[size];
RandomNumberGenerator.Fill(b);
return b;
}
private static bool FixedTimeEquals(byte[] a, byte[] b)
{
if (a == null || b == null || a.Length != b.Length) return false;
return CryptographicOperations.FixedTimeEquals(a, b);
}
}