321 lines
11 KiB
Go
321 lines
11 KiB
Go
package scryptx_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"math"
|
|
"testing"
|
|
|
|
"code.yun.ink/pkg/encryptx/scryptx"
|
|
)
|
|
|
|
// 测试统一使用远小于默认值的内存参数,避免测试机耗时过长。
|
|
const (
|
|
testN = 1 << 10 // 1 MiB 工作内存
|
|
testR = 8
|
|
testP = 1
|
|
)
|
|
|
|
// TestDeriveKeyKnownVectors 使用 RFC 7914 第 12 节的官方 scrypt 向量,
|
|
// 以及 Python 3.13 hashlib.scrypt 独立生成的小参数向量。
|
|
func TestDeriveKeyKnownVectors(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
password []byte
|
|
salt []byte
|
|
n, r, p int
|
|
keyLength int
|
|
want string
|
|
}{
|
|
{
|
|
// RFC 7914 §12: scrypt(P="pleaseletmein", S="SodiumChloride",
|
|
// N=16384, r=8, p=1, dkLen=64)
|
|
name: "RFC7914-N16384-r8-p1",
|
|
password: []byte("pleaseletmein"),
|
|
salt: []byte("SodiumChloride"),
|
|
n: 16384,
|
|
r: 8,
|
|
p: 1,
|
|
keyLength: 64,
|
|
want: "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887",
|
|
},
|
|
{
|
|
// hashlib.scrypt(b'password', salt=b'saltsaltsalt', n=1024, r=8, p=1, dklen=32)
|
|
name: "python-n1024-r8-p1",
|
|
password: []byte("password"),
|
|
salt: []byte("saltsaltsalt"),
|
|
n: 1024,
|
|
r: 8,
|
|
p: 1,
|
|
keyLength: 32,
|
|
want: "3d33aa071ee47b3d50096e7e58fe5cf9918eba38c6a7218e961d0badce7a4deb",
|
|
},
|
|
{
|
|
// hashlib.scrypt(b'password', salt=b'saltsaltsalt', n=16384, r=8, p=1, dklen=32)
|
|
name: "python-n16384-r8-p1",
|
|
password: []byte("password"),
|
|
salt: []byte("saltsaltsalt"),
|
|
n: 16384,
|
|
r: 8,
|
|
p: 1,
|
|
keyLength: 32,
|
|
want: "316fe485a31e8d960549f59c5f9e0a33dec4cc15bd1bf55fe22b1fa484da2c1d",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := scryptx.DeriveKey(tc.password, tc.salt, tc.n, tc.r, tc.p, tc.keyLength)
|
|
if err != nil {
|
|
t.Fatalf("派生失败: %v", err)
|
|
}
|
|
if hex.EncodeToString(got) != tc.want {
|
|
t.Errorf("与已知向量不一致\n got: %s\nwant: %s", hex.EncodeToString(got), tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeriveKeyReproducible(t *testing.T) {
|
|
password := []byte("正确的马电池订书钉")
|
|
salt := []byte("0123456789abcdef")
|
|
|
|
k1, err := scryptx.DeriveKey(password, salt, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("派生失败: %v", err)
|
|
}
|
|
k2, err := scryptx.DeriveKey(password, salt, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("派生失败: %v", err)
|
|
}
|
|
if !bytes.Equal(k1, k2) {
|
|
t.Fatal("相同输入两次派生结果不一致")
|
|
}
|
|
|
|
other, err := scryptx.DeriveKey(password, []byte("fedcba9876543210"), testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("换盐派生失败: %v", err)
|
|
}
|
|
if bytes.Equal(k1, other) {
|
|
t.Error("不同盐得到了相同密钥")
|
|
}
|
|
|
|
otherN, err := scryptx.DeriveKey(password, salt, testN*2, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("换 n 派生失败: %v", err)
|
|
}
|
|
if bytes.Equal(k1, otherN) {
|
|
t.Error("不同 n 得到了相同密钥")
|
|
}
|
|
|
|
otherP, err := scryptx.DeriveKey(password, salt, testN, testR, testP+1, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("换 p 派生失败: %v", err)
|
|
}
|
|
if bytes.Equal(k1, otherP) {
|
|
t.Error("不同 p 得到了相同密钥")
|
|
}
|
|
}
|
|
|
|
// TestDeriveKeyWithRandomSalt 断言返回的盐确实能复现同一个密钥。
|
|
func TestDeriveKeyWithRandomSalt(t *testing.T) {
|
|
password := []byte("hunter2")
|
|
|
|
key, salt, err := scryptx.DeriveKeyWithRandomSalt(password, scryptx.DefaultSaltLength, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("DeriveKeyWithRandomSalt 返回错误: %v", err)
|
|
}
|
|
if len(salt) != scryptx.DefaultSaltLength {
|
|
t.Fatalf("返回的盐长度 = %d, 期望 %d", len(salt), scryptx.DefaultSaltLength)
|
|
}
|
|
if len(key) != scryptx.DefaultKeyLength {
|
|
t.Fatalf("返回的密钥长度 = %d, 期望 %d", len(key), scryptx.DefaultKeyLength)
|
|
}
|
|
|
|
reproduced, err := scryptx.DeriveKey(password, salt, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("用返回的盐复现失败: %v", err)
|
|
}
|
|
if !bytes.Equal(key, reproduced) {
|
|
t.Fatal("用 DeriveKeyWithRandomSalt 返回的盐无法复现同一密钥")
|
|
}
|
|
|
|
key2, salt2, err := scryptx.DeriveKeyWithRandomSalt(password, scryptx.DefaultSaltLength, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
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, scryptx.MinSaltLength - 1, 1025, math.MaxInt} {
|
|
if _, _, err := scryptx.DeriveKeyWithRandomSalt(password, bad, testN, testR, testP, scryptx.DefaultKeyLength); err == nil {
|
|
t.Errorf("saltLength=%d 时未返回错误", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerify(t *testing.T) {
|
|
password := []byte("s3cret-口令")
|
|
salt := []byte("0123456789abcdef")
|
|
key, err := scryptx.DeriveKey(password, salt, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("派生失败: %v", err)
|
|
}
|
|
|
|
ok, err := scryptx.Verify(password, salt, key, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
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 := scryptx.Verify(tc.password, tc.salt, tc.expected, testN, testR, testP, scryptx.DefaultKeyLength)
|
|
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}, scryptx.MinSaltLength)
|
|
password := []byte("pw")
|
|
|
|
cases := []struct {
|
|
name string
|
|
salt []byte
|
|
n, r, p int
|
|
keyLength int
|
|
saltInvalid bool
|
|
}{
|
|
{"盐为 nil", nil, testN, testR, testP, 32, true},
|
|
{"盐为空切片", []byte{}, testN, testR, testP, 32, true},
|
|
{"盐过短", []byte("1234567"), testN, testR, testP, 32, true},
|
|
{"盐过长", bytes.Repeat([]byte{0x01}, 1025), testN, testR, testP, 32, true},
|
|
{"n 为 0", validSalt, 0, testR, testP, 32, false},
|
|
{"n 为 1", validSalt, 1, testR, testP, 32, false},
|
|
{"n 为负", validSalt, -1024, testR, testP, 32, false},
|
|
{"n 为 3(非 2 的幂)", validSalt, 3, testR, testP, 32, false},
|
|
{"n 为 1000(非 2 的幂)", validSalt, 1000, testR, testP, 32, false},
|
|
{"n 超上限", validSalt, scryptx.MaxN * 2, testR, testP, 32, false},
|
|
{"n 为 MaxInt", validSalt, math.MaxInt, testR, testP, 32, false},
|
|
{"r 为 0", validSalt, testN, 0, testP, 32, false},
|
|
{"r 为负", validSalt, testN, -8, testP, 32, false},
|
|
{"p 为 0", validSalt, testN, testR, 0, 32, false},
|
|
{"p 为负", validSalt, testN, testR, -1, 32, false},
|
|
{"密钥长度为 0", validSalt, testN, testR, testP, 0, false},
|
|
{"密钥长度为负", validSalt, testN, testR, testP, -32, false},
|
|
{"密钥长度超上限", validSalt, testN, testR, testP, scryptx.MaxKeyLength + 1, false},
|
|
{"内存代价超上限", validSalt, 1 << 20, 64, 1, 32, false},
|
|
{"p 极大导致内存超上限", validSalt, 2, 1, 1 << 29, 32, false},
|
|
{"参数溢出", validSalt, 1 << 20, math.MaxInt, 1, 32, false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
key, err := scryptx.DeriveKey(password, tc.salt, tc.n, tc.r, tc.p, tc.keyLength)
|
|
if err == nil {
|
|
t.Fatalf("未返回错误,得到 %d 字节密钥", len(key))
|
|
}
|
|
if key != nil {
|
|
t.Errorf("出错时密钥应为 nil,实际长度 %d", len(key))
|
|
}
|
|
if _, err := scryptx.Verify(password, tc.salt, validSalt, tc.n, tc.r, tc.p, tc.keyLength); err == nil {
|
|
t.Error("Verify 对非法参数未返回错误")
|
|
}
|
|
// DeriveKeyWithRandomSalt 自行生成盐,因此仅在 n/r/p/长度非法时才应报错。
|
|
if !tc.saltInvalid {
|
|
if _, _, err := scryptx.DeriveKeyWithRandomSalt(password, scryptx.DefaultSaltLength, tc.n, tc.r, tc.p, tc.keyLength); err == nil {
|
|
t.Error("DeriveKeyWithRandomSalt 对非法参数未返回错误")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBoundaryValues 验证边界参数落在允许一侧时可用,且默认参数可正常完成。
|
|
func TestBoundaryValues(t *testing.T) {
|
|
salt := bytes.Repeat([]byte{0x5A}, scryptx.MinSaltLength)
|
|
|
|
// n=2 是最小的合法值(1<<1),r=p=1。
|
|
key, err := scryptx.DeriveKey([]byte("pw"), salt, 2, 1, 1, 1)
|
|
if err != nil {
|
|
t.Fatalf("最小合法参数派生失败: %v", err)
|
|
}
|
|
if len(key) != 1 {
|
|
t.Errorf("密钥长度 = %d, 期望 1", len(key))
|
|
}
|
|
|
|
// 较大的 n 在内存上限内应被接受:n=1<<18、r=1 约 32 MiB,耗时可接受。
|
|
key, err = scryptx.DeriveKey([]byte("pw"), salt, 1<<18, 1, 1, scryptx.MaxKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("n=1<<18、r=1 应被接受: %v", err)
|
|
}
|
|
if len(key) != scryptx.MaxKeyLength {
|
|
t.Errorf("密钥长度 = %d, 期望 %d", len(key), scryptx.MaxKeyLength)
|
|
}
|
|
|
|
// 默认参数必须可用(32 MiB,约 0.1s)。
|
|
key, err = scryptx.DeriveKey([]byte("pw"), salt, scryptx.DefaultN, scryptx.DefaultR, scryptx.DefaultP, scryptx.DefaultKeyLength)
|
|
if err != nil {
|
|
t.Fatalf("默认参数派生失败: %v", err)
|
|
}
|
|
if len(key) != scryptx.DefaultKeyLength {
|
|
t.Errorf("默认参数派生长度 = %d, 期望 %d", len(key), scryptx.DefaultKeyLength)
|
|
}
|
|
}
|
|
|
|
func TestGenerateSalt(t *testing.T) {
|
|
for _, length := range []int{scryptx.MinSaltLength, scryptx.DefaultSaltLength, 32} {
|
|
salt, err := scryptx.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 := scryptx.GenerateSalt(scryptx.DefaultSaltLength)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
|
}
|
|
b, err := scryptx.GenerateSalt(scryptx.DefaultSaltLength)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSalt 返回错误: %v", err)
|
|
}
|
|
if bytes.Equal(a, b) {
|
|
t.Error("两次生成的盐相同")
|
|
}
|
|
|
|
for _, length := range []int{0, -1, scryptx.MinSaltLength - 1, 1025, math.MaxInt} {
|
|
if _, err := scryptx.GenerateSalt(length); err == nil {
|
|
t.Errorf("GenerateSalt(%d) 未返回错误", length)
|
|
}
|
|
}
|
|
}
|