Files
GeoSusGame/Assets/ClientSDK/Encryption.cs
2026-04-26 12:55:21 +02:00

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