完善不同加密方法的使用
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// Package scryptx 基于 scrypt(RFC 7914)提供内存硬的口令密钥派生与校验。
|
||||
//
|
||||
// scrypt 通过同时消耗 CPU 与内存来提高离线暴力破解成本,适合口令存储。
|
||||
//
|
||||
// 安全注意事项:
|
||||
// - 选型顺序:口令哈希首选 argon2x(Argon2id);scrypt 是同等可用的备选,
|
||||
// 二者都远优于纯 CPU 代价的 pbkdf2x。
|
||||
// - 内存开销约为 128*r*(n+p) 字节,且该内存必须真实分配才具备抗 ASIC 能力。
|
||||
// 默认 DefaultN=1<<15、DefaultR=8、DefaultP=1 时约 32 MiB/次,适合交互式
|
||||
// 登录;服务器内存充裕时应上调到 OWASP 建议的 N=1<<17、r=8、p=1(约 128 MiB)。
|
||||
// - N 必须是大于 1 的 2 的幂,且受 MaxN 限制;n/r/p 组合折算出的内存也不得超过
|
||||
// 上限,防止调用方误传超大参数造成 OOM(进程被杀死,无法恢复)。
|
||||
// - 盐必须是密码学安全随机数、每个口令独立,长度不低于 MinSaltLength 字节。
|
||||
// - 派生密钥与口令都是敏感数据,调用方负责及时清零,且不得写入日志或错误文本。
|
||||
//
|
||||
// 本包任何情况下都不会 panic:所有非法参数都返回 error。
|
||||
package scryptx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultN 是默认的 CPU/内存代价参数,取 1<<15(32768)。调大 N 会同时
|
||||
// 提高 CPU 时间与内存占用,是抵御 GPU/ASIC 的主要旋钮。
|
||||
DefaultN = 1 << 15
|
||||
|
||||
// DefaultR 是默认的块大小参数,8 对应 1024 字节的块;r 越大,内存占用
|
||||
// 与内存访问延迟越高。
|
||||
DefaultR = 8
|
||||
|
||||
// DefaultP 是默认的并行化参数,1 表示单线程完成一次派生。
|
||||
DefaultP = 1
|
||||
|
||||
// MaxN 是允许的最大 N(1<<20)。N=1<<20、r=8 时单次派生需要约 1 GiB 内存,
|
||||
// 该上限用于拦截调用方传入超大 N 导致的 OOM(进程被杀死,无法恢复)。
|
||||
MaxN = 1 << 20
|
||||
|
||||
// DefaultSaltLength 是推荐的盐长度(字节),16 字节足以避免彩虹表碰撞。
|
||||
DefaultSaltLength = 16
|
||||
|
||||
// MinSaltLength 是允许的最小盐长度(字节),更短的盐会显著提高碰撞概率。
|
||||
MinSaltLength = 8
|
||||
|
||||
// DefaultKeyLength 是推荐的派生密钥长度(字节),32 字节可直接作为
|
||||
// AES-256 或 HMAC-SHA256 的密钥。
|
||||
DefaultKeyLength = 32
|
||||
|
||||
// MaxKeyLength 是允许的最大派生密钥长度(字节),用于拦截把缓冲区大小
|
||||
// 误当输出长度传入的调用。
|
||||
MaxKeyLength = 1024
|
||||
)
|
||||
|
||||
const (
|
||||
// maxSaltLength 是允许的最大盐长度(字节)。
|
||||
maxSaltLength = 1024
|
||||
|
||||
// maxMemoryBytes 是单次派生允许申请的 scrypt 工作内存上限。默认参数
|
||||
// (n=1<<15、r=8、p=1)约 32 MiB;把 n 调到 1<<20 且 r=8 时约 1 GiB,仍在
|
||||
// 允许范围内,但明显异常的组合(例如 n=1<<20、r=64,约 8 GiB)会被拒绝,
|
||||
// 而不是让进程因 OOM 被杀死。
|
||||
maxMemoryBytes = 2 << 30
|
||||
)
|
||||
|
||||
// GenerateSalt 返回 length 字节的密码学安全随机盐,供 DeriveKey 使用。
|
||||
//
|
||||
// length 必须落在 [MinSaltLength, 1024] 内,推荐 DefaultSaltLength。每个口令
|
||||
// 必须使用独立的新盐,并与派生结果一起保存;盐是公开值,缺失它无法复现密钥。
|
||||
// 随机源不可用时返回 error,绝不退化为可预测的盐。
|
||||
func GenerateSalt(length int) ([]byte, error) {
|
||||
if length < MinSaltLength {
|
||||
return nil, fmt.Errorf("scryptx: 盐长度 %d 小于最小值 %d", length, MinSaltLength)
|
||||
}
|
||||
if length > maxSaltLength {
|
||||
return nil, fmt.Errorf("scryptx: 盐长度 %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("scryptx: 读取随机盐失败: %w", err)
|
||||
}
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// DeriveKey 使用 scrypt 从 password 与 salt 派生 keyLength 字节密钥。
|
||||
//
|
||||
// n 必须是大于 1 的 2 的幂且不超过 MaxN;r、p 必须大于 0;salt 长度必须在
|
||||
// [MinSaltLength, 1024];keyLength 必须在 [1, MaxKeyLength]。任一条件不满足
|
||||
// 都返回 error,绝不 panic。
|
||||
//
|
||||
// 单次派生的峰值内存约为 128*r*(n+p+2) 字节,耗时随 n、r、p 线性增长。
|
||||
// 默认参数(DefaultN/DefaultR/DefaultP)约需 32 MiB 内存,适合交互式场景。
|
||||
func DeriveKey(password, salt []byte, n, r, p, keyLength int) ([]byte, error) {
|
||||
if err := validateParams(salt, n, r, p, keyLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := scrypt.Key(password, salt, n, r, p, keyLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scryptx: 派生密钥失败: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveKeyWithRandomSalt 生成 saltLength 字节随机盐并立即派生密钥,一次性返回
|
||||
// (key, salt, error)。这是推荐入口:调用方必须把 salt 与 key 一起保存,否则
|
||||
// 派生结果无法复现。
|
||||
//
|
||||
// saltLength 通常传 DefaultSaltLength;n、r、p、keyLength 的含义同 DeriveKey。
|
||||
// 参数非法或随机源失败时返回 error,此时 key 与 salt 均为 nil。
|
||||
func DeriveKeyWithRandomSalt(password []byte, saltLength, n, r, p, keyLength int) (key, salt []byte, err error) {
|
||||
salt, err = GenerateSalt(saltLength)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
key, err = DeriveKey(password, salt, n, r, p, 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, n, r, p, keyLength int) (bool, error) {
|
||||
derived, err := DeriveKey(password, salt, n, r, p, 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
|
||||
}
|
||||
|
||||
// validateParams 是所有派生入口共用的参数闸门,保证非法输入在进入 scrypt 之前
|
||||
// 就变成明确的 error,而不是 panic、OOM 或静默截断。
|
||||
func validateParams(salt []byte, n, r, p, keyLength int) error {
|
||||
if len(salt) == 0 {
|
||||
return fmt.Errorf("scryptx: 盐不能为空")
|
||||
}
|
||||
if len(salt) < MinSaltLength {
|
||||
return fmt.Errorf("scryptx: 盐长度 %d 小于最小值 %d", len(salt), MinSaltLength)
|
||||
}
|
||||
if len(salt) > maxSaltLength {
|
||||
return fmt.Errorf("scryptx: 盐长度 %d 超过最大值 %d", len(salt), maxSaltLength)
|
||||
}
|
||||
if n <= 1 {
|
||||
return fmt.Errorf("scryptx: 代价参数 n=%d 必须大于 1", n)
|
||||
}
|
||||
if n&(n-1) != 0 {
|
||||
return fmt.Errorf("scryptx: 代价参数 n=%d 必须是 2 的幂", n)
|
||||
}
|
||||
if n > MaxN {
|
||||
return fmt.Errorf("scryptx: 代价参数 n=%d 超过上限 %d", n, MaxN)
|
||||
}
|
||||
if r <= 0 {
|
||||
return fmt.Errorf("scryptx: 块大小参数 r=%d 必须大于 0", r)
|
||||
}
|
||||
if p <= 0 {
|
||||
return fmt.Errorf("scryptx: 并行度参数 p=%d 必须大于 0", p)
|
||||
}
|
||||
if keyLength <= 0 {
|
||||
return fmt.Errorf("scryptx: 密钥长度 %d 必须大于 0", keyLength)
|
||||
}
|
||||
if keyLength > MaxKeyLength {
|
||||
return fmt.Errorf("scryptx: 密钥长度 %d 超过最大值 %d", keyLength, MaxKeyLength)
|
||||
}
|
||||
// scrypt 的峰值内存约为 128*r*(n+p+2) 字节:V 数组占 128*n*r,p 个并行单元
|
||||
// 共占 128*r*p,另有 256*r 的临时区。这里统一按 (n+p) 折算,且全程只做除法,
|
||||
// 避免 r 或 p 取极大值时乘法回绕绕过检查(例如 r=1、p=2^29 时 scrypt 允许
|
||||
// 但实际需要约 64 GiB 内存,会直接把进程打死)。
|
||||
units := uint64(n) + uint64(p) // n <= 2^20 且 p <= MaxInt,相加不会溢出 uint64
|
||||
if uint64(r) > maxMemoryBytes/128/units {
|
||||
return fmt.Errorf("scryptx: 参数组合 n=%d, r=%d, p=%d 需要的内存超过上限 %d 字节", n, r, p, maxMemoryBytes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user