using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace LehrerApp.Sync.Crypto; /// /// AES-256-GCM Verschlüsselung für Sync-Payloads. /// Der Schlüssel verlässt nie den Client – der Server sieht nur Ciphertext. /// public class SyncCrypto { private const int KeySize = 32; // 256 bit private const int NonceSize = 12; // 96 bit – GCM Standard private const int TagSize = 16; // 128 bit Auth-Tag // PBKDF2-Parameter für Schlüsselableitung aus Einmal-Code private const int Pbkdf2Iterations = 100_000; private const int Pbkdf2KeySize = 32; // 256 bit // ── Schlüsselverwaltung ─────────────────────────────────────────────────── public static byte[] GenerateKey() { var key = new byte[KeySize]; RandomNumberGenerator.Fill(key); return key; } public static string KeyToBase64(byte[] key) => Convert.ToBase64String(key); public static byte[] KeyFromBase64(string base64) => Convert.FromBase64String(base64); // ── Schlüssel aus Einmal-Code ableiten (PBKDF2) ─────────────────────────── /// /// Leitet einen symmetrischen Schlüssel aus dem Einmal-Code ab. /// PBKDF2 macht Brute-Force bei kurzem Code teuer. /// /// Salt ist fest definiert (kein Geheimnis, nur Domänentrennung). /// Der Code selbst ist das Geheimnis – 24h TTL schützt vor Angriffsfenstern. /// public static byte[] DeriveKeyFromCode(string code) { // Normalisieren: Groß, Leerzeichen entfernen var normalized = code.Trim().ToUpperInvariant(); var codeBytes = Encoding.UTF8.GetBytes(normalized); // Salt: App-spezifisch, öffentlich bekannt – verhindert // Rainbow-Tables gegen andere Anwendungen var salt = Encoding.UTF8.GetBytes("LehrerApp-PairingCode-v1"); return Rfc2898DeriveBytes.Pbkdf2( codeBytes, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256, Pbkdf2KeySize); } /// /// Verschlüsselt den Sync-Schlüssel mit dem Code-Key. /// Ergebnis wird als Base64 auf dem Server hinterlegt. /// public static string EncryptKeyWithCode(byte[] syncKey, string code) { var codeKey = DeriveKeyFromCode(code); var encrypted = Encrypt(syncKey, codeKey); return Convert.ToBase64String(encrypted); } /// /// Entschlüsselt den Sync-Schlüssel mit dem Code. /// Wirft CryptographicException wenn der Code falsch ist. /// public static byte[] DecryptKeyWithCode(string encryptedKeyBase64, string code) { var codeKey = DeriveKeyFromCode(code); var encrypted = Convert.FromBase64String(encryptedKeyBase64); return Decrypt(encrypted, codeKey); } // ── Ver-/Entschlüsselung ────────────────────────────────────────────────── /// /// Verschlüsselt mit AES-256-GCM. /// Format: [Nonce 12 Bytes][Ciphertext][Auth-Tag 16 Bytes] /// public static byte[] Encrypt(byte[] plaintext, byte[] key) { var nonce = new byte[NonceSize]; RandomNumberGenerator.Fill(nonce); var ciphertext = new byte[plaintext.Length]; var tag = new byte[TagSize]; using var aes = new AesGcm(key, TagSize); aes.Encrypt(nonce, plaintext, ciphertext, tag); var result = new byte[NonceSize + ciphertext.Length + TagSize]; nonce.CopyTo(result, 0); ciphertext.CopyTo(result, NonceSize); tag.CopyTo(result, NonceSize + ciphertext.Length); return result; } public static byte[] Decrypt(byte[] encrypted, byte[] key) { if (encrypted.Length < NonceSize + TagSize) throw new CryptographicException("Ungültiges verschlüsseltes Format."); var nonce = encrypted[..NonceSize]; var tag = encrypted[^TagSize..]; var ciphertext = encrypted[NonceSize..^TagSize]; var plaintext = new byte[ciphertext.Length]; using var aes = new AesGcm(key, TagSize); aes.Decrypt(nonce, ciphertext, tag, plaintext); return plaintext; } // ── Komfort-Methoden für JSON-Payloads ──────────────────────────────────── public static string EncryptObject(T obj, byte[] key) { var json = JsonSerializer.SerializeToUtf8Bytes(obj); var encrypted = Encrypt(json, key); return Convert.ToBase64String(encrypted); } public static T? DecryptObject(string base64, byte[] key) { var encrypted = Convert.FromBase64String(base64); var json = Decrypt(encrypted, key); return JsonSerializer.Deserialize(json); } // ── Schlüsselspeicherung ────────────────────────────────────────────────── public static void SaveKey(byte[] key, string keyFilePath) { var dir = Path.GetDirectoryName(keyFilePath); if (dir != null) Directory.CreateDirectory(dir); File.WriteAllText(keyFilePath, KeyToBase64(key)); if (!OperatingSystem.IsWindows()) { File.SetUnixFileMode(keyFilePath, UnixFileMode.UserRead | UnixFileMode.UserWrite); } } public static byte[]? LoadKey(string keyFilePath) { if (!File.Exists(keyFilePath)) return null; return KeyFromBase64(File.ReadAllText(keyFilePath).Trim()); } }