完善不同加密方法的使用
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
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package argon2x_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
|
||||
"code.yun.ink/pkg/encryptx/argon2x"
|
||||
)
|
||||
|
||||
// 测试统一使用远小于默认值的内存/迭代参数,避免测试机耗时过长。
|
||||
const (
|
||||
testTime = 1
|
||||
testMemory = 8 * 1024 // 8 MiB,恰为本包允许的下限
|
||||
testThreads = 1
|
||||
testKeyLen = 32
|
||||
)
|
||||
|
||||
// TestDeriveKeyKnownVector 使用 OpenSSL 3.5.7 的 ARGON2ID KDF 作为独立实现的
|
||||
// 已知答案:
|
||||
//
|
||||
// openssl kdf -keylen 32 -kdfopt pass:password -kdfopt salt:saltsaltsalt \
|
||||
// -kdfopt iter:2 -kdfopt memcost:8192 -kdfopt lanes:1 -kdfopt threads:1 ARGON2ID
|
||||
func TestDeriveKeyKnownVector(t *testing.T) {
|
||||
const want = "5c118b7084f1df7207ae2e4a594ea22c50b22396651abd35b9224f1d4ba0173a"
|
||||
|
||||
got, err := argon2x.DeriveKey([]byte("password"), []byte("saltsaltsalt"), 2, 8192, 1, 32)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if hex.EncodeToString(got) != want {
|
||||
t.Errorf("与 OpenSSL 的 Argon2id 结果不一致\n got: %s\nwant: %s", hex.EncodeToString(got), want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyMatchesArgon2id 交叉验证本包确实使用 Argon2id(而不是 Argon2i/2d),
|
||||
// 且参数透传正确。
|
||||
func TestDeriveKeyMatchesArgon2id(t *testing.T) {
|
||||
password := []byte("password")
|
||||
salt := []byte("saltsaltsalt")
|
||||
|
||||
got, err := argon2x.DeriveKey(password, salt, 2, 16384, 2, 48)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
want := argon2.IDKey(password, salt, 2, 16384, 2, 48)
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Error("DeriveKey 与 argon2.IDKey(Argon2id) 结果不一致")
|
||||
}
|
||||
|
||||
// 与 Argon2i 必须不同,否则说明模式选错了。
|
||||
if bytes.Equal(got, argon2.Key(password, salt, 2, 16384, 2, 48)) {
|
||||
t.Error("DeriveKey 的结果与 Argon2i 相同,未使用 Argon2id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveKeyReproducible(t *testing.T) {
|
||||
password := []byte("正确的马电池订书钉")
|
||||
salt := []byte("0123456789abcdef")
|
||||
|
||||
k1, err := argon2x.DeriveKey(password, salt, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
k2, err := argon2x.DeriveKey(password, salt, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Fatal("相同输入两次派生结果不一致")
|
||||
}
|
||||
if len(k1) != testKeyLen {
|
||||
t.Fatalf("派生长度 = %d, 期望 %d", len(k1), testKeyLen)
|
||||
}
|
||||
|
||||
otherSalt, err := argon2x.DeriveKey(password, []byte("fedcba9876543210"), testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("换盐派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherSalt) {
|
||||
t.Error("不同盐得到了相同密钥")
|
||||
}
|
||||
|
||||
otherTime, err := argon2x.DeriveKey(password, salt, testTime+1, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("换迭代次数派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherTime) {
|
||||
t.Error("不同迭代次数得到了相同密钥")
|
||||
}
|
||||
|
||||
otherMemory, err := argon2x.DeriveKey(password, salt, testTime, testMemory*2, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("换内存代价派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherMemory) {
|
||||
t.Error("不同内存代价得到了相同密钥")
|
||||
}
|
||||
|
||||
otherThreads, err := argon2x.DeriveKey(password, salt, testTime, testMemory, testThreads+1, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("换并行度派生失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(k1, otherThreads) {
|
||||
t.Error("不同并行度得到了相同密钥")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeriveKeyWithRandomSalt 断言返回的盐确实能复现同一个密钥。
|
||||
func TestDeriveKeyWithRandomSalt(t *testing.T) {
|
||||
password := []byte("hunter2")
|
||||
|
||||
key, salt, err := argon2x.DeriveKeyWithRandomSalt(password, argon2x.DefaultSaltLength, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKeyWithRandomSalt 返回错误: %v", err)
|
||||
}
|
||||
if len(salt) != argon2x.DefaultSaltLength {
|
||||
t.Fatalf("返回的盐长度 = %d, 期望 %d", len(salt), argon2x.DefaultSaltLength)
|
||||
}
|
||||
if len(key) != testKeyLen {
|
||||
t.Fatalf("返回的密钥长度 = %d, 期望 %d", len(key), testKeyLen)
|
||||
}
|
||||
|
||||
reproduced, err := argon2x.DeriveKey(password, salt, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("用返回的盐复现失败: %v", err)
|
||||
}
|
||||
if !bytes.Equal(key, reproduced) {
|
||||
t.Fatal("用 DeriveKeyWithRandomSalt 返回的盐无法复现同一密钥")
|
||||
}
|
||||
|
||||
key2, salt2, err := argon2x.DeriveKeyWithRandomSalt(password, argon2x.DefaultSaltLength, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("第二次 DeriveKeyWithRandomSalt 失败: %v", err)
|
||||
}
|
||||
if bytes.Equal(salt, salt2) {
|
||||
t.Error("两次调用返回了相同的盐")
|
||||
}
|
||||
if bytes.Equal(key, key2) {
|
||||
t.Error("两次调用返回了相同的密钥")
|
||||
}
|
||||
|
||||
for _, bad := range []int{0, -1, argon2x.MinSaltLength - 1, 1025, math.MaxInt} {
|
||||
if _, _, err := argon2x.DeriveKeyWithRandomSalt(password, bad, testTime, testMemory, testThreads, testKeyLen); err == nil {
|
||||
t.Errorf("saltLength=%d 时未返回错误", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
password := []byte("s3cret-口令")
|
||||
salt := []byte("0123456789abcdef")
|
||||
key, err := argon2x.DeriveKey(password, salt, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("派生失败: %v", err)
|
||||
}
|
||||
|
||||
ok, err := argon2x.Verify(password, salt, key, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("Verify 返回错误: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("正确口令未通过校验")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
password []byte
|
||||
salt []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"错误口令", []byte("s3cret-口令x"), salt, key},
|
||||
{"错误盐", password, []byte("fedcba9876543210"), key},
|
||||
{"哈希被篡改", password, salt, append(append([]byte{}, key[:len(key)-1]...), key[len(key)-1]^0x01)},
|
||||
{"哈希被截断", password, salt, key[:len(key)-1]},
|
||||
{"哈希被加长", password, salt, append(append([]byte{}, key...), 0x00)},
|
||||
{"空哈希", password, salt, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := argon2x.Verify(tc.password, tc.salt, tc.expected, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("参数合法时 Verify 不应返回错误: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Error("不匹配的哈希被判为匹配")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParameterValidation 覆盖非法参数:必须返回 error 且不 panic。
|
||||
func TestParameterValidation(t *testing.T) {
|
||||
validSalt := bytes.Repeat([]byte{0xA5}, argon2x.MinSaltLength)
|
||||
password := []byte("pw")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
salt []byte
|
||||
time uint32
|
||||
memory uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
saltInvalid bool
|
||||
}{
|
||||
{"盐为 nil", nil, testTime, testMemory, testThreads, testKeyLen, true},
|
||||
{"盐为空切片", []byte{}, testTime, testMemory, testThreads, testKeyLen, true},
|
||||
{"盐过短", []byte("1234567"), testTime, testMemory, testThreads, testKeyLen, true},
|
||||
{"盐过长", bytes.Repeat([]byte{0x01}, 1025), testTime, testMemory, testThreads, testKeyLen, true},
|
||||
{"time 为 0", validSalt, 0, testMemory, testThreads, testKeyLen, false},
|
||||
{"time 为 MaxUint32", validSalt, math.MaxUint32, testMemory, testThreads, testKeyLen, false},
|
||||
{"计算量超上限", validSalt, 1 << 20, 64 * 1024, testThreads, testKeyLen, false},
|
||||
{"memory 为 0", validSalt, testTime, 0, testThreads, testKeyLen, false},
|
||||
{"memory 低于下限", validSalt, testTime, 8*1024 - 1, testThreads, testKeyLen, false},
|
||||
{"memory 超上限", validSalt, testTime, 2*1024*1024 + 1, testThreads, testKeyLen, false},
|
||||
{"memory 为 MaxUint32", validSalt, testTime, math.MaxUint32, testThreads, testKeyLen, false},
|
||||
{"threads 为 0", validSalt, testTime, testMemory, 0, testKeyLen, false},
|
||||
{"keyLength 为 0", validSalt, testTime, testMemory, testThreads, 0, false},
|
||||
{"keyLength 低于下限", validSalt, testTime, testMemory, testThreads, 15, false},
|
||||
{"keyLength 超上限", validSalt, testTime, testMemory, testThreads, argon2x.MaxKeyLength + 1, false},
|
||||
{"keyLength 为 MaxUint32", validSalt, testTime, testMemory, testThreads, math.MaxUint32, false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
key, err := argon2x.DeriveKey(password, tc.salt, tc.time, tc.memory, tc.threads, tc.keyLen)
|
||||
if err == nil {
|
||||
t.Fatalf("未返回错误,得到 %d 字节密钥", len(key))
|
||||
}
|
||||
if key != nil {
|
||||
t.Errorf("出错时密钥应为 nil,实际长度 %d", len(key))
|
||||
}
|
||||
if _, err := argon2x.Verify(password, tc.salt, validSalt, tc.time, tc.memory, tc.threads, tc.keyLen); err == nil {
|
||||
t.Error("Verify 对非法参数未返回错误")
|
||||
}
|
||||
// DeriveKeyWithRandomSalt 与 HashPasswordWithParams 自行生成盐,
|
||||
// 因此仅在 time/memory/threads/keyLength 非法时才应报错。
|
||||
if !tc.saltInvalid {
|
||||
if _, _, err := argon2x.DeriveKeyWithRandomSalt(password, argon2x.DefaultSaltLength, tc.time, tc.memory, tc.threads, tc.keyLen); err == nil {
|
||||
t.Error("DeriveKeyWithRandomSalt 对非法参数未返回错误")
|
||||
}
|
||||
if _, err := argon2x.HashPasswordWithParams(password, tc.time, tc.memory, tc.threads, tc.keyLen); err == nil {
|
||||
t.Error("HashPasswordWithParams 对非法参数未返回错误")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashPasswordDefault 验证默认参数与 PHC 串格式。
|
||||
func TestHashPasswordDefault(t *testing.T) {
|
||||
encoded, err := argon2x.HashPassword([]byte("正确的马电池订书钉"))
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword 返回错误: %v", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(encoded, "$argon2id$v=19$m=65536,t=3,p=4$") {
|
||||
t.Errorf("PHC 串前缀不符合预期: %q", encoded)
|
||||
}
|
||||
if strings.Contains(encoded, "正确的马电池订书钉") {
|
||||
t.Error("PHC 串中不应出现口令明文")
|
||||
}
|
||||
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 6 {
|
||||
t.Fatalf("PHC 字段数 = %d, 期望 6", len(parts))
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
t.Fatalf("盐不是合法的无填充 base64: %v", err)
|
||||
}
|
||||
key, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
t.Fatalf("哈希不是合法的无填充 base64: %v", err)
|
||||
}
|
||||
if len(salt) != argon2x.DefaultSaltLength {
|
||||
t.Errorf("盐长度 = %d, 期望 %d", len(salt), argon2x.DefaultSaltLength)
|
||||
}
|
||||
if len(key) != argon2x.DefaultKeyLength {
|
||||
t.Errorf("哈希长度 = %d, 期望 %d", len(key), argon2x.DefaultKeyLength)
|
||||
}
|
||||
if strings.ContainsAny(parts[4]+parts[5], "=") {
|
||||
t.Error("盐与哈希必须使用无填充 base64")
|
||||
}
|
||||
|
||||
// 同一口令两次调用必须得到不同(随机盐)的 PHC 串,且都能校验通过。
|
||||
second, err := argon2x.HashPassword([]byte("正确的马电池订书钉"))
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword 返回错误: %v", err)
|
||||
}
|
||||
if second == encoded {
|
||||
t.Error("两次 HashPassword 结果相同,随机盐未生效")
|
||||
}
|
||||
for _, e := range []string{encoded, second} {
|
||||
ok, err := argon2x.VerifyPassword([]byte("正确的马电池订书钉"), e)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword 返回错误: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("正确口令未通过 PHC 校验")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashPasswordWithParams 验证自定义参数会写进 PHC 串并能往返校验。
|
||||
func TestHashPasswordWithParams(t *testing.T) {
|
||||
encoded, err := argon2x.HashPasswordWithParams([]byte("hunter2"), testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPasswordWithParams 返回错误: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(encoded, "$argon2id$v=19$m=8192,t=1,p=1$") {
|
||||
t.Errorf("PHC 参数段不符合预期: %q", encoded)
|
||||
}
|
||||
|
||||
ok, err := argon2x.VerifyPassword([]byte("hunter2"), encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyPassword 返回错误: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("正确口令未通过校验")
|
||||
}
|
||||
|
||||
// 错误口令返回 (false, nil)。
|
||||
ok, err = argon2x.VerifyPassword([]byte("hunter3"), encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("口令错误时不应返回 error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Error("错误口令被判为匹配")
|
||||
}
|
||||
|
||||
// 空口令与 nil 口令也必须能正确处理,不 panic。
|
||||
if _, err := argon2x.HashPasswordWithParams(nil, testTime, testMemory, testThreads, testKeyLen); err != nil {
|
||||
t.Errorf("空口令哈希失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyPasswordInvalidFormat 覆盖格式非法:必须返回 error(不能静默返回 false)。
|
||||
func TestVerifyPasswordInvalidFormat(t *testing.T) {
|
||||
valid, err := argon2x.HashPasswordWithParams([]byte("hunter2"), testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("准备测试数据失败: %v", err)
|
||||
}
|
||||
parts := strings.Split(valid, "$")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
encoded string
|
||||
}{
|
||||
{"空串", ""},
|
||||
{"完全垃圾", "not-a-phc-string"},
|
||||
{"缺少首字符 $", valid[1:]},
|
||||
{"缺少尾部哈希", strings.Join(parts[:5], "$")},
|
||||
{"多出一个字段", valid + "$extra"},
|
||||
{"算法标识错误", "$argon2i$v=19$m=8192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"算法标识缺失", "$$v=19$m=8192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"版本不支持", "$argon2id$v=18$m=8192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"版本字段缺少前缀", "$argon2id$19$m=8192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数段顺序错误", "$argon2id$v=19$t=1,m=8192,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数段缺少 p", "$argon2id$v=19$m=8192,t=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数非数字", "$argon2id$v=19$m=abc,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数为负数", "$argon2id$v=19$m=-8192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数带前导零", "$argon2id$v=19$m=08192,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"参数超上限", "$argon2id$v=19$m=4294967295,t=1,p=1$" + parts[4] + "$" + parts[5]},
|
||||
{"盐为空", "$argon2id$v=19$m=8192,t=1,p=1$$" + parts[5]},
|
||||
{"哈希为空", "$argon2id$v=19$m=8192,t=1,p=1$" + parts[4] + "$"},
|
||||
{"盐使用填充 base64", "$argon2id$v=19$m=8192,t=1,p=1$" + parts[4] + "=$" + parts[5]},
|
||||
{"盐含非法字符", "$argon2id$v=19$m=8192,t=1,p=1$" + parts[4] + "!$" + parts[5]},
|
||||
{"哈希含空白字符", "$argon2id$v=19$m=8192,t=1,p=1$" + parts[4] + "$\n" + parts[5]},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := argon2x.VerifyPassword([]byte("hunter2"), tc.encoded)
|
||||
if err == nil {
|
||||
t.Fatalf("格式非法时必须返回 error,实际 ok=%v", ok)
|
||||
}
|
||||
if ok {
|
||||
t.Error("格式非法时不应判定为匹配")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyPasswordTampered 覆盖语义被篡改的合法格式字符串:必须返回 error 或
|
||||
// false,绝不 panic,也绝不判定为匹配。
|
||||
func TestVerifyPasswordTampered(t *testing.T) {
|
||||
password := []byte("hunter2")
|
||||
valid, err := argon2x.HashPasswordWithParams(password, testTime, testMemory, testThreads, testKeyLen)
|
||||
if err != nil {
|
||||
t.Fatalf("准备测试数据失败: %v", err)
|
||||
}
|
||||
parts := strings.Split(valid, "$")
|
||||
salt, hash := parts[4], parts[5]
|
||||
|
||||
// flip 翻转 base64 字符串中的某个字符,保持长度不变。
|
||||
flip := func(s string, i int) string {
|
||||
b := []byte(s)
|
||||
if b[i] == 'A' {
|
||||
b[i] = 'B'
|
||||
} else {
|
||||
b[i] = 'A'
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
encoded string
|
||||
}{
|
||||
{"加大 m", "$argon2id$v=19$m=16384,t=1,p=1$" + salt + "$" + hash},
|
||||
{"减小 m", "$argon2id$v=19$m=4096,t=1,p=1$" + salt + "$" + hash},
|
||||
{"加大 t", "$argon2id$v=19$m=8192,t=2,p=1$" + salt + "$" + hash},
|
||||
{"加大 p", "$argon2id$v=19$m=8192,t=1,p=2$" + salt + "$" + hash},
|
||||
{"篡改盐", "$argon2id$v=19$m=8192,t=1,p=1$" + flip(salt, 0) + "$" + hash},
|
||||
{"篡改哈希", "$argon2id$v=19$m=8192,t=1,p=1$" + salt + "$" + flip(hash, 0)},
|
||||
{"截断哈希", "$argon2id$v=19$m=8192,t=1,p=1$" + salt + "$" + hash[:len(hash)-1]},
|
||||
{"截断整串", valid[:len(valid)-1]},
|
||||
{"截断到一半", valid[:len(valid)/2]},
|
||||
{"交换盐与哈希", "$argon2id$v=19$m=8192,t=1,p=1$" + hash + "$" + salt},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := argon2x.VerifyPassword(password, tc.encoded)
|
||||
if ok {
|
||||
t.Errorf("被篡改的 PHC 串仍然校验通过: %q", tc.encoded)
|
||||
}
|
||||
// err 允许为 nil(参数合法但口令哈希不匹配)或非 nil(参数非法)。
|
||||
_ = err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSalt(t *testing.T) {
|
||||
for _, length := range []int{argon2x.MinSaltLength, argon2x.DefaultSaltLength, 32} {
|
||||
salt, err := argon2x.GenerateSalt(length)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt(%d) 返回错误: %v", length, err)
|
||||
}
|
||||
if len(salt) != length {
|
||||
t.Errorf("盐长度 = %d, 期望 %d", len(salt), length)
|
||||
}
|
||||
}
|
||||
|
||||
a, err := argon2x.GenerateSalt(argon2x.DefaultSaltLength)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
||||
}
|
||||
b, err := argon2x.GenerateSalt(argon2x.DefaultSaltLength)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
||||
}
|
||||
if bytes.Equal(a, b) {
|
||||
t.Error("两次生成的盐相同")
|
||||
}
|
||||
|
||||
for _, length := range []int{0, -1, argon2x.MinSaltLength - 1, 1025, math.MaxInt} {
|
||||
if _, err := argon2x.GenerateSalt(length); err == nil {
|
||||
t.Errorf("GenerateSalt(%d) 未返回错误", length)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user