package crypto import ( "bytes" "errors" "strings" "testing" ) // --------------------------------------------------------------------------- // Passwörter // --------------------------------------------------------------------------- func TestHashPasswordProducesVerifiableHash(testInstance *testing.T) { const plainPassword = "ein-hinreichend-langes-passwort" // secretscan:erlaubt: erfundener Testwert encodedHash, hashError := HashPassword(plainPassword) if hashError != nil { testInstance.Fatalf("HashPassword schlug fehl: %v", hashError) } if verifyError := VerifyPassword(plainPassword, encodedHash); verifyError != nil { testInstance.Fatalf("das eigene Passwort wurde nicht akzeptiert: %v", verifyError) } } func TestHashNeverContainsPlaintext(testInstance *testing.T) { const plainPassword = "streng-geheimes-passwort" // secretscan:erlaubt: erfundener Testwert encodedHash, hashError := HashPassword(plainPassword) if hashError != nil { testInstance.Fatalf("HashPassword schlug fehl: %v", hashError) } // Ein Hash, der das Passwort enthält, wäre kein Hash. if strings.Contains(encodedHash, plainPassword) { testInstance.Fatalf("der Hash enthält das Passwort im Klartext: %s", encodedHash) } } func TestSamePasswordProducesDifferentHashes(testInstance *testing.T) { const plainPassword = "identisches-passwort" // secretscan:erlaubt: erfundener Testwert firstHash, _ := HashPassword(plainPassword) secondHash, _ := HashPassword(plainPassword) // Ohne zufälliges Salz liesse sich aus gleichen Hashes ablesen, // welche Benutzer dasselbe Passwort verwenden. if firstHash == secondHash { testInstance.Fatal("zwei Hashes desselben Passworts sind identisch - das Salz wirkt nicht") } // Beide müssen dennoch prüfbar sein. if VerifyPassword(plainPassword, firstHash) != nil || VerifyPassword(plainPassword, secondHash) != nil { testInstance.Fatal("nicht beide Hashes lassen sich verifizieren") } } func TestVerifyPasswordRejectsWrongPassword(testInstance *testing.T) { encodedHash, _ := HashPassword("das-richtige-passwort") verifyError := VerifyPassword("das-falsche-passwort", encodedHash) if !errors.Is(verifyError, ErrPasswordMismatch) { testInstance.Fatalf("ein falsches Passwort muss ErrPasswordMismatch liefern, war: %v", verifyError) } } func TestVerifyPasswordRejectsMalformedHash(testInstance *testing.T) { malformedHashes := map[string]string{ "leer": "", "kein PHC-Format": "einfach-nur-text", "unbekanntes Verfahren": "$bcrypt$v=19$m=65536,t=3,p=4$c2FsdA$aGFzaA", "zu wenige Teile": "$argon2id$v=19$m=65536,t=3,p=4$c2FsdA", "defektes Salz": "$argon2id$v=19$m=65536,t=3,p=4$!!!nicht-base64!!!$aGFzaA", } for hashDescription, malformedHash := range malformedHashes { verifyError := VerifyPassword("beliebig", malformedHash) // Ein unlesbarer Hash darf niemals als Übereinstimmung durchgehen. if verifyError == nil { testInstance.Errorf("%s: ein ungültiger Hash wurde akzeptiert", hashDescription) } } } func TestHashPasswordRejectsEmptyPassword(testInstance *testing.T) { if _, hashError := HashPassword(""); hashError == nil { testInstance.Fatal("ein leeres Passwort darf nicht gehasht werden") } } func TestNeedsRehashDetectsWeakerParameters(testInstance *testing.T) { // Ein Hash mit deutlich schwächeren Parametern soll erneuert werden. const weakHash = "$argon2id$v=19$m=4096,t=1,p=1$c2FsdHNhbHRzYWx0c2E$aGFzaGhhc2hoYXNoaGFzaGhhc2hoYXNoaGFz" if !NeedsRehash(weakHash) { testInstance.Error("ein Hash mit schwächeren Parametern soll erneuert werden") } currentHash, _ := HashPassword("aktuelles-passwort") if NeedsRehash(currentHash) { testInstance.Error("ein Hash mit aktuellen Parametern soll nicht erneuert werden") } } // --------------------------------------------------------------------------- // Secrets // --------------------------------------------------------------------------- // newTestSecretStore baut einen Secret Store mit zwei Schlüsselversionen. func newTestSecretStore(testInstance *testing.T) *LocalSecretStore { testInstance.Helper() firstKey := bytes.Repeat([]byte{0x01}, masterKeyLength) secondKey := bytes.Repeat([]byte{0x02}, masterKeyLength) secretStore, storeError := NewLocalSecretStore(map[string][]byte{"v1": firstKey, "v2": secondKey}, "v2") if storeError != nil { testInstance.Fatalf("der Secret Store konnte nicht erstellt werden: %v", storeError) } return secretStore } func TestEncryptDecryptRoundTrip(testInstance *testing.T) { secretStore := newTestSecretStore(testInstance) originalSecret := []byte("JBSWY3DPEHPK3PXP") encryptedSecret, keyVersion, encryptError := secretStore.Encrypt(originalSecret) if encryptError != nil { testInstance.Fatalf("Encrypt schlug fehl: %v", encryptError) } // Neu verschlüsselt wird immer mit der aktuellen Version. if keyVersion != "v2" { testInstance.Errorf("es soll mit v2 verschlüsselt werden, war %q", keyVersion) } // Der Geheimtext darf den Klartext nicht enthalten. if bytes.Contains(encryptedSecret, originalSecret) { testInstance.Fatal("der Geheimtext enthält den Klartext") } decryptedSecret, decryptError := secretStore.Decrypt(encryptedSecret, keyVersion) if decryptError != nil { testInstance.Fatalf("Decrypt schlug fehl: %v", decryptError) } if !bytes.Equal(decryptedSecret, originalSecret) { testInstance.Fatalf("der entschlüsselte Wert weicht ab: %q", decryptedSecret) } } func TestEncryptProducesDifferentCiphertextEachTime(testInstance *testing.T) { secretStore := newTestSecretStore(testInstance) originalSecret := []byte("dasselbe-secret") firstCiphertext, _, _ := secretStore.Encrypt(originalSecret) secondCiphertext, _, _ := secretStore.Encrypt(originalSecret) // Gleiche Geheimtexte würden verraten, dass zwei Benutzer dasselbe Secret haben. if bytes.Equal(firstCiphertext, secondCiphertext) { testInstance.Fatal("zweimaliges Verschlüsseln liefert denselben Geheimtext - die Nonce wirkt nicht") } } func TestDecryptDetectsTamperedCiphertext(testInstance *testing.T) { secretStore := newTestSecretStore(testInstance) encryptedSecret, keyVersion, _ := secretStore.Encrypt([]byte("unveränderliches-secret")) // Ein einzelnes verändertes Bit muss auffallen. tamperedSecret := bytes.Clone(encryptedSecret) tamperedSecret[len(tamperedSecret)-1] ^= 0x01 _, decryptError := secretStore.Decrypt(tamperedSecret, keyVersion) if !errors.Is(decryptError, ErrSecretTampered) { testInstance.Fatalf("ein verändertes Bit muss erkannt werden, war: %v", decryptError) } } func TestDecryptWithOldKeyVersionStillWorks(testInstance *testing.T) { firstKey := bytes.Repeat([]byte{0x01}, masterKeyLength) // Zuerst wird mit v1 verschlüsselt. oldStore, _ := NewLocalSecretStore(map[string][]byte{"v1": firstKey}, "v1") encryptedSecret, _, _ := oldStore.Encrypt([]byte("altes-secret")) // Nach der Rotation ist v2 aktuell, v1 aber weiterhin bekannt. rotatedStore := newTestSecretStore(testInstance) decryptedSecret, decryptError := rotatedStore.Decrypt(encryptedSecret, "v1") if decryptError != nil { testInstance.Fatalf("nach einer Rotation muss ein alter Wert lesbar bleiben: %v", decryptError) } if string(decryptedSecret) != "altes-secret" { testInstance.Fatalf("der entschlüsselte Wert weicht ab: %q", decryptedSecret) } } func TestDecryptWithUnknownKeyVersionFails(testInstance *testing.T) { secretStore := newTestSecretStore(testInstance) encryptedSecret, _, _ := secretStore.Encrypt([]byte("secret")) _, decryptError := secretStore.Decrypt(encryptedSecret, "v99") if !errors.Is(decryptError, ErrUnknownKeyVersion) { testInstance.Fatalf("ein unbekannter Schlüssel muss deutlich gemeldet werden, war: %v", decryptError) } } func TestNewLocalSecretStoreRejectsShortKey(testInstance *testing.T) { shortKey := bytes.Repeat([]byte{0x01}, 16) // Ein zu kurzer Schlüssel darf nicht stillschweigend aufgefüllt werden. if _, storeError := NewLocalSecretStore(map[string][]byte{"v1": shortKey}, "v1"); storeError == nil { testInstance.Fatal("ein zu kurzer Schlüssel muss abgelehnt werden") } } func TestParseKeySetReadsSeveralVersions(testInstance *testing.T) { firstKey, _ := GenerateMasterKey() secondKey, _ := GenerateMasterKey() parsedKeys, parseError := ParseKeySet("v1:" + firstKey + ", v2:" + secondKey) if parseError != nil { testInstance.Fatalf("ParseKeySet schlug fehl: %v", parseError) } if len(parsedKeys) != 2 { testInstance.Fatalf("es sollen 2 Schlüssel gelesen werden, waren %d", len(parsedKeys)) } } func TestParseKeySetRejectsInvalidInput(testInstance *testing.T) { invalidKeySets := map[string]string{ "leer": "", "ohne Version": "nur-ein-wert", "kein base64": "v1:!!!kein-base64!!!", "zu kurzer Wert": "v1:c2hvcnQ=", } for setDescription, invalidKeySet := range invalidKeySets { if _, parseError := ParseKeySet(invalidKeySet); parseError == nil { testInstance.Errorf("%s: ungültige Schlüsselliste wurde akzeptiert", setDescription) } } } func TestParseKeySetErrorNeverContainsKeyMaterial(testInstance *testing.T) { const secretLookingKey = "ZGllc2VyLXdlcnQtaXN0LWdlaGVpbS1hYmVyLXp1LWt1cno=" _, parseError := ParseKeySet("v1:" + secretLookingKey) if parseError == nil { testInstance.Fatal("der zu kurze Schlüssel hätte abgelehnt werden müssen") } // Auch eine Fehlermeldung darf kein Schlüsselmaterial preisgeben. if strings.Contains(parseError.Error(), secretLookingKey) { testInstance.Fatalf("die Fehlermeldung enthält Schlüsselmaterial: %v", parseError) } } // --------------------------------------------------------------------------- // Tokens // --------------------------------------------------------------------------- func TestGenerateTokenIsUnique(testInstance *testing.T) { seenTokens := make(map[string]struct{}) // Eine Kollision wäre eine Sitzungsübernahme. for tokenIndex := 0; tokenIndex < 1000; tokenIndex++ { generatedToken, tokenError := GenerateToken() if tokenError != nil { testInstance.Fatalf("GenerateToken schlug fehl: %v", tokenError) } if _, isDuplicate := seenTokens[generatedToken]; isDuplicate { testInstance.Fatal("zwei erzeugte Tokens sind identisch") } seenTokens[generatedToken] = struct{}{} } } func TestHashTokenIsStableAndHidesToken(testInstance *testing.T) { plainToken, _ := GenerateToken() firstHash := HashToken(plainToken) secondHash := HashToken(plainToken) // Ohne Stabilität liesse sich eine Sitzung nicht wiederfinden. if firstHash != secondHash { testInstance.Fatal("derselbe Token liefert unterschiedliche Hashes") } // Der gespeicherte Wert darf den Token nicht enthalten. if strings.Contains(firstHash, plainToken) { testInstance.Fatal("der Hash enthält den Token im Klartext") } } func TestTokensEqualComparesCorrectly(testInstance *testing.T) { firstToken, _ := GenerateToken() secondToken, _ := GenerateToken() if !TokensEqual(HashToken(firstToken), HashToken(firstToken)) { testInstance.Error("gleiche Tokens sollen als gleich gelten") } if TokensEqual(HashToken(firstToken), HashToken(secondToken)) { testInstance.Error("verschiedene Tokens dürfen nicht als gleich gelten") } } func TestGenerateRecoveryCodeAvoidsAmbiguousCharacters(testInstance *testing.T) { // Diese Zeichen werden beim Abtippen regelmäßig verwechselt. const ambiguousCharacters = "01OI" for codeIndex := 0; codeIndex < 200; codeIndex++ { recoveryCode, codeError := GenerateRecoveryCode() if codeError != nil { testInstance.Fatalf("GenerateRecoveryCode schlug fehl: %v", codeError) } if strings.ContainsAny(recoveryCode, ambiguousCharacters) { testInstance.Fatalf("der Code enthält ein verwechselbares Zeichen: %s", recoveryCode) } // Format: 5 Zeichen, Bindestrich, 5 Zeichen. if len(recoveryCode) != 11 || recoveryCode[5] != '-' { testInstance.Fatalf("unerwartetes Codeformat: %s", recoveryCode) } } }