using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace LehrerApp.Sync.Crypto; /// /// AES-256-GCM Verschlüsselung + PBKDF2 Schlüsselableitung. /// /// Pairing-Flow: /// Sender: EncryptedSyncKey = AES(syncKey, PBKDF2(code)) /// Empfänger: syncKey = AES_Decrypt(EncryptedSyncKey, PBKDF2(code)) /// → Schlüssel verlässt nie den Server, nur der Code wird geteilt. /// public static 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 private const int Pbkdf2Iter = 100_000; // ── Schlüssel ────────────────────────────────────────────────────────────── public static byte[] GenerateKey() { var k = new byte[KeySize]; RandomNumberGenerator.Fill(k); return k; } public static string KeyToBase64(byte[] key) => Convert.ToBase64String(key); public static byte[] KeyFromBase64(string b64) => Convert.FromBase64String(b64); /// Leitet Schlüssel aus Einmal-Code ab. PBKDF2 erschwert Brute-Force. public static byte[] DeriveKeyFromCode(string code) { var bytes = Encoding.UTF8.GetBytes(code.Trim().ToUpperInvariant()); var salt = Encoding.UTF8.GetBytes("LehrerApp-PairingCode-v1"); return Rfc2898DeriveBytes.Pbkdf2(bytes, salt, Pbkdf2Iter, HashAlgorithmName.SHA256, 32); } public static string EncryptKeyWithCode(byte[] syncKey, string code) => Convert.ToBase64String(Encrypt(syncKey, DeriveKeyFromCode(code))); public static byte[] DecryptKeyWithCode(string encryptedB64, string code) => Decrypt(Convert.FromBase64String(encryptedB64), DeriveKeyFromCode(code)); // ── Ver-/Entschlüsselung ─────────────────────────────────────────────────── /// Format: [Nonce 12B][Ciphertext][Tag 16B] 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 Datenformat."); 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; } // ── JSON-Komfort ─────────────────────────────────────────────────────────── public static string EncryptObject(T obj, byte[] key) => Convert.ToBase64String(Encrypt(JsonSerializer.SerializeToUtf8Bytes(obj), key)); public static T? DecryptObject(string b64, byte[] key) => JsonSerializer.Deserialize(Decrypt(Convert.FromBase64String(b64), key)); // ── Schlüsselspeicherung ─────────────────────────────────────────────────── public static void SaveKey(byte[] key, string path) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, KeyToBase64(key)); if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite); } public static byte[]? LoadKey(string path) => File.Exists(path) ? KeyFromBase64(File.ReadAllText(path).Trim()) : null; }