96 lines
3.7 KiB
Go
96 lines
3.7 KiB
Go
package onboarding
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/json"
|
||
|
|
"image/png"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Generate the real PNG and JSON record, then replace only the answer digest in
|
||
|
|
// disposable test Redis. This makes verification deterministic without solving
|
||
|
|
// a live CAPTCHA or adding an HTTP bypass.
|
||
|
|
func (f *loginFixture) knownGeneratedImageCaptcha() string {
|
||
|
|
f.t.Helper()
|
||
|
|
w, challenge := f.request("POST", "/api/v1/web/login/captcha", struct{}{}, f.cookie, f.csrf, "")
|
||
|
|
if w.Code != 200 || challenge["mode"] != "image" || challenge["captcha_app_id"] != nil {
|
||
|
|
f.t.Fatal("local image challenge unavailable")
|
||
|
|
}
|
||
|
|
encoded, ok := challenge["image"].(string)
|
||
|
|
if !ok || !strings.HasPrefix(encoded, "data:image/png;base64,") {
|
||
|
|
f.t.Fatal("local image payload missing")
|
||
|
|
}
|
||
|
|
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(encoded, "data:image/png;base64,"))
|
||
|
|
must(f.t, err)
|
||
|
|
image, err := png.Decode(bytes.NewReader(raw))
|
||
|
|
must(f.t, err)
|
||
|
|
if image.Bounds().Dx() != 240 || image.Bounds().Dy() != 80 || strings.Contains(w.Body.String(), "answer_hash") {
|
||
|
|
f.t.Fatal("image dimensions or response confidentiality changed")
|
||
|
|
}
|
||
|
|
hash := digest([]byte(f.cookie.Value))
|
||
|
|
key := f.s.captchaKey(hash)
|
||
|
|
value, err := f.s.redis.Get(f.ctx, key).Result()
|
||
|
|
must(f.t, err)
|
||
|
|
var stored storedCaptcha
|
||
|
|
must(f.t, json.Unmarshal([]byte(value), &stored))
|
||
|
|
if stored.ID != challenge["captcha_id"] || stored.Mode != "image" || stored.Client != "saas" || stored.Version < 1 || stored.AnswerHash == "" {
|
||
|
|
f.t.Fatal("generated JSON challenge metadata missing")
|
||
|
|
}
|
||
|
|
ttl, err := f.s.redis.PTTL(f.ctx, key).Result()
|
||
|
|
must(f.t, err)
|
||
|
|
if ttl <= 0 {
|
||
|
|
f.t.Fatal("generated challenge expiry missing")
|
||
|
|
}
|
||
|
|
stored.AnswerHash = domainMAC(f.s.crypt.hashKey, "CAPTCHA1\n"+hash+"\n"+stored.ID+"\n654321")
|
||
|
|
must(f.t, f.s.redis.Set(f.ctx, key, captchaJSON(stored), ttl).Err())
|
||
|
|
return stored.ID
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLocalImageCaptchaJSONSuccessAndOneTime(t *testing.T) {
|
||
|
|
f := newLoginFixture(t)
|
||
|
|
setCaptchaTestPolicy(t, f.fixture, "image", "image")
|
||
|
|
f.s.options.Captcha = nil
|
||
|
|
id := f.knownGeneratedImageCaptcha()
|
||
|
|
hash := digest([]byte(f.cookie.Value))
|
||
|
|
must(t, f.s.verifyLoginCaptcha(f.ctx, hash, id, "654321"))
|
||
|
|
if f.s.verifyLoginCaptcha(f.ctx, hash, id, "654321") == nil {
|
||
|
|
t.Fatal("generated image challenge replay accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLocalImageCaptchaJSONPolicyChangeInvalidates(t *testing.T) {
|
||
|
|
f := newLoginFixture(t)
|
||
|
|
setCaptchaTestPolicy(t, f.fixture, "image", "image")
|
||
|
|
id := f.knownGeneratedImageCaptcha()
|
||
|
|
setCaptchaTestPolicy(t, f.fixture, "tencent", "image")
|
||
|
|
if f.s.verifyLoginCaptcha(f.ctx, digest([]byte(f.cookie.Value)), id, "654321") == nil {
|
||
|
|
t.Fatal("old image JSON challenge survived a policy change")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLocalImageCaptchaJSONSMSDeliveryDoesNotUseTencent(t *testing.T) {
|
||
|
|
f := newLoginFixture(t)
|
||
|
|
setCaptchaTestPolicy(t, f.fixture, "image", "image")
|
||
|
|
provider := &fakeCaptchaProvider{}
|
||
|
|
f.s.options.Captcha = provider
|
||
|
|
id := f.knownGeneratedImageCaptcha()
|
||
|
|
input := SMSSendInput{Phone: f.phone, Purpose: "LOGIN", CaptchaID: id, CaptchaCode: "654321"}
|
||
|
|
request := uuid.NewString()
|
||
|
|
w, _ := f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, request)
|
||
|
|
if w.Code != 200 || f.sender.calls != 1 || provider.calls != 0 {
|
||
|
|
t.Fatal("local JSON image verification did not deliver one test SMS independently")
|
||
|
|
}
|
||
|
|
w, _ = f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, request)
|
||
|
|
if w.Code != 200 || f.sender.calls != 1 || provider.calls != 0 {
|
||
|
|
t.Fatal("exact SMS retry changed delivery or used Tencent CAPTCHA")
|
||
|
|
}
|
||
|
|
w, _ = f.request("POST", "/api/v1/web/login/sms/send", input, f.cookie, f.csrf, "")
|
||
|
|
if w.Code != 401 || f.sender.calls != 1 || provider.calls != 0 {
|
||
|
|
t.Fatal("generated image challenge replay caused another delivery")
|
||
|
|
}
|
||
|
|
}
|