using System.Buffers.Binary; using System.Security.Cryptography; namespace QSafe.Core; public sealed class VaultKey(byte[] bytes, byte[] salt, int iterations) : IDisposable { public byte[] Bytes { get; } = bytes; public byte[] Salt { get; } = salt; public int Iterations { get; } = iterations; public void Dispose() => CryptographicOperations.ZeroMemory(Bytes); } public static class VaultCrypto { public const int Iterations = 600_000; public const int MaxBytes = 32 * 1024 * 1024; private static ReadOnlySpan Magic => "QSAFE01!"u8; public static void Validate(ReadOnlySpan envelope) { if (envelope.Length < 56 || envelope.Length > MaxBytes || !envelope[..8].SequenceEqual(Magic)) throw new InvalidDataException("Неверный или повреждённый файл QSafe."); if (BinaryPrimitives.ReadInt32BigEndian(envelope[8..]) is < Iterations or > 2_000_000) throw new InvalidDataException("Неподдерживаемые параметры шифрования."); } private static VaultKey Derive(ReadOnlySpan password, byte[] salt, int iterations) => new(Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, 32), salt, iterations); public static VaultKey Create(ReadOnlySpan password) => Derive(password, RandomNumberGenerator.GetBytes(16), Iterations); public static VaultKey Unlock(byte[] envelope, ReadOnlySpan password) { Validate(envelope); var key = Derive(password, envelope[12..28], BinaryPrimitives.ReadInt32BigEndian(envelope.AsSpan(8))); try { var plain = Decrypt(envelope, key); CryptographicOperations.ZeroMemory(plain); return key; } catch { key.Dispose(); throw; } } public static byte[] Encrypt(ReadOnlySpan plain, VaultKey key) { if (plain.Length > MaxBytes - 56) throw new InvalidDataException("Хранилище превышает 32 МиБ."); var envelope = new byte[plain.Length + 56]; Magic.CopyTo(envelope); BinaryPrimitives.WriteInt32BigEndian(envelope.AsSpan(8), key.Iterations); key.Salt.CopyTo(envelope, 12); RandomNumberGenerator.Fill(envelope.AsSpan(28, 12)); using var aes = new AesGcm(key.Bytes, 16); aes.Encrypt(envelope.AsSpan(28, 12), plain, envelope.AsSpan(40, plain.Length), envelope.AsSpan(40 + plain.Length, 16), envelope.AsSpan(0, 40)); return envelope; } public static byte[] Decrypt(byte[] envelope, VaultKey key) { Validate(envelope); if (!CryptographicOperations.FixedTimeEquals(key.Salt, envelope.AsSpan(12, 16)) || key.Iterations != BinaryPrimitives.ReadInt32BigEndian(envelope.AsSpan(8))) throw new CryptographicException("У хранилища другой ключ или мастер-пароль."); var plain = new byte[envelope.Length - 56]; using var aes = new AesGcm(key.Bytes, 16); try { aes.Decrypt(envelope.AsSpan(28, 12), envelope.AsSpan(40, plain.Length), envelope.AsSpan(envelope.Length - 16), plain, envelope.AsSpan(0, 40)); return plain; } catch { CryptographicOperations.ZeroMemory(plain); throw; } } public static string Hash(byte[] data) => Convert.ToHexStringLower(SHA256.HashData(data)); public static string Generate(int length = 24, bool symbols = true) { if (length is < 16 or > 128) throw new ArgumentOutOfRangeException(nameof(length)); string[] groups = symbols ? ["ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnopqrstuvwxyz", "23456789", "!@#$%&*+-=?"] : ["ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnopqrstuvwxyz", "23456789"]; var alphabet = string.Concat(groups); var result = new char[length]; for (int i = 0; i < length; i++) result[i] = alphabet[RandomNumberGenerator.GetInt32(alphabet.Length)]; for (int i = 0; i < groups.Length; i++) result[i] = groups[i][RandomNumberGenerator.GetInt32(groups[i].Length)]; for (int i = length - 1; i > 0; i--) { int j = RandomNumberGenerator.GetInt32(i + 1); (result[i], result[j]) = (result[j], result[i]); } return new string(result); } }