5759c1862e
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
2.9 KiB
Go
104 lines
2.9 KiB
Go
package identity
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
currentPBKDF2Iterations = 600_000
|
|
legacyPBKDF2Iterations = 120_000
|
|
maximumPBKDF2Iterations = 2_000_000
|
|
passwordDigestBytes = 32
|
|
)
|
|
|
|
var ErrInvalidPasswordHash = errors.New("invalid password hash")
|
|
|
|
type PasswordHasher struct{}
|
|
|
|
func (PasswordHasher) Hash(password string) (string, error) {
|
|
saltBytes := make([]byte, 16)
|
|
if _, err := rand.Read(saltBytes); err != nil {
|
|
return "", fmt.Errorf("generate password salt: %w", err)
|
|
}
|
|
return hashWithSalt(password, hex.EncodeToString(saltBytes), currentPBKDF2Iterations), nil
|
|
}
|
|
|
|
func (PasswordHasher) Verify(password, stored string) bool {
|
|
iterations, salt, expected, err := parsePasswordHash(stored)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
actual := pbkdf2SHA256([]byte(password), []byte(salt), iterations, len(expected))
|
|
return subtle.ConstantTimeCompare(actual, expected) == 1
|
|
}
|
|
|
|
func (PasswordHasher) NeedsUpgrade(stored string) bool {
|
|
iterations, _, _, err := parsePasswordHash(stored)
|
|
return err == nil && iterations < currentPBKDF2Iterations
|
|
}
|
|
|
|
func hashWithSalt(password, salt string, iterations int) string {
|
|
digest := pbkdf2SHA256([]byte(password), []byte(salt), iterations, passwordDigestBytes)
|
|
return fmt.Sprintf("pbkdf2_sha256$%d$%s$%s", iterations, salt, hex.EncodeToString(digest))
|
|
}
|
|
|
|
func parsePasswordHash(stored string) (int, string, []byte, error) {
|
|
parts := strings.Split(stored, "$")
|
|
iterations := legacyPBKDF2Iterations
|
|
var salt, encodedDigest string
|
|
switch {
|
|
case len(parts) == 4 && parts[0] == "pbkdf2_sha256":
|
|
parsed, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return 0, "", nil, ErrInvalidPasswordHash
|
|
}
|
|
iterations, salt, encodedDigest = parsed, parts[2], parts[3]
|
|
case len(parts) == 2:
|
|
salt, encodedDigest = parts[0], parts[1]
|
|
default:
|
|
return 0, "", nil, ErrInvalidPasswordHash
|
|
}
|
|
if iterations < 1 || iterations > maximumPBKDF2Iterations || salt == "" {
|
|
return 0, "", nil, ErrInvalidPasswordHash
|
|
}
|
|
digest, err := hex.DecodeString(encodedDigest)
|
|
if err != nil || len(digest) != passwordDigestBytes {
|
|
return 0, "", nil, ErrInvalidPasswordHash
|
|
}
|
|
return iterations, salt, digest, nil
|
|
}
|
|
|
|
func pbkdf2SHA256(password, salt []byte, iterations, keyLength int) []byte {
|
|
const hashLength = sha256.Size
|
|
blocks := (keyLength + hashLength - 1) / hashLength
|
|
result := make([]byte, 0, blocks*hashLength)
|
|
buffer := make([]byte, len(salt)+4)
|
|
copy(buffer, salt)
|
|
for block := 1; block <= blocks; block++ {
|
|
binary.BigEndian.PutUint32(buffer[len(salt):], uint32(block))
|
|
mac := hmac.New(sha256.New, password)
|
|
_, _ = mac.Write(buffer)
|
|
u := mac.Sum(nil)
|
|
t := append([]byte(nil), u...)
|
|
for round := 1; round < iterations; round++ {
|
|
mac.Reset()
|
|
_, _ = mac.Write(u)
|
|
u = mac.Sum(nil)
|
|
for index := range t {
|
|
t[index] ^= u[index]
|
|
}
|
|
}
|
|
result = append(result, t...)
|
|
}
|
|
return result[:keyLength]
|
|
}
|