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