0904
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user