99 lines
2.4 KiB
Go
99 lines
2.4 KiB
Go
|
|
package usecase
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/rand"
|
|||
|
|
"math/big"
|
|||
|
|
"unicode"
|
|||
|
|
|
|||
|
|
"apps/backend/internal/module/member/domain"
|
|||
|
|
|
|||
|
|
"golang.org/x/crypto/bcrypt"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ValidatePasswordPolicy:≥12、含大寫、小寫、數字、符號。
|
|||
|
|
// 規則與 apps/web/src/lib/passwordPolicy.ts 一致;文案見 domain.PasswordPolicyEN/ZH。
|
|||
|
|
func ValidatePasswordPolicy(password string) error {
|
|||
|
|
if password == "" || len([]rune(password)) < domain.PasswordMinLen {
|
|||
|
|
return domain.ErrWeakPassword
|
|||
|
|
}
|
|||
|
|
var upper, lower, digit, symbol bool
|
|||
|
|
for _, r := range password {
|
|||
|
|
switch {
|
|||
|
|
case unicode.IsUpper(r):
|
|||
|
|
upper = true
|
|||
|
|
case unicode.IsLower(r):
|
|||
|
|
lower = true
|
|||
|
|
case unicode.IsDigit(r):
|
|||
|
|
digit = true
|
|||
|
|
case unicode.IsPunct(r) || unicode.IsSymbol(r):
|
|||
|
|
symbol = true
|
|||
|
|
default:
|
|||
|
|
// 空白等非字母數字也算符號類
|
|||
|
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
|||
|
|
symbol = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !upper || !lower || !digit || !symbol {
|
|||
|
|
return domain.ErrWeakPassword
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// HashPassword validates policy then bcrypt.
|
|||
|
|
func HashPassword(password string, cost int) (string, error) {
|
|||
|
|
if err := ValidatePasswordPolicy(password); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
if cost < bcrypt.MinCost {
|
|||
|
|
cost = bcrypt.DefaultCost
|
|||
|
|
}
|
|||
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
|||
|
|
return string(bytes), err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func CheckPasswordHash(password, hash string) bool {
|
|||
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func GetHashingCost(hashedPassword []byte) int {
|
|||
|
|
cost, _ := bcrypt.Cost(hashedPassword)
|
|||
|
|
return cost
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateTempPassword 產生符合政策的臨時密碼(16 字元)。
|
|||
|
|
func GenerateTempPassword() string {
|
|||
|
|
const (
|
|||
|
|
lower = "abcdefghijkmnopqrstuvwxyz"
|
|||
|
|
upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"
|
|||
|
|
digits = "23456789"
|
|||
|
|
symbols = "!@#$%^&*+-="
|
|||
|
|
all = lower + upper + digits + symbols
|
|||
|
|
)
|
|||
|
|
pick := func(set string) byte {
|
|||
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(set))))
|
|||
|
|
if err != nil {
|
|||
|
|
return set[0]
|
|||
|
|
}
|
|||
|
|
return set[n.Int64()]
|
|||
|
|
}
|
|||
|
|
// 保證四類各至少 1
|
|||
|
|
out := []byte{
|
|||
|
|
pick(lower), pick(upper), pick(digits), pick(symbols),
|
|||
|
|
}
|
|||
|
|
// 16 ≥ PasswordMinLen(12)
|
|||
|
|
for len(out) < 16 {
|
|||
|
|
out = append(out, pick(all))
|
|||
|
|
}
|
|||
|
|
// shuffle
|
|||
|
|
for i := len(out) - 1; i > 0; i-- {
|
|||
|
|
jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
|||
|
|
j := 0
|
|||
|
|
if err == nil {
|
|||
|
|
j = int(jBig.Int64())
|
|||
|
|
}
|
|||
|
|
out[i], out[j] = out[j], out[i]
|
|||
|
|
}
|
|||
|
|
return string(out)
|
|||
|
|
}
|