using System.Security.Cryptography;
namespace Deal.Telegram.Sessions;
///
/// AES-256-GCM-обёртка файла сессии тенанта.
///
public sealed class SessionFileCipher
{
///
/// Префикс зашифрованного значения
///
public const string EncryptedPrefix = "enc:";
// Размер nonce AES-GCM (рекомендованный NIST — 96 бит).
private const int NonceSizeBytes = 12;
// Размер тега аутентичности.
private const int TagSizeBytes = 16;
private readonly byte[] _key;
///
/// Создаёт шифр с ключом из опций
///
/// Опции хранения сессий.
public SessionFileCipher(TgOptions options)
{
_key = options.SessionKey;
}
///
/// Шифрует содержимое файла сессии
///
/// Открытое содержимое сессии (расшифрованная копия в памяти процесса).
/// Зашифрованное значение для записи в файл.
public string EncryptBytes(byte[] plainBytes)
{
ArgumentNullException.ThrowIfNull(plainBytes);
byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeBytes);
byte[] cipherBytes = new byte[plainBytes.Length];
byte[] tag = new byte[TagSizeBytes];
using (AesGcm aesGcm = new AesGcm(_key, TagSizeBytes))
{
aesGcm.Encrypt(nonce, plainBytes, cipherBytes, tag);
}
byte[] payload = new byte[nonce.Length + cipherBytes.Length + tag.Length];
nonce.CopyTo(payload, 0);
cipherBytes.CopyTo(payload, nonce.Length);
tag.CopyTo(payload, nonce.Length + cipherBytes.Length);
return EncryptedPrefix + Convert.ToBase64String(payload);
}
///
/// Расшифровывает значение файла сессии.
///
/// Содержимое файла сессии (enc: + base64).
/// Открытые байты сессии либо null (не наш формат).
/// Тег аутентичности не совпал (битый файл/чужой ключ).
public byte[]? DecryptBytes(string encryptedValue)
{
if (string.IsNullOrEmpty(encryptedValue) || !encryptedValue.StartsWith(EncryptedPrefix, StringComparison.Ordinal))
{
return null;
}
byte[] payload;
try
{
payload = Convert.FromBase64String(encryptedValue[EncryptedPrefix.Length..]);
}
catch (FormatException)
{
return null;
}
if (payload.Length < NonceSizeBytes + TagSizeBytes)
{
// Слишком короткий payload: nonce и tag в нём не помещаются.
return null;
}
int cipherTextLength = payload.Length - NonceSizeBytes - TagSizeBytes;
byte[] plainBytes = new byte[cipherTextLength];
using (AesGcm aesGcm = new AesGcm(_key, TagSizeBytes))
{
aesGcm.Decrypt(
new ReadOnlySpan(payload, 0, NonceSizeBytes),
new ReadOnlySpan(payload, NonceSizeBytes, cipherTextLength),
new ReadOnlySpan(payload, NonceSizeBytes + cipherTextLength, TagSizeBytes),
plainBytes);
}
return plainBytes;
}
}