fix: check a password even when the username does not exist

This commit is contained in:
DiheChen committed 2026-11-08 22:44:30 +08:00
1 parent d5e1de09db
commit 27a98e990a
4 files changed
+47

No files matched your search

+7
View File
@@ -117,6 +117,13 @@ func (a *Accounts) Login(ctx context.Context, current member.Member, currentToke
func (a *Accounts) verify(ctx context.Context, username, password string) (port.Account, error) {
account, err := a.members.AccountByUsername(ctx, username)
if errors.Is(err, port.ErrMemberNotFound) {
dummy, err := dummyPasswordHash()
if err != nil {
return port.Account{}, err
}
if _, err := checkPassword(ctx, dummy, password); err != nil {
return port.Account{}, err
}
return port.Account{}, ErrInvalidCredentials
}
if err != nil {
+18
View File
@@ -450,3 +450,21 @@ func TestLoginRejectedWhenGuestSessionOrPasswordChangesMeanwhile(t *testing.T) {
t.Error("登录失败时游客的 token 应仍然有效")
}
}
// 名额占满、请求已经取消时,要做密码计算的请求以 context.Canceled 结束,不做的直接返回结果。
func TestLoginDoesPasswordWorkForUnknownUsername(t *testing.T) {
a, s, _, _, _ := newAccounts(t)
registerAccount(t, a, s, "alice", "correct horse")
guest := createMember(t, s)
if _, err := dummyPasswordHash(); err != nil {
t.Fatal(err)
}
occupyArgonSlots(t)
ctx, cancel := context.WithCancel(t.Context())
cancel()
for _, username := range []string{"alice", "nobody"} {
if _, _, err := a.Login(ctx, guest.Member, guest.MemberToken, clientIP, username, "wrong horse"); !errors.Is(err, context.Canceled) {
t.Errorf("用户名 %s:Login = %v,期望同样等待密码计算的名额", username, err)
}
}
}
+7
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"runtime"
"strings"
"sync"
"golang.org/x/crypto/argon2"
)
@@ -52,6 +53,12 @@ func HashPassword(ctx context.Context, password string) (string, error) {
return passwordPrefix + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(key), nil
}
// 用户名不存在时拿它做一次同样的计算,响应时间不暴露用户名是否存在。密码是随机值,不会与任何人输入的密码匹配。
var dummyPasswordHash = sync.OnceValues(func() (string, error) {
password, _ := newSecret()
return HashPassword(context.Background(), password)
})
// 只接受按当前参数生成的哈希:存储中的密码哈希都由 HashPassword 生成。
func checkPassword(ctx context.Context, hash, password string) (bool, error) {
encoded, ok := strings.CutPrefix(hash, passwordPrefix)
+15
View File
@@ -87,3 +87,18 @@ func TestPasswordWorkReturnsItsSlot(t *testing.T) {
}
}
}
func TestDummyPasswordHashMatchesNoPassword(t *testing.T) {
hash, err := dummyPasswordHash()
if err != nil {
t.Fatal(err)
}
if again, _ := dummyPasswordHash(); again != hash {
t.Error("假哈希应只生成一次")
}
for _, password := range []string{"", "correct horse", "12345678"} {
if ok, err := checkPassword(t.Context(), hash, password); err != nil || ok {
t.Errorf("checkPassword(假哈希, %q) = %v, %v,期望能解析且不匹配", password, ok, err)
}
}
}