216 lines
6.5 KiB
C#
216 lines
6.5 KiB
C#
using System;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
public enum ControllerAuthenticationState
|
|
{
|
|
NotStarted,
|
|
AwaitingControllerVersion,
|
|
ReadyToStartHandshake,
|
|
AwaitingChallenge,
|
|
AwaitingControllerProof,
|
|
AwaitingReady,
|
|
Ready,
|
|
Failed
|
|
}
|
|
|
|
public sealed class ControllerAuthenticator
|
|
{
|
|
private string _hostChallenge;
|
|
|
|
public ControllerAuthenticationState State { get; private set; }
|
|
|
|
public void BeginControllerReset()
|
|
{
|
|
_hostChallenge = null;
|
|
State = ControllerAuthenticationState.AwaitingControllerVersion;
|
|
}
|
|
|
|
public bool AcceptControllerVersion(string line)
|
|
{
|
|
if (State != ControllerAuthenticationState.AwaitingControllerVersion
|
|
|| !BoardAuthentication.IsControllerVersionLine(line))
|
|
return false;
|
|
|
|
State = ControllerAuthenticationState.ReadyToStartHandshake;
|
|
return true;
|
|
}
|
|
|
|
public string StartHandshake()
|
|
{
|
|
if (State != ControllerAuthenticationState.ReadyToStartHandshake)
|
|
return null;
|
|
|
|
_hostChallenge = BoardAuthentication.CreateChallengeHex();
|
|
State = ControllerAuthenticationState.AwaitingChallenge;
|
|
return "07" + _hostChallenge;
|
|
}
|
|
|
|
public string Handle(string line)
|
|
{
|
|
if (State == ControllerAuthenticationState.AwaitingChallenge
|
|
&& BoardAuthentication.TryExtractHexPayload(line, "CHAL", out string controllerChallenge))
|
|
{
|
|
State = ControllerAuthenticationState.AwaitingControllerProof;
|
|
return "08" + BoardAuthentication.EncryptHexBlock(controllerChallenge);
|
|
}
|
|
|
|
if (State == ControllerAuthenticationState.AwaitingControllerProof
|
|
&& BoardAuthentication.TryExtractHexPayload(line, "RAES", out string encryptedResponse))
|
|
{
|
|
if (!BoardAuthentication.ValidateEncryptedResponse(_hostChallenge, encryptedResponse))
|
|
{
|
|
Fail();
|
|
return null;
|
|
}
|
|
|
|
State = ControllerAuthenticationState.AwaitingReady;
|
|
return null;
|
|
}
|
|
|
|
if (State == ControllerAuthenticationState.AwaitingReady
|
|
&& string.Equals(line, "READY!", StringComparison.Ordinal))
|
|
{
|
|
State = ControllerAuthenticationState.Ready;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_hostChallenge = null;
|
|
State = ControllerAuthenticationState.NotStarted;
|
|
}
|
|
|
|
public void Fail()
|
|
{
|
|
_hostChallenge = null;
|
|
State = ControllerAuthenticationState.Failed;
|
|
}
|
|
}
|
|
|
|
public static class BoardAuthentication
|
|
{
|
|
private const int _blockSizeBytes = 16;
|
|
private const int _hexBlockLength = _blockSizeBytes * 2;
|
|
private const byte _keyObfuscationMask = 0x37;
|
|
|
|
private static readonly byte[] _aesObfuscatedKey =
|
|
{
|
|
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
|
|
};
|
|
|
|
public static string CreateChallengeHex()
|
|
{
|
|
byte[] challenge = new byte[_blockSizeBytes];
|
|
using RandomNumberGenerator generator = RandomNumberGenerator.Create();
|
|
generator.GetBytes(challenge);
|
|
return ToHex(challenge);
|
|
}
|
|
|
|
public static bool IsControllerVersionLine(string line)
|
|
=> !string.IsNullOrEmpty(line)
|
|
&& line.IndexOf("ver_", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
|
|
public static string EncryptHexBlock(string plainHex)
|
|
{
|
|
byte[] plain = ParseHexBlock(plainHex);
|
|
|
|
using Aes aes = Aes.Create();
|
|
aes.KeySize = 256;
|
|
aes.BlockSize = 128;
|
|
aes.Mode = CipherMode.ECB;
|
|
aes.Padding = PaddingMode.None;
|
|
aes.Key = GetAesKey();
|
|
|
|
using ICryptoTransform encryptor = aes.CreateEncryptor();
|
|
byte[] encrypted = encryptor.TransformFinalBlock(plain, 0, plain.Length);
|
|
return ToHex(encrypted);
|
|
}
|
|
|
|
public static bool TryExtractHexPayload(string line, string prefix, out string payload)
|
|
{
|
|
payload = null;
|
|
if (string.IsNullOrEmpty(line)
|
|
|| string.IsNullOrEmpty(prefix)
|
|
|| line.Length != prefix.Length + _hexBlockLength
|
|
|| !line.StartsWith(prefix, StringComparison.Ordinal))
|
|
return false;
|
|
|
|
string candidate = line.Substring(prefix.Length);
|
|
for (int i = 0; i < candidate.Length; i++)
|
|
{
|
|
if (!IsHexCharacter(candidate[i]))
|
|
return false;
|
|
}
|
|
|
|
payload = candidate.ToUpperInvariant();
|
|
return true;
|
|
}
|
|
|
|
public static bool ValidateEncryptedResponse(string challengeHex, string responseHex)
|
|
{
|
|
if (!TryNormalizeHexBlock(responseHex, out string normalizedResponse))
|
|
return false;
|
|
|
|
byte[] expectedResponse = ParseHexBlock(EncryptHexBlock(challengeHex));
|
|
byte[] receivedResponse = ParseHexBlock(normalizedResponse);
|
|
return CryptographicOperations.FixedTimeEquals(expectedResponse, receivedResponse);
|
|
}
|
|
|
|
private static byte[] ParseHexBlock(string value)
|
|
{
|
|
if (!TryNormalizeHexBlock(value, out string normalized))
|
|
throw new ArgumentException($"Ожидался HEX-блок длиной {_hexBlockLength} символа.", nameof(value));
|
|
|
|
byte[] bytes = new byte[_blockSizeBytes];
|
|
for (int i = 0; i < bytes.Length; i++)
|
|
bytes[i] = Convert.ToByte(normalized.Substring(i * 2, 2), 16);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
private static bool TryNormalizeHexBlock(string value, out string normalized)
|
|
{
|
|
normalized = null;
|
|
if (string.IsNullOrEmpty(value) || value.Length != _hexBlockLength)
|
|
return false;
|
|
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
if (!IsHexCharacter(value[i]))
|
|
return false;
|
|
}
|
|
|
|
normalized = value.ToUpperInvariant();
|
|
return true;
|
|
}
|
|
|
|
private static bool IsHexCharacter(char value)
|
|
=> value >= '0' && value <= '9'
|
|
|| value >= 'A' && value <= 'F'
|
|
|| value >= 'a' && value <= 'f';
|
|
|
|
private static byte[] GetAesKey()
|
|
{
|
|
byte[] key = new byte[_aesObfuscatedKey.Length];
|
|
for (int i = 0; i < key.Length; i++)
|
|
key[i] = (byte)(_aesObfuscatedKey[i] ^ _keyObfuscationMask);
|
|
|
|
return key;
|
|
}
|
|
|
|
private static string ToHex(byte[] bytes)
|
|
{
|
|
StringBuilder builder = new(bytes.Length * 2);
|
|
for (int i = 0; i < bytes.Length; i++)
|
|
builder.Append(bytes[i].ToString("X2"));
|
|
|
|
return builder.ToString();
|
|
}
|
|
}
|