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