init 1.0.0
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user