Files
LehrerApp/LehrerApp.Sync/Crypto/SyncCrypto.cs
T
2026-06-19 00:42:00 +02:00

97 lines
4.4 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 + 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.
/// </summary>
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);
/// <summary>Leitet Schlüssel aus Einmal-Code ab. PBKDF2 erschwert Brute-Force.</summary>
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 ───────────────────────────────────────────────────
/// <summary>Format: [Nonce 12B][Ciphertext][Tag 16B]</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 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>(T obj, byte[] key) =>
Convert.ToBase64String(Encrypt(JsonSerializer.SerializeToUtf8Bytes(obj), key));
public static T? DecryptObject<T>(string b64, byte[] key) =>
JsonSerializer.Deserialize<T>(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;
}