88 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|