Danil Iarosh 79b66b2d17
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat: add controller disconnect/reconnect handling, MAC identification, lighting pause/resume system
- Рефакторинг системы подсветки: вместо длительности в секундах теперь
  используется количество циклов (float). Добавлена таблица длительности
  циклов эффектов `LightCycleDurations` для пересчёта циклов в секунды.
  Префикс команд подсветки изменён с `85` на `86`.
- `LightSequence`: добавлены `Pause()` / `Resume()` с запоминанием текущего
  шага последовательности; возобновление продолжает с сохранённой позиции.
- `LightController`: добавлены `PauseAll()` / `ResumeAll()` для
  приостановки и восстановления подсветки и мигания шляпы при
  отключении/переподключении платы. Свойства `IsSequencePaused`,
  `IsBlinkPaused`.
- `ComDeviceClient`: после успешной авторизации отправляются команды
  `890003` и `8600`. Добавлен запрос MAC-адреса через команду `06`
  (ответ — 12 hex-символов без префикса). Новое событие `ConnectionLost`
  при начале KeepAlive-реконнекта.
- `ComHub`: очередь `_disconnectedQueue` с типом `DeviceDisconnectMessage`
  (DeviceId + Role + Mac) — сохраняет идентификатор устройства до его
  удаления из словаря. Подписка на `ConnectionLost`.
  Добавлен `GetDeviceMac()`.
- `ComSystemManager`: при инициализации, если плата не обнаружена,
  подсветка сразу ставится на паузу. При отвале дефолтного устройства —
  `PauseAll()`, при переподключении — сравнение по MAC и роли через
  `IsSameDevice()`: та же плата → `ResumeAll()`, другая → `Blink()` с нуля.
  Добавлены событие `OnDeviceDisconnected` и метод `GetDeviceMac()`.
- `COMInput`: при событии `OnDeviceDisconnected` сбрасываются все состояния
  кнопок через `ResetAllButtons()`, генерируя `buttonUp` для зажатых кнопок.
  Устраняет баг залипания управления после переподключения платы.
- `GameManager`: расширена последовательность световых эффектов в меню
  (добавлены `DiagonalWave3`, `WaveUp`, `OrangeWave`); вызовы `SetLight`
  для game over/won переведены на float-циклы.
- Обновлены `ARCHITECTURE.md` и `README.md`: переписана документация
  на актуальную структуру проекта и текущие возможности игры.
2026-03-04 13:55:08 +03:00

866 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RR.Game2D.Core.Managers;
using UnityEngine;
namespace RR.Common.Controls
{
internal sealed class ComDeviceClient
{
private const string RequestInput = "91";
private const string DefaultInputBits = "11111111111111111111111111";
private const long CoinDebounceMs = 250;
private static readonly byte[] AesEncryptedKey =
{
0x69, 0x40, 0xC7, 0x0E, 0xF2, 0xC7, 0x45, 0x70,
0x72, 0xCD, 0x28, 0x58, 0x99, 0x8F, 0x73, 0xA8,
0x5F, 0x5F, 0xED, 0xB4, 0xC1, 0x62, 0xB4, 0x24,
0xAC, 0xD7, 0xA3, 0xD6, 0xD8, 0x0B, 0x50, 0x37
};
private readonly object _ioLock = new object();
private readonly bool _enableEncryptedHandshake;
private readonly int _handshakeTimeoutMs;
private readonly bool _keepAlive;
private readonly string _roleRequestCommand;
private readonly string _roleResponsePrefix;
private readonly int _roleRequestTimeoutMs;
private CancellationTokenSource _lifecycleCts;
private Task _pollTask;
private Task _keepAliveTask;
private int _handle;
private long _lastCoinTime;
private int _inputToggle;
private int _missedInputResponses;
private bool _isAuthorized;
private bool _isOperational;
private bool _handshakeInProgress;
private string _requestHex = string.Empty;
private string _lastHandshakeResponse = string.Empty;
private string _lastInputBits = DefaultInputBits;
private byte[] _aesKey32;
private HandshakeStage _handshakeStage = HandshakeStage.None;
private TaskCompletionSource<bool> _handshakeCompletion;
private TaskCompletionSource<bool> _chalReceivedCompletion;
private TaskCompletionSource<string> _roleCompletion;
private TaskCompletionSource<string> _macCompletion;
private enum HandshakeStage
{
None,
WaitingChal,
WaitingRaes,
Completed,
Failed
}
public string DeviceId { get; }
public string PortName { get; }
public bool IsOperational => _isOperational;
public string LastInputBits => _lastInputBits;
public string ControllerRole { get; private set; } = string.Empty;
public string MacAddress { get; private set; } = string.Empty;
public event Action<string> CoinAccepted;
public event Action<string, string> InputReceived;
public event Action<string, string> RawLineReceived;
public event Action<string> Authorized;
public event Action<string> Disconnected;
public event Action<string> ConnectionLost;
public ComDeviceClient(
string portName,
bool enableEncryptedHandshake,
int handshakeTimeoutMs,
bool keepAlive,
string roleRequestCommand,
string roleResponsePrefix,
int roleRequestTimeoutMs)
{
PortName = portName;
DeviceId = portName;
_enableEncryptedHandshake = enableEncryptedHandshake;
_handshakeTimeoutMs = Math.Max(500, handshakeTimeoutMs);
_keepAlive = keepAlive;
_roleRequestCommand = roleRequestCommand?.Trim() ?? string.Empty;
_roleResponsePrefix = string.IsNullOrWhiteSpace(roleResponsePrefix) ? "ROLE" : roleResponsePrefix.Trim();
_roleRequestTimeoutMs = Math.Max(200, roleRequestTimeoutMs);
if (_enableEncryptedHandshake)
{
_aesKey32 = BuildAesKey();
}
}
public bool Start()
{
if (!OpenPort()) return false;
_lifecycleCts = new CancellationTokenSource();
_pollTask = Task.Run(() => PollLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
if (_enableEncryptedHandshake)
{
_ = RunHandshakeAsync(_lifecycleCts.Token);
}
else
{
MarkOperational();
}
return true;
}
public void Stop(bool sendShutdownCommands)
{
try
{
_lifecycleCts?.Cancel();
}
catch
{
// ignored
}
if (sendShutdownCommands)
{
SendRaw("2170", true);
SendRaw("2130", true);
}
lock (_ioLock)
{
if (_handle > 0)
{
ComNativeWrapper.Serial_Close(_handle);
_handle = 0;
}
}
_isOperational = false;
_isAuthorized = false;
_handshakeInProgress = false;
_handshakeStage = HandshakeStage.None;
_handshakeCompletion?.TrySetCanceled();
_chalReceivedCompletion?.TrySetCanceled();
_roleCompletion?.TrySetCanceled();
_macCompletion?.TrySetCanceled();
ControllerRole = string.Empty;
_keepAliveTask = null;
Disconnected?.Invoke(DeviceId);
_lifecycleCts?.Dispose();
_lifecycleCts = null;
}
public void Send(string message)
{
SendRaw(message, false);
}
private bool OpenPort()
{
int handle = ComNativeWrapper.Serial_Open(PortName, 115200, 8, 0, 1, 5, 10);
if (handle <= 0)
{
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Open failed: {ComNativeWrapper.GetLastError()}");
return false;
}
lock (_ioLock)
{
_handle = handle;
}
_lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
FlushPortBuffer();
return true;
}
private async Task PollLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(1, token);
string line = ReadBufferLine();
if (string.IsNullOrEmpty(line)) continue;
ProcessLine(line);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception e)
{
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Poll error: {e.Message}");
}
}
}
private void ProcessLine(string line)
{
line = line.Trim();
if (string.IsNullOrEmpty(line)) return;
if (_handshakeInProgress)
{
TryProcessHandshakeLine(line);
return;
}
if (!_isOperational) return;
if (TryHandleRoleLine(line))
{
return;
}
if (TryHandleMacLine(line))
{
return;
}
if (line.StartsWith("INPUT", StringComparison.Ordinal))
{
_inputToggle = 1;
if (line.Length <= 5) return;
string rawData = line.Substring(5);
if (string.IsNullOrWhiteSpace(rawData)) return;
string bits = DecimalToBinary(rawData);
_lastInputBits = bits;
InputReceived?.Invoke(DeviceId, bits);
return;
}
RawLineReceived?.Invoke(DeviceId, line);
}
private async Task RunHandshakeAsync(CancellationToken token)
{
_handshakeInProgress = true;
_isAuthorized = false;
_isOperational = false;
_handshakeStage = HandshakeStage.WaitingChal;
_lastHandshakeResponse = string.Empty;
_handshakeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_chalReceivedCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Debug.Log($"[COM AUTH][{DeviceId}] Handshake started. Stage={_handshakeStage}");
_requestHex = GenerateRandomBlock16Hex();
// Порядок handshake по протоколу платы:
// PC -> 07[random], board -> CHAL, PC -> 08[enc(CHAL)], board -> RAES, board -> AUTH_CNT_OK.
SendRaw("07" + _requestHex, true);
Debug.Log($"[COM AUTH][{DeviceId}] -> 07{_requestHex}");
// Если на 07 не получили ожидаемый CHAL быстро, сразу пропускаем устройство.
int chalFastTimeoutMs = Math.Min(1200, Math.Max(250, _handshakeTimeoutMs / 4));
Task chalFastTimeoutTask = Task.Delay(chalFastTimeoutMs, token);
Task chalFastFinished = await Task.WhenAny(_chalReceivedCompletion.Task, chalFastTimeoutTask);
if (chalFastFinished != _chalReceivedCompletion.Task)
{
Debug.LogWarning($"[COM AUTH][{DeviceId}] No CHAL after 07 in {chalFastTimeoutMs}ms. Skipping device.");
FailHandshake($"Нет ожидаемого CHAL на 07 за {chalFastTimeoutMs}ms");
return;
}
Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token);
Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask);
if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result)
{
Debug.Log($"[COM AUTH][{DeviceId}] Handshake completed successfully");
MarkOperational();
return;
}
GameManager.Logger.LogWarning($"COM AUTH [{DeviceId}]", $"Handshake timeout. Last board response: {_lastHandshakeResponse}");
FailHandshake($"Таймаут рукопожатия. Последний ответ платы: {_lastHandshakeResponse}");
}
private void TryProcessHandshakeLine(string line)
{
Debug.Log($"[COM AUTH][{DeviceId}] <- {line}; stage={_handshakeStage}");
_lastHandshakeResponse = line;
// Во время handshake обрабатываем только ожидаемые сообщения:
// WaitingChal -> CHAL
// WaitingRaes -> RAES
// Все остальные строки игнорируем.
if (_handshakeStage == HandshakeStage.WaitingChal)
{
if (line.StartsWith("CHAL", StringComparison.Ordinal))
{
HandleChal(line);
}
else
{
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingChal: {line}");
}
return;
}
if (_handshakeStage == HandshakeStage.WaitingRaes)
{
if (line.StartsWith("RAES", StringComparison.Ordinal))
{
HandleRaes(line);
}
else
{
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingRaes: {line}");
}
return;
}
Debug.Log($"[COM AUTH][{DeviceId}] Ignored in stage {_handshakeStage}: {line}");
}
private void HandleRaes(string line)
{
try
{
Debug.Log($"[COM AUTH][{DeviceId}] Handling RAES...");
if (_handshakeStage != HandshakeStage.WaitingRaes)
{
FailHandshake($"RAES в неверной фазе: {_handshakeStage}");
return;
}
if (line.Length < 36)
{
FailHandshake("Некорректная длина RAES");
return;
}
string hex = line.Substring(4, 32);
if (!IsHexString(hex, 32))
{
FailHandshake("RAES содержит некорректный HEX");
return;
}
string decrypted = DecryptHexBlock16(hex);
if (!string.Equals(decrypted, _requestHex, StringComparison.OrdinalIgnoreCase))
{
FailHandshake("RAES не совпадает с запросом 07");
return;
}
_handshakeStage = HandshakeStage.Completed;
_isAuthorized = true;
Debug.Log($"[COM AUTH][{DeviceId}] RAES verified. Raw RAES={line}; decrypted={decrypted}; handshake complete");
_handshakeCompletion?.TrySetResult(true);
}
catch (Exception e)
{
FailHandshake($"Ошибка RAES: {e.Message}");
}
}
private void HandleChal(string line)
{
try
{
Debug.Log($"[COM AUTH][{DeviceId}] Handling CHAL...");
if (_handshakeStage != HandshakeStage.WaitingChal)
{
FailHandshake($"CHAL в неверной фазе: {_handshakeStage}");
return;
}
_chalReceivedCompletion?.TrySetResult(true);
if (line.Length < 36)
{
FailHandshake("Некорректная длина CHAL");
return;
}
string hex = line.Substring(4, 32);
if (!IsHexString(hex, 32))
{
FailHandshake("CHAL содержит некорректный HEX");
return;
}
string encrypted = EncryptHexBlock16(hex);
SendRaw("08" + encrypted, true);
_handshakeStage = HandshakeStage.WaitingRaes;
Debug.Log($"[COM AUTH][{DeviceId}] -> 08{encrypted}; stage={_handshakeStage}");
}
catch (Exception e)
{
FailHandshake($"Ошибка CHAL: {e.Message}");
}
}
private void MarkOperational()
{
_handshakeInProgress = false;
_isAuthorized = true;
_isOperational = true;
_handshakeStage = HandshakeStage.Completed;
ControllerRole = string.Empty;
_missedInputResponses = 0;
Debug.Log($"[COM AUTH][{DeviceId}] Device marked operational");
SendRaw("2171", true);
SendRaw("890003", true);
SendRaw("8600", true);
if (_keepAlive && (_keepAliveTask == null || _keepAliveTask.IsCompleted))
{
_keepAliveTask = Task.Run(() => KeepAliveLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
}
_ = Task.Run(() => ResolveRoleAfterAuthorizationAsync(_lifecycleCts.Token), _lifecycleCts.Token);
}
private void FailHandshake(string message)
{
_isAuthorized = false;
_isOperational = false;
_handshakeInProgress = false;
_handshakeStage = HandshakeStage.Failed;
_handshakeCompletion?.TrySetResult(false);
Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake failed: {message}");
GameManager.Logger?.LogWarning("ComDeviceClient", $"[{DeviceId}] {message}");
// Если порт отвечает не по ожидаемому протоколу платы,
// отключаем устройство, чтобы менеджер выбрал другой порт.
Stop(false);
}
private async Task KeepAliveLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(2000, token);
if (!_isOperational)
{
await Task.Delay(8000, token);
continue;
}
SendRaw(RequestInput, false);
_inputToggle = 0;
await Task.Delay(1000, token);
if (_inputToggle == 0)
{
_missedInputResponses++;
if (_missedInputResponses >= 3)
{
GameManager.Logger?.LogWarning("ComDeviceClient",
$"[{DeviceId}] Нет INPUT-ответа {_missedInputResponses} цикла подряд, выполняю reconnect");
_missedInputResponses = 0;
await ReconnectAsync(token);
}
}
else
{
_missedInputResponses = 0;
}
await Task.Delay(7000, token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception e)
{
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] KeepAlive error: {e.Message}");
}
}
}
private async Task ReconnectAsync(CancellationToken token)
{
_isOperational = false;
_isAuthorized = false;
_handshakeInProgress = false;
_handshakeStage = HandshakeStage.None;
_lastInputBits = DefaultInputBits;
_inputToggle = 0;
ControllerRole = string.Empty;
MacAddress = string.Empty;
ConnectionLost?.Invoke(DeviceId);
lock (_ioLock)
{
if (_handle > 0)
{
ComNativeWrapper.Serial_Close(_handle);
_handle = 0;
}
}
if (!OpenPort())
{
// Порт недоступен при переподключении: отключаем устройство и даем менеджеру
// переключиться на другие доступные порты.
Stop(false);
return;
}
if (_enableEncryptedHandshake)
{
await RunHandshakeAsync(token);
}
else
{
MarkOperational();
}
}
private void FlushPortBuffer()
{
lock (_ioLock)
{
if (_handle <= 0) return;
byte[] localBuf = new byte[1024];
int flushedCount = 0;
int read;
while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length)) > 0)
{
flushedCount++;
if (flushedCount > 100) break;
}
}
}
private string ReadBufferLine()
{
lock (_ioLock)
{
if (_handle <= 0) return string.Empty;
byte[] localBuf = new byte[1024];
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length);
if (read <= 0) return string.Empty;
if (read > localBuf.Length) read = localBuf.Length;
string line = Encoding.UTF8.GetString(localBuf, 0, read).Trim();
if (line != "COIN") return line;
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now - _lastCoinTime < CoinDebounceMs) return string.Empty;
_lastCoinTime = now;
if (_isOperational)
{
ComNativeWrapper.Serial_WriteStringUTF8(_handle, "10", 1);
CoinAccepted?.Invoke(DeviceId);
}
return string.Empty;
}
}
private void SendRaw(string message, bool bypassAuthorization)
{
if (string.IsNullOrWhiteSpace(message)) return;
if (!bypassAuthorization && !_isOperational) return;
lock (_ioLock)
{
if (_handle <= 0) return;
ComNativeWrapper.Serial_WriteStringUTF8(_handle, message, 1);
}
}
private bool TryHandleRoleLine(string line)
{
if (_roleCompletion == null) return false;
if (!_roleCompletion.Task.IsCompleted && TryExtractRole(line, out string role))
{
ControllerRole = role;
_roleCompletion.TrySetResult(role);
return true;
}
return false;
}
private bool TryHandleMacLine(string line)
{
if (_macCompletion == null) return false;
if (!_macCompletion.Task.IsCompleted && TryExtractMac(line, out string mac))
{
MacAddress = mac;
_macCompletion.TrySetResult(mac);
return true;
}
return false;
}
private static bool TryExtractMac(string line, out string mac)
{
mac = string.Empty;
if (string.IsNullOrWhiteSpace(line)) return false;
string trimmed = line.Trim();
// Ответ на "06" — голый MAC без двоеточий, 12 hex-символов
if (trimmed.Length == 12 && IsHexString(trimmed))
{
mac = trimmed.ToUpperInvariant();
return true;
}
return false;
}
private static bool IsHexString(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
return false;
}
return true;
}
private async Task ResolveRoleAfterAuthorizationAsync(CancellationToken token)
{
// Роль и MAC считываем только после завершенной авторизации рукопожатия.
if (!_enableEncryptedHandshake || !_isAuthorized || string.IsNullOrWhiteSpace(_roleRequestCommand))
{
Authorized?.Invoke(DeviceId);
return;
}
try
{
// --- Запрос роли ---
_roleCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRaw(_roleRequestCommand, false);
Task timeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
Task finished = await Task.WhenAny(_roleCompletion.Task, timeoutTask);
if (finished == _roleCompletion.Task &&
_roleCompletion.Task.IsCompletedSuccessfully &&
!string.IsNullOrWhiteSpace(_roleCompletion.Task.Result))
{
ControllerRole = _roleCompletion.Task.Result;
}
else
{
GameManager.Logger?.LogWarning("ComDeviceClient",
$"[{DeviceId}] Не удалось получить роль контроллера за {_roleRequestTimeoutMs}ms");
}
// --- Запрос MAC-адреса ---
_macCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
SendRaw("06", false);
Task macTimeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
Task macFinished = await Task.WhenAny(_macCompletion.Task, macTimeoutTask);
if (macFinished == _macCompletion.Task &&
_macCompletion.Task.IsCompletedSuccessfully &&
!string.IsNullOrWhiteSpace(_macCompletion.Task.Result))
{
MacAddress = _macCompletion.Task.Result;
GameManager.Logger?.Log("ComDeviceClient",
$"[{DeviceId}] MAC-адрес: {MacAddress}");
}
else
{
GameManager.Logger?.LogWarning("ComDeviceClient",
$"[{DeviceId}] Не удалось получить MAC-адрес за {_roleRequestTimeoutMs}ms");
}
}
catch (OperationCanceledException)
{
// ignore
}
catch (Exception e)
{
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role/MAC resolve error: {e.Message}");
}
finally
{
Authorized?.Invoke(DeviceId);
}
}
private bool TryExtractRole(string line, out string role)
{
role = string.Empty;
if (string.IsNullOrWhiteSpace(line)) return false;
string trimmed = line.Trim();
if (!trimmed.StartsWith(_roleResponsePrefix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
string tail = trimmed.Substring(_roleResponsePrefix.Length).Trim();
if (tail.StartsWith(":") || tail.StartsWith("="))
{
tail = tail.Substring(1).Trim();
}
if (string.IsNullOrWhiteSpace(tail)) return false;
role = tail;
return true;
}
private static string DecimalToBinary(string decimalString)
{
try
{
if (!long.TryParse(decimalString, out long value)) return DefaultInputBits;
string binary = Convert.ToString(value, 2);
char[] chars = binary.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
catch
{
return DefaultInputBits;
}
}
private static byte[] BuildAesKey()
{
byte[] key = new byte[32];
for (int i = 0; i < key.Length; i++)
{
key[i] = (byte)(AesEncryptedKey[i] ^ 0x37);
}
return key;
}
private string EncryptHexBlock16(string plainHex)
{
byte[] plain = HexToBytes(plainHex);
if (plain.Length != 16) throw new InvalidOperationException("Plain must be 16 bytes.");
return BytesToHex(Aes256EcbEncrypt(plain));
}
private string DecryptHexBlock16(string cipherHex)
{
byte[] cipher = HexToBytes(cipherHex);
if (cipher.Length != 16) throw new InvalidOperationException("Cipher must be 16 bytes.");
return BytesToHex(Aes256EcbDecrypt(cipher));
}
private byte[] Aes256EcbEncrypt(byte[] data)
{
using (Aes aes = Aes.Create())
{
aes.Key = _aesKey32;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
using (ICryptoTransform encryptor = aes.CreateEncryptor())
{
return encryptor.TransformFinalBlock(data, 0, data.Length);
}
}
}
private byte[] Aes256EcbDecrypt(byte[] data)
{
using (Aes aes = Aes.Create())
{
aes.Key = _aesKey32;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
using (ICryptoTransform decryptor = aes.CreateDecryptor())
{
return decryptor.TransformFinalBlock(data, 0, data.Length);
}
}
}
private static string GenerateRandomBlock16Hex()
{
byte[] buffer = new byte[16];
RandomNumberGenerator.Fill(buffer);
return BytesToHex(buffer);
}
private static byte[] HexToBytes(string hex)
{
if (hex == null) throw new ArgumentNullException(nameof(hex));
hex = hex.Trim();
if (hex.Length % 2 != 0)
{
throw new InvalidOperationException("Hex string length must be even.");
}
byte[] result = new byte[hex.Length / 2];
for (int i = 0; i < result.Length; i++)
{
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return result;
}
private static string BytesToHex(byte[] bytes)
{
StringBuilder builder = new StringBuilder(bytes.Length * 2);
for (int i = 0; i < bytes.Length; i++)
{
builder.AppendFormat("{0:X2}", bytes[i]);
}
return builder.ToString();
}
private static bool IsHexString(string value, int expectedLength)
{
if (string.IsNullOrWhiteSpace(value) || value.Length != expectedLength)
{
return false;
}
for (int i = 0; i < value.Length; i++)
{
if (!Uri.IsHexDigit(value[i]))
{
return false;
}
}
return true;
}
}
}