package yubikey import ( "crypto/hmac" "crypto/sha1" "encoding/hex" "strings" "testing" "gitea.apointless.space/bsncubed/dial/internal/store" ) // fakeKey simulates a YubiKey slot programmed with a fixed secret by computing // HMAC-SHA1 over the challenge, mirroring ykman's challenge-response output. func fakeKey(t *testing.T, secret []byte) func(args ...string) ([]byte, error) { t.Helper() return func(args ...string) ([]byte, error) { // args: otp calculate if len(args) < 4 { t.Fatalf("unexpected ykman args: %v", args) } chal, err := hex.DecodeString(args[3]) if err != nil { t.Fatalf("bad challenge hex: %v", err) } m := hmac.New(sha1.New, secret) m.Write(chal) return []byte(hex.EncodeToString(m.Sum(nil)) + "\n"), nil } } func TestEnrollThenVerifySucceeds(t *testing.T) { orig := runYkman defer func() { runYkman = orig }() runYkman = fakeKey(t, []byte("secret-slot-2")) cfg, err := Enroll(2) if err != nil { t.Fatal(err) } if !cfg.IsEnrolled() { t.Fatal("config should be enrolled") } if err := Verify(cfg, 2); err != nil { t.Errorf("verify with same key should succeed: %v", err) } } func TestVerifyWrongKeyFails(t *testing.T) { orig := runYkman defer func() { runYkman = orig }() runYkman = fakeKey(t, []byte("correct-secret")) cfg, err := Enroll(2) if err != nil { t.Fatal(err) } // Now a different physical key (different secret) is present. runYkman = fakeKey(t, []byte("attacker-secret")) if err := Verify(cfg, 2); err != ErrKeyMismatch { t.Errorf("expected ErrKeyMismatch, got %v", err) } } func TestInvalidSlot(t *testing.T) { if _, err := Enroll(3); err == nil { t.Error("slot 3 should be rejected") } } func TestRecoveryCodeRoundTrip(t *testing.T) { code, h, err := GenerateRecoveryCode() if err != nil { t.Fatal(err) } // Format: 6 groups of 5 chars separated by dashes. parts := strings.Split(code, "-") if len(parts) != recoveryGroups { t.Fatalf("expected %d groups, got %d (%q)", recoveryGroups, len(parts), code) } for _, p := range parts { if len(p) != recoveryGroupLen { t.Errorf("group %q wrong length", p) } } if !VerifyRecoveryCode(code, h) { t.Error("exact code should verify") } // With separators stripped / lowercased it should still verify. if !VerifyRecoveryCode(strings.ToLower(strings.ReplaceAll(code, "-", " ")), h) { t.Error("normalised code should verify") } if VerifyRecoveryCode("WRONG-WRONG-WRONG-WRONG-WRONG-WRONG", h) { t.Error("wrong code should not verify") } } func TestVerifyRecoveryUnsetHash(t *testing.T) { if VerifyRecoveryCode("anything", store.RecoveryHash{}) { t.Error("empty hash should never verify") } }