- Статус
- Оффлайн
- Регистрация
- 13 Фев 2026
- Сообщения
- 394
- Реакции
- 7
Народ, кто до сих пор хранит конфиги, дллки или ресурсы своих лоадеров в открытом виде или юзает дефолтный XOR — пора переходить на нормальные рельсы. Попал в руки годный класс для реализации действительно криптостойкого шифрования на C#.
Это не просто «паста» с MSDN. Здесь реализован грамотный стек: AES-256 для контента и PBKDF2 (SHA512) для деривации ключа. Из приятных плюшек — использование Pepper (дополнительная статическая соль) и автоматическое сжатие GZip перед шифрованием. Последнее не только уменьшает вес, но и ломает структуру файла, что полезно против сигнатурного анализа.
Что внутри тех-части:
Почему это FUD?
Потому что здесь используются стандартные системные библиотеки .NET без кастомных и подозрительных оберток, которые любят детектить проактивки. Сжатие данных перед шифрованием полностью меняет энтропию файла, делая его «белым шумом» для любого сканера.
Совет по внедрению:
Если юзаете это в лоадере, зашивайте _pepper в константы или генерируйте его на основе HWID юзера. Это создаст дополнительный геморрой для тех, кто захочет вскрыть ваши ресурсы на другом ПК.
Кто уже пробовал связку AES + GZip для защиты ассетов в своих проектах, как полет?
Это не просто «паста» с MSDN. Здесь реализован грамотный стек: AES-256 для контента и PBKDF2 (SHA512) для деривации ключа. Из приятных плюшек — использование Pepper (дополнительная статическая соль) и автоматическое сжатие GZip перед шифрованием. Последнее не только уменьшает вес, но и ломает структуру файла, что полезно против сигнатурного анализа.
Что внутри тех-части:
- Алгоритм: AES-256 (CipherMode.CBC, PaddingMode.PKCS7).
- Деривация: PBKDF2 с 100,000 итераций (медленно для брутфорса, надежно для нас).
- Безопасность: Юзается SecureString для паролей, чтобы не светить ими в открытой памяти (RAM).
- Очистка: Ручное затирание массивов байтов через Array.Clear после использования.
- Соль и Пеппер: 128-bit salt + 32-byte pepper.
Код:
using System;
using System.IO;
using System.IO.Compression;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Encryption
{
public sealed class Encryptor : IDisposable
{
private const int KeySize = 256; // AES-256
private const int Iterations = 100000; // PBKDF2 iteration count
private const int SaltSize = 128; // 128-bit salt
private const int PepperSize = 32; // 256-bit pepper
private const int MinPasswordLength = 12;
private readonly byte[] _pepper;
private bool _disposed = false;
public Encryptor()
{
try
{
// Generate application-specific pepper
_pepper = new byte[PepperSize];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(_pepper);
}
}
catch (Exception ex)
{
throw new CryptographicException("Failed to initialize encryptor", ex);
}
}
public void EncryptFile(string inputFile, string outputFile, SecureString password)
{
ValidateParameters(inputFile, outputFile, password);
try
{
byte[] salt = GenerateSalt();
byte[] compressedData = CompressFile(inputFile);
byte[] passwordBytes = SecureStringToBytes(password);
try
{
PerformEncryption(outputFile, salt, compressedData, passwordBytes);
}
finally
{
SecureClear(compressedData);
SecureClear(passwordBytes);
}
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
throw new CryptographicException($"Encryption failed: {ex.Message}", ex);
}
}
public void DecryptFile(string inputFile, string outputFile, SecureString password)
{
ValidateParameters(inputFile, outputFile, password);
try
{
byte[] passwordBytes = SecureStringToBytes(password);
try
{
byte[] salt = ReadSaltFromFile(inputFile);
byte[] decryptedData = PerformDecryption(inputFile, passwordBytes, salt);
DecompressToFile(decryptedData, outputFile);
SecureClear(decryptedData);
}
finally
{
SecureClear(passwordBytes);
}
}
catch (CryptographicException)
{
throw; // Preserve original cryptographic exceptions
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
throw new CryptographicException($"Decryption failed: {ex.Message}", ex);
}
}
#region Core Operations
private byte[] GenerateSalt()
{
var salt = new byte[SaltSize / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
return salt;
}
private void PerformEncryption(string outputFile, byte[] salt, byte[] data, byte[] passwordBytes)
{
using (var derivedKey = new Rfc2898DeriveBytes(
password: Combine(passwordBytes, salt),
salt: Combine(salt, _pepper),
iterations: Iterations,
hashAlgorithm: HashAlgorithmName.SHA512))
{
byte[] key = derivedKey.GetBytes(KeySize / 8);
byte[] iv = derivedKey.GetBytes(128 / 8);
try
{
using (var aes = Aes.Create())
{
ConfigureAes(aes, key, iv);
WriteEncryptedFile(outputFile, salt, data, aes);
}
}
finally
{
SecureClear(key);
SecureClear(iv);
}
}
}
private byte[] PerformDecryption(string inputFile, byte[] passwordBytes, byte[] salt)
{
using (var derivedKey = new Rfc2898DeriveBytes(
password: Combine(passwordBytes, salt),
salt: Combine(salt, _pepper),
iterations: Iterations,
hashAlgorithm: HashAlgorithmName.SHA512))
{
byte[] key = derivedKey.GetBytes(KeySize / 8);
byte[] iv = derivedKey.GetBytes(128 / 8);
try
{
using (var aes = Aes.Create())
{
ConfigureAes(aes, key, iv);
return ReadAndDecryptFile(inputFile, salt.Length, aes);
}
}
finally
{
SecureClear(key);
SecureClear(iv);
}
}
}
#endregion
#region File Operations
private byte[] CompressFile(string inputFile)
{
try
{
using (var memoryStream = new MemoryStream())
{
using (var gzipStream = new GZipStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
using (var fileStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
fileStream.CopyTo(gzipStream);
}
return memoryStream.ToArray();
}
}
catch (IOException ex)
{
throw new InvalidOperationException($"Compression failed for {inputFile}", ex);
}
}
private void DecompressToFile(byte[] compressedData, string outputFile)
{
try
{
using (var memoryStream = new MemoryStream(compressedData))
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
using (var fileStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
{
gzipStream.CopyTo(fileStream);
}
}
catch (InvalidDataException ex)
{
throw new InvalidOperationException("Invalid compressed data format", ex);
}
catch (IOException ex)
{
throw new InvalidOperationException($"Failed to write decompressed file to {outputFile}", ex);
}
}
private byte[] ReadSaltFromFile(string inputFile)
{
try
{
using (var fileStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
{
byte[] salt = new byte[SaltSize / 8];
int bytesRead = fileStream.Read(salt, 0, salt.Length);
if (bytesRead != salt.Length)
{
throw new InvalidDataException("File is too short to contain salt");
}
return salt;
}
}
catch (IOException ex)
{
throw new InvalidOperationException($"Failed to read salt from {inputFile}", ex);
}
}
private void WriteEncryptedFile(string outputPath, byte[] salt, byte[] data, SymmetricAlgorithm algorithm)
{
try
{
using (var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
// Write salt
fileStream.Write(salt, 0, salt.Length);
// Write encrypted data
using (var cryptoStream = new CryptoStream(
fileStream,
algorithm.CreateEncryptor(),
CryptoStreamMode.Write))
{
cryptoStream.Write(data, 0, data.Length);
cryptoStream.FlushFinalBlock();
}
}
}
catch (IOException ex)
{
throw new InvalidOperationException($"Failed to write encrypted file to {outputPath}", ex);
}
}
private byte[] ReadAndDecryptFile(string inputPath, int saltLength, SymmetricAlgorithm algorithm)
{
try
{
using (var fileStream = new FileStream(inputPath, FileMode.Open, FileAccess.Read))
{
// Skip salt
fileStream.Position = saltLength;
using (var memoryStream = new MemoryStream())
using (var cryptoStream = new CryptoStream(
fileStream,
algorithm.CreateDecryptor(),
CryptoStreamMode.Read))
{
cryptoStream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}
}
catch (IOException ex)
{
throw new InvalidOperationException($"Failed to read encrypted file from {inputPath}", ex);
}
}
#endregion
#region Security Utilities
private byte[] SecureStringToBytes(SecureString secureString)
{
IntPtr bstr = IntPtr.Zero;
try
{
bstr = Marshal.SecureStringToBSTR(secureString);
int length = Marshal.ReadInt32(bstr, -4);
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++)
{
bytes[i] = Marshal.ReadByte(bstr, i);
}
return bytes;
}
finally
{
if (bstr != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(bstr);
}
}
}
private void SecureClear(byte[] data)
{
if (data != null)
{
Array.Clear(data, 0, data.Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte[] Combine(byte[] first, byte[] second)
{
var combined = new byte[first.Length + second.Length];
Buffer.BlockCopy(first, 0, combined, 0, first.Length);
Buffer.BlockCopy(second, 0, combined, first.Length, second.Length);
return combined;
}
private void ConfigureAes(SymmetricAlgorithm algorithm, byte[] key, byte[] iv)
{
algorithm.Key = key;
algorithm.IV = iv;
algorithm.Mode = CipherMode.CBC;
algorithm.Padding = PaddingMode.PKCS7;
}
#endregion
#region Validation
private void ValidateParameters(string inputFile, string outputFile, SecureString password)
{
if (string.IsNullOrWhiteSpace(inputFile))
throw new ArgumentNullException(nameof(inputFile), "Input file path cannot be empty");
if (string.IsNullOrWhiteSpace(outputFile))
throw new ArgumentNullException(nameof(outputFile), "Output file path cannot be empty");
if (password == null)
throw new ArgumentNullException(nameof(password), "Password cannot be null");
if (password.Length < MinPasswordLength)
throw new ArgumentException($"Password must be at least {MinPasswordLength} characters", nameof(password));
if (!File.Exists(inputFile))
throw new FileNotFoundException("Input file not found", inputFile);
try
{
string outputDir = Path.GetDirectoryName(outputFile);
if (!Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
}
catch (Exception ex)
{
throw new ArgumentException($"Invalid output path: {ex.Message}", nameof(outputFile), ex);
}
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && _pepper != null)
{
SecureClear(_pepper);
}
_disposed = true;
}
}
~Encryptor()
{
Dispose(false);
}
#endregion
}
}
Почему это FUD?
Потому что здесь используются стандартные системные библиотеки .NET без кастомных и подозрительных оберток, которые любят детектить проактивки. Сжатие данных перед шифрованием полностью меняет энтропию файла, делая его «белым шумом» для любого сканера.
Совет по внедрению:
Если юзаете это в лоадере, зашивайте _pepper в константы или генерируйте его на основе HWID юзера. Это создаст дополнительный геморрой для тех, кто захочет вскрыть ваши ресурсы на другом ПК.
Кто уже пробовал связку AES + GZip для защиты ассетов в своих проектах, как полет?