package totp import ( "encoding/base32" "errors" "strings" "testing" "time" ) // RFC 6238 Anhang B nennt Testvektoren für das Secret "12345678901234567890" // (bzw. dessen Wiederholung für SHA256/SHA512). Diese Vektoren sind der // maßgebliche Nachweis, dass die Umsetzung korrekt ist. func encodeTestSecret(asciiSecret string) string { return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(asciiSecret)) } func TestGenerateCodeMatchesRFC6238TestVectors(testInstance *testing.T) { // Die Testvektoren verwenden acht Ziffern. const sha1Secret = "12345678901234567890" // secretscan:erlaubt: erfundener Testwert const sha256Secret = "12345678901234567890123456789012" // secretscan:erlaubt: erfundener Testwert const sha512Secret = "1234567890123456789012345678901234567890123456789012345678901234" // secretscan:erlaubt: erfundener Testwert testVectors := []struct { unixTime int64 algorithm Algorithm asciiSecret string expectedCode string }{ {59, AlgorithmSHA1, sha1Secret, "94287082"}, {59, AlgorithmSHA256, sha256Secret, "46119246"}, {59, AlgorithmSHA512, sha512Secret, "90693936"}, {1111111109, AlgorithmSHA1, sha1Secret, "07081804"}, {1111111111, AlgorithmSHA1, sha1Secret, "14050471"}, {1234567890, AlgorithmSHA1, sha1Secret, "89005924"}, {2000000000, AlgorithmSHA1, sha1Secret, "69279037"}, {20000000000, AlgorithmSHA1, sha1Secret, "65353130"}, {1111111109, AlgorithmSHA256, sha256Secret, "68084774"}, {1234567890, AlgorithmSHA512, sha512Secret, "93441116"}, } for _, testVector := range testVectors { configuration := Configuration{ Algorithm: testVector.algorithm, Digits: 8, Period: 30 * time.Second, Skew: 0, } generatedCode, generateError := GenerateCode( encodeTestSecret(testVector.asciiSecret), time.Unix(testVector.unixTime, 0).UTC(), configuration, ) if generateError != nil { testInstance.Fatalf("GenerateCode schlug fehl: %v", generateError) } if generatedCode != testVector.expectedCode { testInstance.Errorf("%s bei t=%d: erwartet %s, war %s", testVector.algorithm, testVector.unixTime, testVector.expectedCode, generatedCode) } } } func TestValidateAcceptsCurrentCode(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() currentCode, _ := GenerateCode(testSecret, validationTime, configuration) validationResult, validateError := Validate(testSecret, currentCode, validationTime, 0, configuration) if validateError != nil { testInstance.Fatalf("der aktuelle Code wurde abgelehnt: %v", validateError) } expectedTimeStep := validationTime.UTC().Unix() / 30 if validationResult.TimeStep != expectedTimeStep { testInstance.Errorf("Zeitschritt soll %d sein, war %d", expectedTimeStep, validationResult.TimeStep) } } func TestValidateRejectsCodeReuse(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() currentCode, _ := GenerateCode(testSecret, validationTime, configuration) firstResult, firstError := Validate(testSecret, currentCode, validationTime, 0, configuration) if firstError != nil { testInstance.Fatalf("die erste Prüfung schlug fehl: %v", firstError) } // Derselbe Code darf innerhalb seines Zeitfensters kein zweites Mal gelten: // sonst könnte ein abgefangener Code erneut verwendet werden. _, secondError := Validate(testSecret, currentCode, validationTime, firstResult.TimeStep, configuration) if !errors.Is(secondError, ErrCodeAlreadyUsed) { testInstance.Fatalf("die Wiederverwendung muss erkannt werden, war: %v", secondError) } } func TestValidateToleratesClockSkew(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() // Eine um einen Schritt nachgehende Uhr soll noch akzeptiert werden. previousCode, _ := GenerateCode(testSecret, validationTime.Add(-30*time.Second), configuration) if _, validateError := Validate(testSecret, previousCode, validationTime, 0, configuration); validateError != nil { testInstance.Errorf("ein Code des vorherigen Zeitschritts soll gelten: %v", validateError) } // Ebenso eine leicht vorgehende Uhr. nextCode, _ := GenerateCode(testSecret, validationTime.Add(30*time.Second), configuration) if _, validateError := Validate(testSecret, nextCode, validationTime, 0, configuration); validateError != nil { testInstance.Errorf("ein Code des nächsten Zeitschritts soll gelten: %v", validateError) } } func TestValidateRejectsCodeOutsideSkewWindow(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() // Zwei Schritte Abweichung liegen ausserhalb der Toleranz. staleCode, _ := GenerateCode(testSecret, validationTime.Add(-90*time.Second), configuration) if _, validateError := Validate(testSecret, staleCode, validationTime, 0, configuration); !errors.Is(validateError, ErrInvalidCode) { testInstance.Fatalf("ein zu alter Code muss abgelehnt werden, war: %v", validateError) } } func TestValidateRejectsWrongCode(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() if _, validateError := Validate(testSecret, "000000", time.Now(), 0, configuration); validateError == nil { testInstance.Fatal("ein falscher Code wurde akzeptiert") } } func TestValidateRejectsCodeOfWrongLength(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() for _, malformedCode := range []string{"", "1234", "1234567", "abcdef"} { if _, validateError := Validate(testSecret, malformedCode, time.Now(), 0, configuration); validateError == nil { testInstance.Errorf("der Code %q wurde akzeptiert", malformedCode) } } } func TestValidateAcceptsCodeWithSpaces(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() currentCode, _ := GenerateCode(testSecret, validationTime, configuration) // Authenticator-Apps zeigen Codes gruppiert an ("123 456"); ein Benutzer, // der das mit abtippt, soll deshalb nicht scheitern. spacedCode := currentCode[:3] + " " + currentCode[3:] if _, validateError := Validate(testSecret, spacedCode, validationTime, 0, configuration); validateError != nil { testInstance.Fatalf("ein Code mit Leerzeichen soll gelten: %v", validateError) } } func TestValidateRejectsInvalidSecret(testInstance *testing.T) { configuration := DefaultConfiguration() if _, validateError := Validate("!!!kein-base32!!!", "123456", time.Now(), 0, configuration); validateError == nil { testInstance.Fatal("ein ungültiges Secret wurde akzeptiert") } } func TestSecretErrorNeverLeaksSecret(testInstance *testing.T) { const secretLookingValue = "GEHEIMESSECRETABER!!!UNGUELTIG" _, validateError := Validate(secretLookingValue, "123456", time.Now(), 0, DefaultConfiguration()) if validateError == nil { testInstance.Fatal("das ungültige Secret hätte abgelehnt werden müssen") } // Auch eine Fehlermeldung darf kein Secret preisgeben. if strings.Contains(validateError.Error(), secretLookingValue) { testInstance.Fatalf("die Fehlermeldung enthält das Secret: %v", validateError) } } func TestGenerateSecretIsUnique(testInstance *testing.T) { seenSecrets := make(map[string]struct{}) for secretIndex := 0; secretIndex < 500; secretIndex++ { generatedSecret, secretError := GenerateSecret() if secretError != nil { testInstance.Fatalf("GenerateSecret schlug fehl: %v", secretError) } if _, isDuplicate := seenSecrets[generatedSecret]; isDuplicate { testInstance.Fatal("zwei erzeugte Secrets sind identisch") } seenSecrets[generatedSecret] = struct{}{} } } func TestGeneratedSecretWorksEndToEnd(testInstance *testing.T) { generatedSecret, _ := GenerateSecret() configuration := DefaultConfiguration() validationTime := time.Now() generatedCode, generateError := GenerateCode(generatedSecret, validationTime, configuration) if generateError != nil { testInstance.Fatalf("GenerateCode schlug fehl: %v", generateError) } if _, validateError := Validate(generatedSecret, generatedCode, validationTime, 0, configuration); validateError != nil { testInstance.Fatalf("ein selbst erzeugter Code wurde abgelehnt: %v", validateError) } } func TestProvisioningURIContainsExpectedParameters(testInstance *testing.T) { const testSecret = "JBSWY3DPEHPK3PXP" // secretscan:erlaubt: erfundener Testwert provisioningURI := ProvisioningURI(testSecret, "admin@example.local", "Syncova", DefaultConfiguration()) for _, expectedFragment := range []string{ "otpauth://totp/", "secret=" + testSecret, "issuer=Syncova", "algorithm=SHA1", "digits=6", "period=30", } { if !strings.Contains(provisioningURI, expectedFragment) { testInstance.Errorf("die URI enthält %q nicht: %s", expectedFragment, provisioningURI) } } } func TestCodePreservesLeadingZeros(testInstance *testing.T) { testSecret, _ := GenerateSecret() configuration := DefaultConfiguration() // Über viele Zeitpunkte hinweg muss jeder Code exakt sechs Zeichen haben; // eine abgeschnittene führende Null wäre ein Anmeldefehler. for stepIndex := int64(0); stepIndex < 2000; stepIndex++ { generatedCode, _ := GenerateCode(testSecret, time.Unix(stepIndex*30, 0), configuration) if len(generatedCode) != DefaultDigits { testInstance.Fatalf("der Code %q hat nicht %d Ziffern", generatedCode, DefaultDigits) } } }