169 lines
6.4 KiB
C#
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);
|
|
}
|
|
} |