完善不同加密方法的使用
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
// Package argon2x 基于 Argon2id(RFC 9106)提供口令哈希与密钥派生,是当前
|
||||
// 口令存储与低熵口令派生的首选方案。
|
||||
//
|
||||
// Argon2id 同时具备数据无关(Argon2i 式)与数据相关(Argon2d 式)的内存访问
|
||||
// 模式,兼顾侧信道抵抗与 GPU/ASIC 抵抗能力。
|
||||
//
|
||||
// 安全注意事项:
|
||||
// - 参数选择依据:OWASP 口令存储建议 Argon2id 至少满足
|
||||
// m=19456 KiB/t=2/p=1 或 m=47104 KiB/t=1/p=1;本包默认
|
||||
// t=3、m=64 MiB、p=4,对应 RFC 9106 的第二推荐档,适合服务器登录场景。
|
||||
// 移动端或高并发服务应实测后下调 t/m,但不能低于本包的下限。
|
||||
// - 每个口令必须使用独立的随机盐(默认 16 字节),盐是公开值,需与哈希串
|
||||
// 一起保存。
|
||||
// - 推荐直接用 HashPassword 返回的 PHC 字符串(自描述参数)落库,用
|
||||
// VerifyPassword 校验;手工管理参数时参数一旦变更旧哈希将无法校验。
|
||||
// - 口令、盐、哈希都不得写入日志或错误文本,本包的所有 error 只包含长度与
|
||||
// 参数信息。
|
||||
//
|
||||
// 本包任何情况下都不会 panic:所有非法输入都返回 error。
|
||||
package argon2x
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTime 是默认的迭代次数(time cost)。次数越高,攻击者每条候选口令
|
||||
// 需要重复的内存填充越多,耗时线性增长。
|
||||
DefaultTime = 3
|
||||
|
||||
// DefaultMemory 是默认的内存代价,单位 KiB,64*1024 即 64 MiB。内存是
|
||||
// 抵抗 GPU/ASIC 的主要参数,不能低于 8*1024。
|
||||
DefaultMemory = 64 * 1024
|
||||
|
||||
// DefaultThreads 是默认的并行度(lanes)。它只影响本机计算速度、不改变
|
||||
// 总内存占用,一般取 CPU 物理核心数。
|
||||
DefaultThreads = 4
|
||||
|
||||
// DefaultSaltLength 是推荐的盐长度(字节)。
|
||||
DefaultSaltLength = 16
|
||||
|
||||
// MinSaltLength 是允许的最小盐长度(字节),更短的盐会显著提高碰撞概率。
|
||||
MinSaltLength = 8
|
||||
|
||||
// DefaultKeyLength 是推荐的输出长度(字节),32 字节可直接作为
|
||||
// AES-256 或 HMAC-SHA256 的密钥。
|
||||
DefaultKeyLength = 32
|
||||
|
||||
// MaxKeyLength 是允许的最大输出长度(字节),用于拦截误传的缓冲区大小。
|
||||
MaxKeyLength = 1024
|
||||
)
|
||||
|
||||
const (
|
||||
// minKeyLength 是允许的最小输出长度(字节)。16 字节是口令校验值的最低
|
||||
// 可接受强度,低于该值的 PHC 串一律拒绝。
|
||||
minKeyLength = 16
|
||||
|
||||
// maxSaltLength 是允许的最大盐长度(字节)。
|
||||
maxSaltLength = 1024
|
||||
|
||||
// maxMemory 是允许的最大内存代价(KiB),2*1024*1024 即 2 GiB,用于拦截
|
||||
// 误传或恶意构造的超大 memory 值导致的 OOM(进程被杀死,无法恢复)。
|
||||
maxMemory = 2 * 1024 * 1024
|
||||
|
||||
// maxWork 是单次派生允许的"内存代价 × 迭代次数"上限(单位 KiB·次),
|
||||
// 用于约束最坏情况下的 CPU 时间。VerifyPassword 的参数来自可被篡改的 PHC
|
||||
// 字符串,如果没有该上限,攻击者只要写入 t=4294967295 就能让校验线程
|
||||
// 长时间占用 CPU(拒绝服务)。4<<20 约对应 4 GiB 的内存填充量,
|
||||
// 默认参数(m=64 MiB、t=3)只占该预算的 5%。
|
||||
maxWork = 4 << 20
|
||||
|
||||
// phcAlgorithm 是本包使用的 PHC 算法标识。
|
||||
phcAlgorithm = "argon2id"
|
||||
)
|
||||
|
||||
// GenerateSalt 返回 length 字节的密码学安全随机盐,供 DeriveKey 使用。
|
||||
//
|
||||
// length 必须落在 [MinSaltLength, 1024],推荐 DefaultSaltLength。每次哈希口令
|
||||
// 都必须生成新盐并与派生结果一起保存;盐是公开值,缺失它无法复现密钥。
|
||||
// 随机源不可用时返回 error,绝不退化为可预测的盐。
|
||||
func GenerateSalt(length int) ([]byte, error) {
|
||||
if length < MinSaltLength {
|
||||
return nil, fmt.Errorf("argon2x: 盐长度 %d 小于最小值 %d", length, MinSaltLength)
|
||||
}
|
||||
if length > maxSaltLength {
|
||||
return nil, fmt.Errorf("argon2x: 盐长度 %d 超过最大值 %d", length, maxSaltLength)
|
||||
}
|
||||
|
||||
salt := make([]byte, length)
|
||||
// 用 io.ReadFull(rand.Reader) 而非 rand.Read:后者在随机源故障时会直接
|
||||
// 终止进程,而本包要求把失败作为 error 返回。
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, fmt.Errorf("argon2x: 读取随机盐失败: %w", err)
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// DeriveKey 使用 Argon2id 从 password 与 salt 派生 keyLength 字节密钥。
|
||||
//
|
||||
// time 为迭代次数(>=1),memory 为内存代价(KiB,>=8*1024 且 <=2 GiB),
|
||||
// threads 为并行度(>=1),keyLength 为输出长度([16, 1024])。此外 memory×time
|
||||
// 不得超过单次派生的计算量上限(约 4 GiB 内存填充量)。任一参数越界都返回
|
||||
// error,绝不 panic。
|
||||
//
|
||||
// 返回的密钥是确定性函数:相同 (password, salt, time, memory, threads,
|
||||
// keyLength) 必然得到相同结果,因此必须保存盐与参数才能复现。
|
||||
func DeriveKey(password, salt []byte, time, memory uint32, threads uint8, keyLength uint32) ([]byte, error) {
|
||||
if err := validateParams(salt, time, memory, threads, keyLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := argon2.IDKey(password, salt, time, memory, threads, keyLength)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveKeyWithRandomSalt 生成 saltLength 字节随机盐并立即派生密钥,一次性返回
|
||||
// (key, salt, error)。调用方必须把 salt 与参数一起保存,否则派生结果无法复现。
|
||||
//
|
||||
// saltLength 通常传 DefaultSaltLength;其余参数含义同 DeriveKey。参数非法或
|
||||
// 随机源失败时返回 error,此时 key 与 salt 均为 nil。
|
||||
func DeriveKeyWithRandomSalt(password []byte, saltLength int, time, memory uint32, threads uint8, keyLength uint32) (key, salt []byte, err error) {
|
||||
salt, err = GenerateSalt(saltLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
key, err = DeriveKey(password, salt, time, memory, threads, keyLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return key, salt, nil
|
||||
}
|
||||
|
||||
// Verify 用相同参数重新派生并以常量时间比较 expectedKey,返回是否匹配。
|
||||
//
|
||||
// 参数非法时返回 error;参数合法但密钥不匹配时返回 (false, nil)。expectedKey
|
||||
// 长度与 keyLength 不一致时判定为不匹配。
|
||||
//
|
||||
// 安全提示:必须用本函数而不是 bytes.Equal/== 比较口令哈希,后者会提前返回并
|
||||
// 泄漏匹配前缀长度,构成时序侧信道。
|
||||
func Verify(password, salt, expectedKey []byte, time, memory uint32, threads uint8, keyLength uint32) (bool, error) {
|
||||
derived, err := DeriveKey(password, salt, time, memory, threads, keyLength)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// 比较后立即清零本地派生结果,缩短口令哈希在内存中的驻留时间。
|
||||
defer func() {
|
||||
for i := range derived {
|
||||
derived[i] = 0
|
||||
}
|
||||
}()
|
||||
|
||||
if len(expectedKey) != len(derived) {
|
||||
return false, nil
|
||||
}
|
||||
return subtle.ConstantTimeCompare(derived, expectedKey) == 1, nil
|
||||
}
|
||||
|
||||
// HashPassword 使用默认参数(time=3、memory=64 MiB、threads=4、keyLength=32)
|
||||
// 与 16 字节随机盐,把口令编码成自描述的 PHC 字符串返回,形如:
|
||||
//
|
||||
// $argon2id$v=19$m=65536,t=3,p=4$<base64盐>$<base64哈希>
|
||||
//
|
||||
// 盐与哈希使用 encoding/base64.RawStdEncoding(无填充)编码。返回值不含口令
|
||||
// 明文,可以安全落库;每个口令必须重新调用本函数以获得新的随机盐。校验请使用
|
||||
// VerifyPassword,不要手工比较字符串。
|
||||
//
|
||||
// 安全提示:默认参数在常见服务器上约需 50~200ms,请结合部署环境压测后调整;
|
||||
// 需要自定义参数时使用 HashPasswordWithParams。
|
||||
func HashPassword(password []byte) (string, error) {
|
||||
return HashPasswordWithParams(password, DefaultTime, DefaultMemory, DefaultThreads, DefaultKeyLength)
|
||||
}
|
||||
|
||||
// HashPasswordWithParams 使用指定参数与 16 字节随机盐生成 PHC 格式的口令哈希。
|
||||
//
|
||||
// 参数含义与校验规则同 DeriveKey:time>=1、memory>=8*1024 且 <=2 GiB、
|
||||
// threads>=1、keyLength 在 [16, 1024]。调大 time/memory 会提高暴力破解成本,
|
||||
// 也会等比增加登录耗时,应压测后再定。
|
||||
func HashPasswordWithParams(password []byte, time, memory uint32, threads uint8, keyLength uint32) (string, error) {
|
||||
salt, err := GenerateSalt(DefaultSaltLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := DeriveKey(password, salt, time, memory, threads, keyLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encodePHC(salt, key, time, memory, threads), nil
|
||||
}
|
||||
|
||||
// VerifyPassword 解析 PHC 字符串并用其中的算法参数重新派生口令哈希,以常量时间
|
||||
// 比较后返回是否匹配。
|
||||
//
|
||||
// 格式非法(字段数量、算法标识、版本、参数、base64、长度不合法或非规范编码)时
|
||||
// 返回 error,绝不静默返回 false;格式合法但口令错误时返回 (false, nil)。
|
||||
// 由于参数来自被校验的字符串本身,本函数会自动兼容历史参数(例如旧数据使用
|
||||
// m=65536,t=3,p=4,新数据使用 m=131072,t=4,p=8)。
|
||||
//
|
||||
// 安全提示:encodedHash 属于可被篡改的输入,本函数对其做严格解析并限制参数
|
||||
// 上限(memory <= 2 GiB、memory×time 计算量 <= 4 GiB 填充量),避免攻击者构造
|
||||
// 超大 memory 造成 OOM、或超大 time 长时间占用 CPU。
|
||||
func VerifyPassword(password []byte, encodedHash string) (bool, error) {
|
||||
time, memory, threads, salt, expected, err := decodePHC(encodedHash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return Verify(password, salt, expected, time, memory, threads, uint32(len(expected)))
|
||||
}
|
||||
|
||||
// validateParams 是所有派生入口共用的参数闸门,保证非法输入在进入 Argon2 之前
|
||||
// 就变成明确的 error,而不是 panic 或 OOM。
|
||||
func validateParams(salt []byte, time, memory uint32, threads uint8, keyLength uint32) error {
|
||||
if len(salt) == 0 {
|
||||
return fmt.Errorf("argon2x: 盐不能为空")
|
||||
}
|
||||
if len(salt) < MinSaltLength {
|
||||
return fmt.Errorf("argon2x: 盐长度 %d 小于最小值 %d", len(salt), MinSaltLength)
|
||||
}
|
||||
if len(salt) > maxSaltLength {
|
||||
return fmt.Errorf("argon2x: 盐长度 %d 超过最大值 %d", len(salt), maxSaltLength)
|
||||
}
|
||||
if time < 1 {
|
||||
return fmt.Errorf("argon2x: 迭代次数 time=%d 必须大于等于 1", time)
|
||||
}
|
||||
if memory < 8*1024 {
|
||||
return fmt.Errorf("argon2x: 内存代价 memory=%d KiB 小于下限 %d KiB", memory, 8*1024)
|
||||
}
|
||||
if memory > maxMemory {
|
||||
return fmt.Errorf("argon2x: 内存代价 memory=%d KiB 超过上限 %d KiB", memory, maxMemory)
|
||||
}
|
||||
if threads < 1 {
|
||||
return fmt.Errorf("argon2x: 并行度 threads=%d 必须大于等于 1", threads)
|
||||
}
|
||||
if uint32(threads)*8 > memory {
|
||||
// RFC 9106 要求 m >= 8p;本包下限已隐含满足,这里仅作防御性校验。
|
||||
return fmt.Errorf("argon2x: 内存代价 memory=%d KiB 不足以支撑 %d 个 lane(至少需要 %d KiB)", memory, threads, uint32(threads)*8)
|
||||
}
|
||||
// memory 与 time 都是 uint32,乘积最大 (2^32-1)^2 < 2^64,uint64 乘法不会溢出。
|
||||
if uint64(memory)*uint64(time) > maxWork {
|
||||
return fmt.Errorf("argon2x: 参数组合 memory=%d KiB × time=%d 超过单次派生的计算量上限 %d KiB·次", memory, time, maxWork)
|
||||
}
|
||||
if keyLength < minKeyLength {
|
||||
return fmt.Errorf("argon2x: 输出长度 %d 小于最小值 %d", keyLength, minKeyLength)
|
||||
}
|
||||
if keyLength > MaxKeyLength {
|
||||
return fmt.Errorf("argon2x: 输出长度 %d 超过最大值 %d", keyLength, MaxKeyLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodePHC 生成标准 PHC 字符串:$argon2id$v=19$m=<KiB>,t=<次>,p=<并行度>$<盐>$<哈希>,
|
||||
// 盐与哈希用无填充 base64(RawStdEncoding)编码。
|
||||
func encodePHC(salt, key []byte, time, memory uint32, threads uint8) string {
|
||||
return fmt.Sprintf("$%s$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
phcAlgorithm,
|
||||
argon2.Version,
|
||||
memory,
|
||||
time,
|
||||
threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
)
|
||||
}
|
||||
|
||||
// decodePHC 严格解析 PHC 字符串并返回其中的参数、盐与期望哈希。
|
||||
// 任何字段缺失、顺序错误、非规范数字或非规范 base64 都返回 error。
|
||||
func decodePHC(encodedHash string) (time, memory uint32, threads uint8, salt, key []byte, err error) {
|
||||
if encodedHash == "" {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: PHC 字符串不能为空")
|
||||
}
|
||||
|
||||
// 严格按 PHC 字符串格式(RFC 9106 / phc-string-format)解析:必须以 "$" 开头,
|
||||
// 共 6 个字段,省略首字符或增删字段都视为非法输入。
|
||||
parts := strings.Split(encodedHash, "$")
|
||||
if len(parts) != 6 || parts[0] != "" {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: PHC 字符串格式非法:期望 6 个由 $ 分隔的字段")
|
||||
}
|
||||
if parts[1] != phcAlgorithm {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 不支持的算法标识,仅支持 %s", phcAlgorithm)
|
||||
}
|
||||
|
||||
version, err := parseUintField(parts[2], "v=", 32)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, err
|
||||
}
|
||||
if version != uint64(argon2.Version) {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 不支持的 Argon2 版本 v=%d,仅支持 v=%d", version, argon2.Version)
|
||||
}
|
||||
|
||||
params := strings.Split(parts[3], ",")
|
||||
if len(params) != 3 {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: PHC 参数段格式非法, 期望形如 m=65536,t=3,p=4")
|
||||
}
|
||||
m, err := parseUintField(params[0], "m=", 32)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, err
|
||||
}
|
||||
t, err := parseUintField(params[1], "t=", 32)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, err
|
||||
}
|
||||
p, err := parseUintField(params[2], "p=", 8)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, err
|
||||
}
|
||||
|
||||
salt, err = decodeBase64Field(parts[4])
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 解析盐失败: %w", err)
|
||||
}
|
||||
key, err = decodeBase64Field(parts[5])
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 解析哈希失败: %w", err)
|
||||
}
|
||||
if len(salt) == 0 {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 盐不能为空")
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return 0, 0, 0, nil, nil, fmt.Errorf("argon2x: 哈希不能为空")
|
||||
}
|
||||
|
||||
return uint32(t), uint32(m), uint8(p), salt, key, nil
|
||||
}
|
||||
|
||||
// parseUintField 解析 "<prefix><十进制整数>" 形式的字段,并拒绝前导零、正负号等
|
||||
// 非规范写法,避免同一组参数存在多种字符串表示。
|
||||
func parseUintField(field, prefix string, bitSize int) (uint64, error) {
|
||||
digits, ok := strings.CutPrefix(field, prefix)
|
||||
if !ok || digits == "" {
|
||||
return 0, fmt.Errorf("argon2x: PHC 字段缺少前缀 %q 或值为空", prefix)
|
||||
}
|
||||
value, err := strconv.ParseUint(digits, 10, bitSize)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("argon2x: PHC 字段 %q 不是合法的十进制整数: %w", prefix, err)
|
||||
}
|
||||
if strconv.FormatUint(value, 10) != digits {
|
||||
return 0, fmt.Errorf("argon2x: PHC 字段 %q 不是规范十进制写法", prefix)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// decodeBase64Field 解码无填充 base64 字段,并要求重新编码后与原文完全一致,
|
||||
// 从而拒绝 "=" 填充、空白字符、非规范尾比特等畸形输入。
|
||||
func decodeBase64Field(field string) ([]byte, error) {
|
||||
if field == "" {
|
||||
return nil, fmt.Errorf("字段为空")
|
||||
}
|
||||
raw, err := base64.RawStdEncoding.DecodeString(field)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无填充 base64 解码失败: %w", err)
|
||||
}
|
||||
if base64.RawStdEncoding.EncodeToString(raw) != field {
|
||||
return nil, fmt.Errorf("不是规范的无填充 base64 编码")
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
Reference in New Issue
Block a user