86 lines
2.8 KiB
Go
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)
|
|
}
|