286 lines
11 KiB
C#
286 lines
11 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace GeoSus.Client
|
|
{
|
|
// Klientská strana šifrování - generuje session key, šifruje RSA, AES-CBC session
|
|
// Používá AES-CBC místo AES-GCM pro kompatibilitu s Unity
|
|
public class ClientEncryption : IDisposable
|
|
{
|
|
private byte[] _sessionKey;
|
|
private byte[] _sessionIv;
|
|
private long _nonceCounter;
|
|
private readonly object _lock = new object();
|
|
|
|
// Kontrola, zda je session key nastaven
|
|
public bool HasSessionKey => _sessionKey != null && _sessionIv != null;
|
|
|
|
// Generuje nový session key a IV
|
|
public void GenerateSessionKey()
|
|
{
|
|
_sessionKey = new byte[32]; // AES-256
|
|
_sessionIv = new byte[16]; // CBC IV (16 bytes)
|
|
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
rng.GetBytes(_sessionKey);
|
|
rng.GetBytes(_sessionIv);
|
|
}
|
|
}
|
|
|
|
public byte[] SessionKey => _sessionKey ?? throw new InvalidOperationException("Session key not generated");
|
|
public byte[] SessionIV => _sessionIv ?? throw new InvalidOperationException("Session IV not generated");
|
|
|
|
// Zašifruje session key pomocí RSA public key serveru
|
|
public (string EncryptedKey, string EncryptedIV) EncryptSessionKeyForServer(string rsaPublicKeyPem)
|
|
{
|
|
if (_sessionKey == null || _sessionIv == null)
|
|
throw new InvalidOperationException("Session key not generated");
|
|
|
|
using (var rsa = RSA.Create())
|
|
{
|
|
// Parse PEM - extrahuj Base64 obsah
|
|
var pemLines = rsaPublicKeyPem.Split('\n');
|
|
var base64 = new StringBuilder();
|
|
foreach (var line in pemLines)
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (!trimmed.StartsWith("-----") && !string.IsNullOrEmpty(trimmed))
|
|
{
|
|
base64.Append(trimmed);
|
|
}
|
|
}
|
|
|
|
var keyBytes = Convert.FromBase64String(base64.ToString());
|
|
|
|
// Unity kompatibilní import - parsujeme SubjectPublicKeyInfo ručně
|
|
ImportSubjectPublicKeyInfoManual(rsa, keyBytes);
|
|
|
|
// Používáme OaepSHA1 pro Unity kompatibilitu (OaepSHA256 není podporován)
|
|
var encryptedKey = rsa.Encrypt(_sessionKey, RSAEncryptionPadding.OaepSHA1);
|
|
var encryptedIv = rsa.Encrypt(_sessionIv, RSAEncryptionPadding.OaepSHA1);
|
|
|
|
return (Convert.ToBase64String(encryptedKey), Convert.ToBase64String(encryptedIv));
|
|
}
|
|
}
|
|
|
|
// Ručně parsuje SubjectPublicKeyInfo (DER) a importuje RSA klíč - Unity kompatibilní
|
|
private static void ImportSubjectPublicKeyInfoManual(RSA rsa, byte[] subjectPublicKeyInfo)
|
|
{
|
|
// SubjectPublicKeyInfo ::= SEQUENCE {
|
|
// algorithm AlgorithmIdentifier,
|
|
// subjectPublicKey BIT STRING }
|
|
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }
|
|
|
|
int index = 0;
|
|
|
|
// Outer SEQUENCE
|
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
|
throw new InvalidOperationException("Invalid SubjectPublicKeyInfo");
|
|
ReadLength(subjectPublicKeyInfo, ref index);
|
|
|
|
// AlgorithmIdentifier SEQUENCE - skip it
|
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
|
throw new InvalidOperationException("Invalid AlgorithmIdentifier");
|
|
int algLen = ReadLength(subjectPublicKeyInfo, ref index);
|
|
index += algLen;
|
|
|
|
// BIT STRING containing RSAPublicKey
|
|
if (subjectPublicKeyInfo[index++] != 0x03)
|
|
throw new InvalidOperationException("Invalid BIT STRING");
|
|
ReadLength(subjectPublicKeyInfo, ref index);
|
|
index++; // Skip unused bits byte (should be 0)
|
|
|
|
// RSAPublicKey SEQUENCE
|
|
if (subjectPublicKeyInfo[index++] != 0x30)
|
|
throw new InvalidOperationException("Invalid RSAPublicKey");
|
|
ReadLength(subjectPublicKeyInfo, ref index);
|
|
|
|
// Modulus INTEGER
|
|
byte[] modulus = ReadInteger(subjectPublicKeyInfo, ref index);
|
|
|
|
// Exponent INTEGER
|
|
byte[] exponent = ReadInteger(subjectPublicKeyInfo, ref index);
|
|
|
|
var parameters = new RSAParameters
|
|
{
|
|
Modulus = modulus,
|
|
Exponent = exponent
|
|
};
|
|
rsa.ImportParameters(parameters);
|
|
}
|
|
|
|
private static int ReadLength(byte[] data, ref int index)
|
|
{
|
|
int length = data[index++];
|
|
if ((length & 0x80) != 0)
|
|
{
|
|
int numBytes = length & 0x7F;
|
|
length = 0;
|
|
for (int i = 0; i < numBytes; i++)
|
|
{
|
|
length = (length << 8) | data[index++];
|
|
}
|
|
}
|
|
return length;
|
|
}
|
|
|
|
private static byte[] ReadInteger(byte[] data, ref int index)
|
|
{
|
|
if (data[index++] != 0x02)
|
|
throw new InvalidOperationException("Expected INTEGER");
|
|
int length = ReadLength(data, ref index);
|
|
|
|
// Skip leading zero if present (used for positive sign in DER)
|
|
int originalLength = length;
|
|
int start = index;
|
|
if (length > 1 && data[start] == 0x00)
|
|
{
|
|
start++;
|
|
length--;
|
|
}
|
|
|
|
byte[] result = new byte[length];
|
|
Buffer.BlockCopy(data, start, result, 0, length);
|
|
index += originalLength;
|
|
|
|
return result;
|
|
}
|
|
|
|
// Šifruje zprávu pomocí AES-256-CBC s HMAC
|
|
public byte[] Encrypt(byte[] plaintext)
|
|
{
|
|
if (_sessionKey == null || _sessionIv == null)
|
|
throw new InvalidOperationException("Session key not set");
|
|
|
|
lock (_lock)
|
|
{
|
|
// Generuj unikátní IV pro tuto zprávu
|
|
var iv = GetNextIV();
|
|
|
|
using (var aes = Aes.Create())
|
|
{
|
|
aes.Key = _sessionKey;
|
|
aes.IV = iv;
|
|
aes.Mode = CipherMode.CBC;
|
|
aes.Padding = PaddingMode.PKCS7;
|
|
|
|
byte[] ciphertext;
|
|
using (var encryptor = aes.CreateEncryptor())
|
|
using (var ms = new MemoryStream())
|
|
{
|
|
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
|
|
{
|
|
cs.Write(plaintext, 0, plaintext.Length);
|
|
}
|
|
ciphertext = ms.ToArray();
|
|
}
|
|
|
|
// Compute HMAC pro integritu
|
|
byte[] hmac;
|
|
using (var hmacSha = new HMACSHA256(_sessionKey))
|
|
{
|
|
var toSign = new byte[iv.Length + ciphertext.Length];
|
|
Buffer.BlockCopy(iv, 0, toSign, 0, iv.Length);
|
|
Buffer.BlockCopy(ciphertext, 0, toSign, iv.Length, ciphertext.Length);
|
|
hmac = hmacSha.ComputeHash(toSign);
|
|
}
|
|
|
|
// Výstup: [16 bytes IV][32 bytes HMAC][ciphertext]
|
|
var result = new byte[16 + 32 + ciphertext.Length];
|
|
Buffer.BlockCopy(iv, 0, result, 0, 16);
|
|
Buffer.BlockCopy(hmac, 0, result, 16, 32);
|
|
Buffer.BlockCopy(ciphertext, 0, result, 48, ciphertext.Length);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dešifruje zprávu pomocí AES-256-CBC s HMAC ověřením
|
|
public byte[] Decrypt(byte[] encrypted)
|
|
{
|
|
if (_sessionKey == null)
|
|
throw new InvalidOperationException("Session key not set");
|
|
|
|
if (encrypted.Length < 48) return null;
|
|
|
|
try
|
|
{
|
|
var iv = new byte[16];
|
|
var hmac = new byte[32];
|
|
var ciphertext = new byte[encrypted.Length - 48];
|
|
|
|
Buffer.BlockCopy(encrypted, 0, iv, 0, 16);
|
|
Buffer.BlockCopy(encrypted, 16, hmac, 0, 32);
|
|
Buffer.BlockCopy(encrypted, 48, ciphertext, 0, ciphertext.Length);
|
|
|
|
// Ověř HMAC
|
|
byte[] expectedHmac;
|
|
using (var hmacSha = new HMACSHA256(_sessionKey))
|
|
{
|
|
var toVerify = new byte[iv.Length + ciphertext.Length];
|
|
Buffer.BlockCopy(iv, 0, toVerify, 0, iv.Length);
|
|
Buffer.BlockCopy(ciphertext, 0, toVerify, iv.Length, ciphertext.Length);
|
|
expectedHmac = hmacSha.ComputeHash(toVerify);
|
|
}
|
|
|
|
// Constant-time compare
|
|
var diff = 0;
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
diff |= hmac[i] ^ expectedHmac[i];
|
|
}
|
|
if (diff != 0) return null; // HMAC mismatch
|
|
|
|
using (var aes = Aes.Create())
|
|
{
|
|
aes.Key = _sessionKey;
|
|
aes.IV = iv;
|
|
aes.Mode = CipherMode.CBC;
|
|
aes.Padding = PaddingMode.PKCS7;
|
|
|
|
using (var decryptor = aes.CreateDecryptor())
|
|
using (var ms = new MemoryStream(ciphertext))
|
|
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
|
|
using (var output = new MemoryStream())
|
|
{
|
|
cs.CopyTo(output);
|
|
return output.ToArray();
|
|
}
|
|
}
|
|
}
|
|
catch (CryptographicException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private byte[] GetNextIV()
|
|
{
|
|
if (_sessionIv == null)
|
|
throw new InvalidOperationException("Session IV not set");
|
|
|
|
var iv = new byte[16];
|
|
Buffer.BlockCopy(_sessionIv, 0, iv, 0, 8);
|
|
|
|
var counter = System.Threading.Interlocked.Increment(ref _nonceCounter);
|
|
var counterBytes = BitConverter.GetBytes(counter);
|
|
Buffer.BlockCopy(counterBytes, 0, iv, 8, 8);
|
|
|
|
return iv;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_sessionKey != null)
|
|
{
|
|
Array.Clear(_sessionKey, 0, _sessionKey.Length);
|
|
_sessionKey = null;
|
|
}
|
|
}
|
|
}
|
|
}
|