Files

88 lines
4.2 KiB
C#

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<byte> Magic => "QSAFE01!"u8;
public static void Validate(ReadOnlySpan<byte> 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<char> password, byte[] salt, int iterations) =>
new(Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, 32), salt, iterations);
public static VaultKey Create(ReadOnlySpan<char> password) => Derive(password, RandomNumberGenerator.GetBytes(16), Iterations);
public static VaultKey Unlock(byte[] envelope, ReadOnlySpan<char> 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<byte> 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);
}
}