using System.Security.Cryptography; using LehrerApp.Sync.Crypto; using Xunit; namespace LehrerApp.Sync.Tests; public sealed class SyncCryptoTests { [Fact] public void GenerateKey_LiefertZweiUnterschiedlicheSchluessel() { var a = SyncCrypto.GenerateKey(); var b = SyncCrypto.GenerateKey(); Assert.Equal(32, a.Length); Assert.NotEqual(a, b); } [Fact] public void SaveKeyUndLoadKey_Roundtrip() { var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-synccrypto-tests-{Guid.NewGuid():N}.key"); try { var key = SyncCrypto.GenerateKey(); SyncCrypto.SaveKey(key, path); Assert.Equal(key, SyncCrypto.LoadKey(path)); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void LoadKey_DateiFehlt_LiefertNull() => Assert.Null(SyncCrypto.LoadKey(Path.Combine(Path.GetTempPath(), $"nie-existiert-{Guid.NewGuid():N}.key"))); [Fact] public void EncryptUndDecrypt_Roundtrip() { var key = SyncCrypto.GenerateKey(); var plaintext = "Ein geheimer Sitzplan-Sync-Test"u8.ToArray(); var encrypted = SyncCrypto.Encrypt(plaintext, key); var decrypted = SyncCrypto.Decrypt(encrypted, key); Assert.Equal(plaintext, decrypted); } [Fact] public void Decrypt_FalscherSchluessel_WirftCryptographicException() { var encrypted = SyncCrypto.Encrypt("geheim"u8.ToArray(), SyncCrypto.GenerateKey()); Assert.ThrowsAny(() => SyncCrypto.Decrypt(encrypted, SyncCrypto.GenerateKey())); } [Fact] public void EncryptKeyWithCodeUndDecryptKeyWithCode_Roundtrip() { var syncKey = SyncCrypto.GenerateKey(); var wrapped = SyncCrypto.EncryptKeyWithCode(syncKey, "TIGER-42-BLAU"); Assert.Equal(syncKey, SyncCrypto.DecryptKeyWithCode(wrapped, "TIGER-42-BLAU")); } [Fact] public void DecryptKeyWithCode_FalscherCode_WirftCryptographicException() { var syncKey = SyncCrypto.GenerateKey(); var wrapped = SyncCrypto.EncryptKeyWithCode(syncKey, "TIGER-42-BLAU"); Assert.ThrowsAny(() => SyncCrypto.DecryptKeyWithCode(wrapped, "FALSCH-99-CODE")); } // ── Selbstverwalteter Wiederherstellungscode (10.3.2) ──────────────────────── [Fact] public void GenerateRecoveryCode_LiefertSechsGruppenZuJeVierZeichen() { var code = SyncCrypto.GenerateRecoveryCode(); var groups = code.Split('-'); Assert.Equal(6, groups.Length); Assert.All(groups, g => Assert.Equal(4, g.Length)); // RFC 4648 Base32-Alphabet, keine Verwechslungsgefahr mit 0/1/8/9 o.ä. Assert.All(code.Where(c => c != '-'), c => Assert.Contains(c, "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")); } [Fact] public void GenerateRecoveryCode_LiefertJedesMalEinenAnderenCode() { var a = SyncCrypto.GenerateRecoveryCode(); var b = SyncCrypto.GenerateRecoveryCode(); Assert.NotEqual(a, b); } [Fact] public void EncryptKeyWithRecoveryCodeUndDecryptKeyWithRecoveryCode_Roundtrip() { var syncKey = SyncCrypto.GenerateKey(); var code = SyncCrypto.GenerateRecoveryCode(); var wrapped = SyncCrypto.EncryptKeyWithRecoveryCode(syncKey, code); Assert.Equal(syncKey, SyncCrypto.DecryptKeyWithRecoveryCode(wrapped, code)); } /// Der Code wird von Hand abgetippt - Groß-/Kleinschreibung und ob die Trennstriche mitgetippt /// wurden, dürfen die Wiederherstellung nicht scheitern lassen. [Fact] public void DecryptKeyWithRecoveryCode_ToleriertKleinschreibungUndFehlendeTrennstriche() { var syncKey = SyncCrypto.GenerateKey(); var code = SyncCrypto.GenerateRecoveryCode(); var wrapped = SyncCrypto.EncryptKeyWithRecoveryCode(syncKey, code); var retyped = code.Replace("-", "").ToLowerInvariant(); Assert.Equal(syncKey, SyncCrypto.DecryptKeyWithRecoveryCode(wrapped, retyped)); } [Fact] public void DecryptKeyWithRecoveryCode_FalscherCode_WirftCryptographicException() { var syncKey = SyncCrypto.GenerateKey(); var wrapped = SyncCrypto.EncryptKeyWithRecoveryCode(syncKey, SyncCrypto.GenerateRecoveryCode()); Assert.ThrowsAny( () => SyncCrypto.DecryptKeyWithRecoveryCode(wrapped, SyncCrypto.GenerateRecoveryCode())); } /// Pairing-Code und Wiederherstellungscode müssen unabhängige Ableitungen sein (eigenes Salt) - /// sonst könnte ein kurzlebiger Pairing-Code auch den langlebigen Wiederherstellungsschutz /// aushebeln. [Fact] public void DeriveKeyFromCodeUndDeriveRecoveryKeyFromCode_LiefernBeiGleichemCodeUnterschiedlicheSchluessel() { const string code = "GEMEINSAMER-CODE-123"; Assert.NotEqual(SyncCrypto.DeriveKeyFromCode(code), SyncCrypto.DeriveRecoveryKeyFromCode(code)); } }