Files
encryptx/argon2x/argon2x_test.go
2026-09-30 15:49:12 +08:00

469 lines
17 KiB
Go

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)
}
}
}