package passwordutil import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // 算法标识。 // 历史数据用 legacy(sha256 单轮,无迭代拉伸,抗 GPU 爆破能力弱); // 新密码统一用 argon2id,登录成功检测到 legacy 时应重新哈希升级。 const ( AlgoArgon2id = "argon2id" AlgoLegacy = "legacy" ) // argon2id 参数(OWASP 推荐起步配置:64MB / 3 轮 / 并行 2) const ( argonTime = 3 argonMemory = 64 * 1024 // 单位 KB,即 64MB argonThreads = 2 argonKeyLen = 32 argonSaltLen = 16 ) // legacy 兼容参数 const ( saltBytes = 16 separator = "$" hashLength = 64 // sha256 hex length ) // Hash 使用当前默认算法(argon2id)生成密码存储串。 // // 返回 PHC 标准格式(盐与参数内联,无需独立 salt 列): // // $argon2id$v=19$m=65536,t=3,p=2$$ // // 注意:函数签名与旧版一致,全部调用点无需改动即可切换到新算法。 func Hash(plain string) (string, error) { plain = strings.TrimSpace(plain) if plain == "" { return "", errors.New("password 不能为空") } salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", err } key := argon2.IDKey([]byte(plain), salt, argonTime, argonMemory, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemory, argonTime, argonThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key), ), nil } // Verify 校验明文密码是否匹配存储串,自动识别算法: // - $argon2id$... → argon2id(新) // - $ → legacy sha256(旧,兼容) func Verify(stored, plain string) bool { stored = strings.TrimSpace(stored) plain = strings.TrimSpace(plain) if stored == "" || plain == "" { return false } if strings.HasPrefix(stored, "$argon2id$") { return verifyArgon2id(stored, plain) } return verifyLegacy(stored, plain) } // NeedsRehash 判断已存储的密码是否需要用当前算法重新哈希。 // 登录成功后调用:返回 true 时应拿当次登录的明文重新 Hash 并落库, // 实现旧算法用户「首次登录自动升级」,无需强制全员重置密码。 func NeedsRehash(stored string) bool { return !strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$") } // AlgoOf 返回存储串使用的算法标识,用于审计/统计旧算法存量。 func AlgoOf(stored string) string { if strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$") { return AlgoArgon2id } return AlgoLegacy } // verifyArgon2id 解析 PHC 串并用相同参数重算比对(恒定时间比较)。 func verifyArgon2id(stored, plain string) bool { parts := strings.Split(stored, "$") // ["", "argon2id", "v=19", "m=...,t=...,p=...", salt, hash] if len(parts) != 6 { return false } var memory, iterations, parallelism uint32 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism); err != nil { return false } if memory == 0 || iterations == 0 || parallelism == 0 || parallelism > 255 { return false } threads := uint8(parallelism) salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil || len(salt) == 0 { return false } want, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil || len(want) == 0 { return false } got := argon2.IDKey([]byte(plain), salt, iterations, memory, threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1 } // verifyLegacy 校验旧格式:salt$hash(均为 hex),sha256(salt+plain)。 // 仅用于兼容历史数据,不再用于新密码。 func verifyLegacy(stored, plain string) bool { stored = strings.TrimSpace(stored) plain = strings.TrimSpace(plain) if stored == "" || plain == "" { return false } parts := strings.Split(stored, separator) if len(parts) != 2 { return false } saltHex := strings.TrimSpace(parts[0]) hashHexStored := strings.TrimSpace(parts[1]) if saltHex == "" || len(hashHexStored) != hashLength { return false } if _, err := hex.DecodeString(saltHex); err != nil { return false } return hashHex(saltHex, plain) == strings.ToLower(hashHexStored) } // hashHex 旧算法:sha256(saltHex + plain),供 verifyLegacy 使用。 func hashHex(saltHex, plain string) string { sum := sha256.Sum256([]byte(saltHex + plain)) return hex.EncodeToString(sum[:]) }