Решение по 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.
102 lines
3.7 KiB
C#
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;
|
|
}
|
|
}
|