Files
tamabox/internal/db/password.go
T
2026-09-08 10:10:44 +08:00

86 lines
2.8 KiB
Go

package db
import (
"crypto/subtle"
"encoding/hex"
"strings"
"github.com/sirupsen/logrus"
"github.com/wuhan005/gadget"
"golang.org/x/crypto/bcrypt"
"github.com/tamakyi/TamaBox/internal/conf"
)
// bcryptCost is the cost parameter for bcrypt (10 is a good default, 12 for higher security).
const bcryptCost = 10
// bcryptMaxPasswordLen is the effective input length of bcrypt (72 bytes).
// bcrypt 只使用前 72 字节;新版 x/crypto 对超长密码直接返回错误,
// 这里统一截断到 72 字节(与 bcrypt 原生语义一致),避免走错误分支。
const bcryptMaxPasswordLen = 72
// encodePasswordBcrypt hashes a password with bcrypt.
// 注意:失败时绝不能静默降级为 HMAC-SHA1 弱哈希(此前行为),
// 返回空串会让该账号所有密码校验失败并留下错误日志,比弱哈希更安全。
func encodePasswordBcrypt(password string) string {
if len(password) > bcryptMaxPasswordLen {
password = password[:bcryptMaxPasswordLen]
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
// cost 为常量 10,正常不会到这里;防御性记录,不降级为弱哈希。
logrus.WithError(err).Error("bcrypt generate failed")
return ""
}
return string(hash)
}
// isBcryptHash checks if the given string is a bcrypt hash.
func isBcryptHash(hash string) bool {
return strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$")
}
// IsBcryptHash exported for use in other packages.
func IsBcryptHash(hash string) bool {
return isBcryptHash(hash)
}
// authenticatePassword verifies a password against either bcrypt or legacy HMAC-SHA1 hash.
// Returns (true, newHash) if the password matches. If the old HMAC-SHA1 format was used,
// newHash contains the upgraded bcrypt hash — caller should persist it.
func authenticatePassword(storedHash, password string) (bool, string) {
if storedHash == "" {
return false, ""
}
// Try bcrypt first (new format).
if isBcryptHash(storedHash) {
err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
return err == nil, ""
}
// Fall back to legacy HMAC-SHA1(常量时间比较,防时序侧信道)。
legacyHash := gadget.HmacSha1(password, conf.Server.Salt)
if subtle.ConstantTimeCompare([]byte(legacyHash), []byte(storedHash)) == 1 {
// Password matches legacy format — upgrade to bcrypt.
return true, encodePasswordBcrypt(password)
}
return false, ""
}
// needsRehash checks if a bcrypt hash should be rehashed (e.g., cost changed).
func needsRehash(hash string) bool {
cost, err := bcrypt.Cost([]byte(hash))
if err != nil {
return true
}
return cost != bcryptCost
}
// HexEncodeToString is a helper to encode bytes as hex for legacy hash comparison.
func HexEncodeToString(src []byte) string {
return hex.EncodeToString(src)
}