You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
85 lines
3.0 KiB
85 lines
3.0 KiB
package authcaptcha
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestChallengeIsPNGSubjectBoundAndOneTime(t *testing.T) {
|
|
now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC)
|
|
random := append(bytes.Repeat([]byte{0x23}, 18), bytes.Repeat([]byte{0x00}, answerLength)...)
|
|
random = append(random, bytes.Repeat([]byte{0x55}, 256)...)
|
|
manager := New(Options{Clock: func() time.Time { return now }, Random: bytes.NewReader(random)})
|
|
challenge, err := manager.Create("192.0.2.10")
|
|
if err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
if !strings.HasPrefix(challenge.ImageDataURL, "data:image/png;base64,") {
|
|
t.Fatalf("image data URL = %q", challenge.ImageDataURL[:minInt(len(challenge.ImageDataURL), 40)])
|
|
}
|
|
payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(challenge.ImageDataURL, "data:image/png;base64,"))
|
|
if err != nil || len(payload) < 8 || string(payload[:8]) != "\x89PNG\r\n\x1a\n" {
|
|
t.Fatalf("captcha image is not a PNG: error=%v bytes=%x", err, payload[:minInt(len(payload), 8)])
|
|
}
|
|
if manager.Verify("198.51.100.5", challenge.ChallengeID, "2222") {
|
|
t.Fatal("challenge verified for a different subject")
|
|
}
|
|
if manager.Verify("192.0.2.10", challenge.ChallengeID, "2222") {
|
|
t.Fatal("challenge was reusable after a failed subject check")
|
|
}
|
|
}
|
|
|
|
func TestChallengeCorrectAnswerVerifiesOnceAndExpires(t *testing.T) {
|
|
now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC)
|
|
newRandom := func() *bytes.Reader {
|
|
payload := append(bytes.Repeat([]byte{0x31}, 18), bytes.Repeat([]byte{0x00}, answerLength)...)
|
|
payload = append(payload, bytes.Repeat([]byte{0x44}, 256)...)
|
|
return bytes.NewReader(payload)
|
|
}
|
|
manager := New(Options{Clock: func() time.Time { return now }, Random: newRandom(), TTL: time.Minute})
|
|
challenge, err := manager.Create("peer")
|
|
if err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
if !manager.Verify("peer", challenge.ChallengeID, "2222") {
|
|
t.Fatal("correct answer was rejected")
|
|
}
|
|
if manager.Verify("peer", challenge.ChallengeID, "2222") {
|
|
t.Fatal("challenge verified twice")
|
|
}
|
|
|
|
manager = New(Options{Clock: func() time.Time { return now }, Random: newRandom(), TTL: time.Minute})
|
|
challenge, err = manager.Create("peer")
|
|
if err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
now = now.Add(time.Minute)
|
|
if manager.Verify("peer", challenge.ChallengeID, "2222") {
|
|
t.Fatal("expired challenge verified")
|
|
}
|
|
}
|
|
|
|
func TestChallengesAreBoundedPerSubject(t *testing.T) {
|
|
now := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC)
|
|
randomPayload := make([]byte, 5*(18+answerLength+256))
|
|
for index := range randomPayload {
|
|
randomPayload[index] = byte(index)
|
|
}
|
|
manager := New(Options{
|
|
Clock: func() time.Time { return now },
|
|
Random: bytes.NewReader(randomPayload),
|
|
Capacity: 32,
|
|
})
|
|
for index := 0; index < 5; index++ {
|
|
now = now.Add(time.Second)
|
|
if _, err := manager.Create("peer"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if len(manager.entries) != maxPerSubject {
|
|
t.Fatalf("subject challenge count=%d, want %d", len(manager.entries), maxPerSubject)
|
|
}
|
|
}
|
|
|