package auth import ( "crypto/rand" "crypto/subtle" "encoding/hex" "errors" "fmt" "log" "strconv" "strings" "time" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" ) var defaultPasswordManager = NewPassword() // Password 密码管理器(Argon2id) type Password struct { memory uint32 iterations uint32 parallelism uint8 saltLength uint32 keyLength uint32 } // NewPassword 创建密码管理器 func NewPassword() *Password { return &Password{ memory: 64 * 1024, // 64MB(符合 OWASP 建议) iterations: 5, // 5 次迭代(保守值,高于 OWASP 建议的 3) parallelism: 4, // 4 并行(符合 OWASP 建议,防御 GPU 破解) saltLength: 16, // 16 字节盐(符合 OWASP 最低要求) keyLength: 32, // 32 字节密钥 } } // CalibrateArgon2id 在当前机器上自动校准 Argon2id 参数,确保单次哈希时间不超过 budget。 // // 校准策略(优先保留 memory,其次降低 iterations): // 1. 用默认参数(64MB/5iter)测量一次哈希耗时。 // 2. 若耗时 ≤ budget,直接返回:默认参数已安全。 // 3. 若耗时 > budget,先尝试降低 iterations(最低 2)。 // 4. 若仍超预算,再二分降低 memory(最低 16MB)。 // 5. 若仍超预算,打印 warn 但不更改参数(避免参数过弱)。 // // 建议在 main() 启动阶段调用一次,结果会更新全局 defaultPasswordManager。 // budget 推荐值:500ms(登录接口 P99 目标 < 1000ms,留出网络/DB 余量)。 func CalibrateArgon2id(budget time.Duration) { if budget <= 0 { budget = 500 * time.Millisecond } probe := func(mem uint32, iter uint32, par uint8) time.Duration { salt := make([]byte, 16) _, _ = rand.Read(salt) start := time.Now() _ = argon2.IDKey([]byte("calibration-probe"), salt, iter, mem, par, 32) return time.Since(start) } mem := defaultPasswordManager.memory iter := defaultPasswordManager.iterations par := defaultPasswordManager.parallelism elapsed := probe(mem, iter, par) log.Printf("argon2id calibration: default params (m=%dKB, t=%d, p=%d) → %v", mem, iter, par, elapsed) if elapsed <= budget { log.Printf("argon2id calibration: default params are within budget (%v ≤ %v), no adjustment needed", elapsed, budget) return } // Step 1:尝试降低 iterations(最低 2,低于 2 不满足 OWASP 最低要求) for iter > 2 { iter-- elapsed = probe(mem, iter, par) log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed) if elapsed <= budget { break } } // Step 2:若仍超预算,二分降低 memory(最低 16MB = 16*1024 KiB) if elapsed > budget { const minMem = 16 * 1024 for mem > minMem && elapsed > budget { mem /= 2 if mem < minMem { mem = minMem } elapsed = probe(mem, iter, par) log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed) } } if elapsed > budget { log.Printf("argon2id calibration: WARN — even minimum params (m=%dKB, t=%d) take %v > %v; check server load", mem, iter, elapsed, budget) // 不降低到不安全参数,保持当前已尝试的最低值 } else { log.Printf("argon2id calibration: adjusted params m=%dKB t=%d p=%d → %v (budget: %v)", mem, iter, par, elapsed, budget) } // 更新全局默认管理器(仅在此阶段修改,后续不再变更) defaultPasswordManager.memory = mem defaultPasswordManager.iterations = iter } // Hash 哈希密码(使用Argon2id + 随机盐) func (p *Password) Hash(password string) (string, error) { // 使用 crypto/rand 生成真正随机的盐 salt := make([]byte, p.saltLength) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("生成随机盐失败: %w", err) } // 使用Argon2id哈希密码 hash := argon2.IDKey( []byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength, ) // 格式: $argon2id$v=$m=,t=,p=$$ encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, hex.EncodeToString(salt), hex.EncodeToString(hash), ) return encoded, nil } // Verify 验证密码 func (p *Password) Verify(hashedPassword, password string) bool { // 支持 bcrypt 格式(兼容旧数据) if strings.HasPrefix(hashedPassword, "$2a$") || strings.HasPrefix(hashedPassword, "$2b$") { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil } // 解析 Argon2id 格式 parts := strings.Split(hashedPassword, "$") // 格式: ["", "argon2id", "v=", "m=,t=,p=", "", ""] if len(parts) != 6 || parts[1] != "argon2id" { return false } // 解析参数 var memory, iterations uint32 var parallelism uint8 params := strings.Split(parts[3], ",") if len(params) != 3 { return false } for _, param := range params { kv := strings.SplitN(param, "=", 2) if len(kv) != 2 { return false } val, err := strconv.ParseUint(kv[1], 10, 64) if err != nil { return false } switch kv[0] { case "m": // #nosec G115 - argon2 memory param is constrained by spec to reasonable values memory = uint32(val) // #nosec G115 case "t": // #nosec G115 - argon2 iterations param is constrained by spec to reasonable values iterations = uint32(val) // #nosec G115 case "p": // #nosec G115 - argon2 parallelism param is constrained by spec to reasonable values parallelism = uint8(val) // #nosec G115 } } // 解码盐和存储的哈希 salt, err := hex.DecodeString(parts[4]) if err != nil { return false } storedHash, err := hex.DecodeString(parts[5]) if err != nil { return false } // 用相同参数重新计算哈希 // #nosec G115 - bcrypt hash is typically 60 chars, fits in uint32 computedHash := argon2.IDKey( []byte(password), salt, iterations, memory, parallelism, uint32(len(storedHash)), // #nosec G115 ) // 常数时间比较,防止时序攻击 return subtle.ConstantTimeCompare(storedHash, computedHash) == 1 } // HashPassword hashes passwords with Argon2id for new credentials. func HashPassword(password string) (string, error) { return defaultPasswordManager.Hash(password) } // VerifyPassword verifies both Argon2id and legacy bcrypt password hashes. func VerifyPassword(hashedPassword, password string) bool { return defaultPasswordManager.Verify(hashedPassword, password) } // ErrInvalidPassword 密码无效错误 var ErrInvalidPassword = errors.New("密码无效") // BcryptHash 使用bcrypt哈希密码(兼容性支持) func BcryptHash(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("bcrypt加密失败: %w", err) } return string(hash), nil } // BcryptVerify 使用bcrypt验证密码 func BcryptVerify(hashedPassword, password string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil }