Files
Deal/src/telegram-service/Deal.Telegram/Sessions/SessionFileCipher.cs
T
Rustam Khalimov 713d554dc2 Нормализовать переводы строк в LF
Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF,
CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были
LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения),
.editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize.
Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
2026-09-11 19:01:42 +03:00

102 lines
3.7 KiB
C#

using System.Security.Cryptography;
namespace Deal.Telegram.Sessions;
/// <summary>
/// AES-256-GCM-обёртка файла сессии тенанта.
/// </summary>
public sealed class SessionFileCipher
{
/// <summary>
/// Префикс зашифрованного значения
/// </summary>
public const string EncryptedPrefix = "enc:";
// Размер nonce AES-GCM (рекомендованный NIST — 96 бит).
private const int NonceSizeBytes = 12;
// Размер тега аутентичности.
private const int TagSizeBytes = 16;
private readonly byte[] _key;
/// <summary>
/// Создаёт шифр с ключом из опций
/// </summary>
/// <param name="options">Опции хранения сессий.</param>
public SessionFileCipher(TgOptions options)
{
_key = options.SessionKey;
}
/// <summary>
/// Шифрует содержимое файла сессии
/// </summary>
/// <param name="plainBytes">Открытое содержимое сессии (расшифрованная копия в памяти процесса).</param>
/// <returns>Зашифрованное значение для записи в файл.</returns>
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);
}
/// <summary>
/// Расшифровывает значение файла сессии.
/// </summary>
/// <param name="encryptedValue">Содержимое файла сессии (enc: + base64).</param>
/// <returns>Открытые байты сессии либо null (не наш формат).</returns>
/// <exception cref="CryptographicException">Тег аутентичности не совпал (битый файл/чужой ключ).</exception>
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<byte>(payload, 0, NonceSizeBytes),
new ReadOnlySpan<byte>(payload, NonceSizeBytes, cipherTextLength),
new ReadOnlySpan<byte>(payload, NonceSizeBytes + cipherTextLength, TagSizeBytes),
plainBytes);
}
return plainBytes;
}
}