mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat: integrate passwap for human user password hashing (#6196)
* feat: use passwap for human user passwords * fix tests * passwap config * add the event mapper * cleanup query side and api * solve linting errors * regression test * try to fix linter errors again * pass systemdefaults into externalConfigChange migration * fix: user password set in auth view * pin passwap v0.2.0 * v2: validate hashed password hash based on prefix * resolve remaining comments * add error tag and translation for unsupported hash encoding * fix unit test --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
const (
|
||||
TypeEncryption CryptoType = iota
|
||||
TypeHash
|
||||
TypeHash // Depcrecated: use [passwap.Swapper] instead
|
||||
)
|
||||
|
||||
type Crypto interface {
|
||||
@@ -26,6 +26,7 @@ type EncryptionAlgorithm interface {
|
||||
DecryptString(hashed []byte, keyID string) (string, error)
|
||||
}
|
||||
|
||||
// Depcrecated: use [passwap.Swapper] instead
|
||||
type HashAlgorithm interface {
|
||||
Crypto
|
||||
Hash(value []byte) ([]byte, error)
|
||||
|
209
internal/crypto/passwap.go
Normal file
209
internal/crypto/passwap.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/zitadel/passwap"
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
"github.com/zitadel/passwap/verifier"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
type PasswordHasher struct {
|
||||
*passwap.Swapper
|
||||
Prefixes []string
|
||||
}
|
||||
|
||||
func (h *PasswordHasher) EncodingSupported(encodedHash string) bool {
|
||||
for _, prefix := range h.Prefixes {
|
||||
if strings.HasPrefix(encodedHash, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type HashName string
|
||||
|
||||
const (
|
||||
HashNameArgon2 HashName = "argon2" // used for the common argon2 verifier
|
||||
HashNameArgon2i HashName = "argon2i" // hash only
|
||||
HashNameArgon2id HashName = "argon2id" // hash only
|
||||
HashNameBcrypt HashName = "bcrypt" // hash and verify
|
||||
HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNameScrypt HashName = "scrypt" // hash and verify
|
||||
)
|
||||
|
||||
type PasswordHashConfig struct {
|
||||
Verifiers []HashName
|
||||
Hasher HasherConfig
|
||||
}
|
||||
|
||||
func (c *PasswordHashConfig) PasswordHasher() (*PasswordHasher, error) {
|
||||
verifiers, vPrefixes, err := c.buildVerifiers()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(err, "CRYPT-sahW9", "password hash config invalid")
|
||||
}
|
||||
hasher, hPrefixes, err := c.Hasher.buildHasher()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(err, "CRYPT-Que4r", "password hash config invalid")
|
||||
}
|
||||
return &PasswordHasher{
|
||||
Swapper: passwap.NewSwapper(hasher, verifiers...),
|
||||
Prefixes: append(hPrefixes, vPrefixes...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type prefixVerifier struct {
|
||||
prefixes []string
|
||||
verifier verifier.Verifier
|
||||
}
|
||||
|
||||
// map HashNames to Verifier instances.
|
||||
var knowVerifiers = map[HashName]prefixVerifier{
|
||||
HashNameArgon2: {
|
||||
// only argon2i and argon2id are suppored.
|
||||
// The Prefix constant also covers argon2d.
|
||||
prefixes: []string{argon2.Prefix},
|
||||
verifier: argon2.Verifier,
|
||||
},
|
||||
HashNameBcrypt: {
|
||||
prefixes: []string{bcrypt.Prefix},
|
||||
verifier: bcrypt.Verifier,
|
||||
},
|
||||
HashNameMd5: {
|
||||
prefixes: []string{md5.Prefix},
|
||||
verifier: md5.Verifier,
|
||||
},
|
||||
HashNameScrypt: {
|
||||
prefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
verifier: scrypt.Verifier,
|
||||
},
|
||||
}
|
||||
|
||||
func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
|
||||
verifiers = make([]verifier.Verifier, len(c.Verifiers))
|
||||
prefixes = make([]string, 0, len(c.Verifiers)+1)
|
||||
for i, name := range c.Verifiers {
|
||||
v, ok := knowVerifiers[name]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("invalid verifier %q", name)
|
||||
}
|
||||
verifiers[i] = v.verifier
|
||||
prefixes = append(prefixes, v.prefixes...)
|
||||
}
|
||||
return verifiers, prefixes, nil
|
||||
}
|
||||
|
||||
type HasherConfig struct {
|
||||
Algorithm HashName
|
||||
Params map[string]any `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string, err error) {
|
||||
switch c.Algorithm {
|
||||
case HashNameArgon2i:
|
||||
return c.argon2i()
|
||||
case HashNameArgon2id:
|
||||
return c.argon2id()
|
||||
case HashNameBcrypt:
|
||||
return c.bcrypt()
|
||||
case HashNameScrypt:
|
||||
return c.scrypt()
|
||||
case "":
|
||||
return nil, nil, fmt.Errorf("missing hasher algorithm")
|
||||
case HashNameArgon2, HashNameMd5:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HasherConfig) decodeParams(dst any) error {
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
ErrorUnused: true,
|
||||
ErrorUnset: true,
|
||||
Result: dst,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(c.Params)
|
||||
}
|
||||
|
||||
// argon2Params decodes [HasherConfig.Params] into a [argon2.Params] used as defaults.
|
||||
// p is passed a copy and therfore will not be modified.
|
||||
func (c *HasherConfig) argon2Params(p argon2.Params) (argon2.Params, error) {
|
||||
var dst struct {
|
||||
Time uint32 `mapstructure:"Time"`
|
||||
Memory uint32 `mapstructure:"Memory"`
|
||||
Threads uint8 `mapstructure:"Threads"`
|
||||
}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return argon2.Params{}, fmt.Errorf("decode argon2i params: %w", err)
|
||||
}
|
||||
p.Time = dst.Time
|
||||
p.Memory = dst.Memory
|
||||
p.Threads = dst.Threads
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) argon2i() (passwap.Hasher, []string, error) {
|
||||
p, err := c.argon2Params(argon2.RecommendedIParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return argon2.NewArgon2i(p), []string{argon2.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) argon2id() (passwap.Hasher, []string, error) {
|
||||
p, err := c.argon2Params(argon2.RecommendedIDParams)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return argon2.NewArgon2id(p), []string{argon2.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) bcryptCost() (int, error) {
|
||||
var dst = struct {
|
||||
Cost int `mapstructure:"Cost"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return 0, fmt.Errorf("decode bcrypt params: %w", err)
|
||||
}
|
||||
return dst.Cost, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) bcrypt() (passwap.Hasher, []string, error) {
|
||||
cost, err := c.bcryptCost()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return bcrypt.New(cost), []string{bcrypt.Prefix}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) scryptParams() (scrypt.Params, error) {
|
||||
var dst = struct {
|
||||
Cost int `mapstructure:"Cost"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return scrypt.Params{}, fmt.Errorf("decode scrypt params: %w", err)
|
||||
}
|
||||
p := scrypt.RecommendedParams // copy
|
||||
p.N = 1 << dst.Cost
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) scrypt() (passwap.Hasher, []string, error) {
|
||||
p, err := c.scryptParams()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return scrypt.New(p), []string{scrypt.Prefix, scrypt.Prefix_Linux}, nil
|
||||
}
|
486
internal/crypto/passwap_test.go
Normal file
486
internal/crypto/passwap_test.go
Normal file
@@ -0,0 +1,486 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
)
|
||||
|
||||
func TestPasswordHasher_EncodingSupported(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encodedHash string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty string, false",
|
||||
encodedHash: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "scrypt, false",
|
||||
encodedHash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "bcrypt, true",
|
||||
encodedHash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "argo2i, true",
|
||||
encodedHash: "$argon2i$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$YMvo8AUoNtnKYGqeODruCjHdiEbl1pKL2MsYy9VgU/E",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "argo2id, true",
|
||||
encodedHash: "$argon2d$v=19$m=4096,t=3,p=1$cmFuZG9tc2FsdGlzaGFyZA$CB0Du96aj3fQVcVSqb0LIA6Z6fpStjzjVkaC3RlpK9A",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &PasswordHasher{
|
||||
Prefixes: []string{bcrypt.Prefix, argon2.Prefix},
|
||||
}
|
||||
got := h.EncodingSupported(tt.encodedHash)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
type fields struct {
|
||||
Verifiers []HashName
|
||||
Hasher HasherConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantPrefixes []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "invalid verifier",
|
||||
fields: fields{
|
||||
Verifiers: []HashName{
|
||||
HashNameArgon2,
|
||||
HashNameBcrypt,
|
||||
HashNameMd5,
|
||||
HashNameScrypt,
|
||||
"foobar",
|
||||
},
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid hasher",
|
||||
fields: fields{
|
||||
Verifiers: []HashName{
|
||||
HashNameArgon2,
|
||||
HashNameBcrypt,
|
||||
HashNameMd5,
|
||||
HashNameScrypt,
|
||||
},
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: "foobar",
|
||||
Params: map[string]any{
|
||||
"cost": 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing algorithm",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid md5",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameMd5,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid argon2",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2i, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2i,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2i, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2i,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "argon2id, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2id,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "argon2id, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameArgon2id,
|
||||
Params: map[string]any{
|
||||
"time": 3,
|
||||
"memory": 32768,
|
||||
"threads": 4,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameBcrypt, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{argon2.Prefix, bcrypt.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "bcrypt, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"foo": 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bcrypt, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameBcrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 3,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameMd5, HashNameScrypt},
|
||||
},
|
||||
wantPrefixes: []string{bcrypt.Prefix, argon2.Prefix, md5.Prefix, scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
},
|
||||
{
|
||||
name: "scrypt, error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameScrypt,
|
||||
Params: map[string]any{
|
||||
"cost": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "scrypt, ok",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameScrypt,
|
||||
Params: map[string]any{
|
||||
"cost": 3,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &PasswordHashConfig{
|
||||
Verifiers: tt.fields.Verifiers,
|
||||
Hasher: tt.fields.Hasher,
|
||||
}
|
||||
got, err := c.PasswordHasher()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if tt.wantPrefixes != nil {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.wantPrefixes, got.Prefixes)
|
||||
encoded, err := got.Hash("password")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_decodeParams(t *testing.T) {
|
||||
type dst struct {
|
||||
A int
|
||||
B uint32
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
params map[string]any
|
||||
want dst
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "unused",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unset",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": "2",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
params: map[string]any{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
},
|
||||
want: dst{
|
||||
A: 1,
|
||||
B: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.params,
|
||||
}
|
||||
var got dst
|
||||
err := c.decodeParams(&got)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_argon2Params(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
type args struct {
|
||||
p argon2.Params
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want argon2.Params
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
p: argon2.RecommendedIDParams,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"time": 2,
|
||||
"memory": 256,
|
||||
"threads": 8,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
p: argon2.RecommendedIDParams,
|
||||
},
|
||||
want: argon2.Params{
|
||||
Time: 2,
|
||||
Memory: 256,
|
||||
Threads: 8,
|
||||
KeyLen: 32,
|
||||
SaltLen: 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.argon2Params(tt.args.p)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_bcryptCost(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"cost": 12,
|
||||
},
|
||||
},
|
||||
want: 12,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.bcryptCost()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_scryptParams(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want scrypt.Params
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"cost": 2,
|
||||
},
|
||||
},
|
||||
want: scrypt.Params{
|
||||
N: 4,
|
||||
R: 8,
|
||||
P: 1,
|
||||
KeyLen: 32,
|
||||
SaltLen: 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got, err := c.scryptParams()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user