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); } }