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; } }