Files
LehrerApp/LehrerApp.Sync/Crypto/SyncCrypto.cs
2026-03-29 23:47:31 +02:00

164 lines
5.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace LehrerApp.Sync.Crypto;
/// <summary>
/// AES-256-GCM Verschlüsselung für Sync-Payloads.
/// Der Schlüssel verlässt nie den Client der Server sieht nur Ciphertext.
/// </summary>
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) ───────────────────────────
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Verschlüsselt den Sync-Schlüssel mit dem Code-Key.
/// Ergebnis wird als Base64 auf dem Server hinterlegt.
/// </summary>
public static string EncryptKeyWithCode(byte[] syncKey, string code)
{
var codeKey = DeriveKeyFromCode(code);
var encrypted = Encrypt(syncKey, codeKey);
return Convert.ToBase64String(encrypted);
}
/// <summary>
/// Entschlüsselt den Sync-Schlüssel mit dem Code.
/// Wirft CryptographicException wenn der Code falsch ist.
/// </summary>
public static byte[] DecryptKeyWithCode(string encryptedKeyBase64, string code)
{
var codeKey = DeriveKeyFromCode(code);
var encrypted = Convert.FromBase64String(encryptedKeyBase64);
return Decrypt(encrypted, codeKey);
}
// ── Ver-/Entschlüsselung ──────────────────────────────────────────────────
/// <summary>
/// Verschlüsselt mit AES-256-GCM.
/// Format: [Nonce 12 Bytes][Ciphertext][Auth-Tag 16 Bytes]
/// </summary>
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>(T obj, byte[] key)
{
var json = JsonSerializer.SerializeToUtf8Bytes(obj);
var encrypted = Encrypt(json, key);
return Convert.ToBase64String(encrypted);
}
public static T? DecryptObject<T>(string base64, byte[] key)
{
var encrypted = Convert.FromBase64String(base64);
var json = Decrypt(encrypted, key);
return JsonSerializer.Deserialize<T>(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());
}
}