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) }