mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 19:27:46 +00:00
feat(crypto): add pbkdf2 support (#6303)
This change brings pbkdf2 support for password hashing and verification.
This commit is contained in:
parent
dd480f8a8d
commit
4d09409328
@ -383,7 +383,12 @@ SystemDefaults:
|
||||
# Hasher:
|
||||
# Algorithm: "scrypt"
|
||||
# Cost: 15
|
||||
|
||||
|
||||
# Hasher:
|
||||
# Algorithm: "pbkdf2"
|
||||
# Rounds: 290000
|
||||
# Hash: "sha256" # Can be "sha1", "sha224", "sha256", "sha384" or "sha512"
|
||||
|
||||
# Verifiers enable the possibility of verifying
|
||||
# passwords that are previously hashed using another
|
||||
# algorithm then the Hasher.
|
||||
@ -402,6 +407,7 @@ SystemDefaults:
|
||||
# - "bcrypt"
|
||||
# - "md5"
|
||||
# - "scrypt"
|
||||
# - "pbkdf2" # verifier for all pbkdf2 hash modes.
|
||||
Multifactors:
|
||||
OTP:
|
||||
# If this is empty, the issuer is the requested domain
|
||||
|
2
go.mod
2
go.mod
@ -60,7 +60,7 @@ require (
|
||||
github.com/ttacon/libphonenumber v1.2.1
|
||||
github.com/zitadel/logging v0.3.4
|
||||
github.com/zitadel/oidc/v2 v2.7.0
|
||||
github.com/zitadel/passwap v0.2.0
|
||||
github.com/zitadel/passwap v0.3.0
|
||||
github.com/zitadel/saml v0.0.11
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
|
||||
|
4
go.sum
4
go.sum
@ -898,8 +898,8 @@ github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM
|
||||
github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
|
||||
github.com/zitadel/oidc/v2 v2.7.0 h1:IGX4EDk6tegTjUSsZDWeTfLseFU0BdJ/Glf1tgys2lU=
|
||||
github.com/zitadel/oidc/v2 v2.7.0/go.mod h1:zkUkVJS0sDVy9m0UA9RgO3f8i/C0rtjvXU36UJj7T+0=
|
||||
github.com/zitadel/passwap v0.2.0 h1:rkYrax9hfRIpVdXJ7pS8JHkQOhuQTdZQxEhsY0dFFrU=
|
||||
github.com/zitadel/passwap v0.2.0/go.mod h1:KRTL4LL8ugJIn2xLoQYZf5t4kDyr7w41uq3XqvUlO6w=
|
||||
github.com/zitadel/passwap v0.3.0 h1:kC/vzN9xQlEQjUAZs0z2P5nKrZs9AuTqprteSQ2S4Ag=
|
||||
github.com/zitadel/passwap v0.3.0/go.mod h1:sIpG6HfmnP28qwxu8kf+ot53ERbLwU9fOITstAwZSms=
|
||||
github.com/zitadel/saml v0.0.11 h1:kObucnBrcu1PHCO7RGT0iVeuJL/5I50gUgr40S41nMs=
|
||||
github.com/zitadel/saml v0.0.11/go.mod h1:YGWAvPZRv4DbEZ78Ht/2P0AWzGn+6WGhFf90PMXl0Po=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/pbkdf2"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
"github.com/zitadel/passwap/verifier"
|
||||
|
||||
@ -38,6 +39,19 @@ const (
|
||||
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
|
||||
HashNamePBKDF2 HashName = "pbkdf2" // hash and verify
|
||||
)
|
||||
|
||||
type HashMode string
|
||||
|
||||
// HashMode defines a underlying [hash.Hash] implementation
|
||||
// for algorithms like pbkdf2
|
||||
const (
|
||||
HashModeSHA1 HashMode = "sha1"
|
||||
HashModeSHA224 HashMode = "sha224"
|
||||
HashModeSHA256 HashMode = "sha256"
|
||||
HashModeSHA384 HashMode = "sha384"
|
||||
HashModeSHA512 HashMode = "sha512"
|
||||
)
|
||||
|
||||
type PasswordHashConfig struct {
|
||||
@ -85,6 +99,10 @@ var knowVerifiers = map[HashName]prefixVerifier{
|
||||
prefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux},
|
||||
verifier: scrypt.Verifier,
|
||||
},
|
||||
HashNamePBKDF2: {
|
||||
prefixes: []string{pbkdf2.Prefix},
|
||||
verifier: pbkdf2.Verifier,
|
||||
},
|
||||
}
|
||||
|
||||
func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
|
||||
@ -116,6 +134,8 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string,
|
||||
return c.bcrypt()
|
||||
case HashNameScrypt:
|
||||
return c.scrypt()
|
||||
case HashNamePBKDF2:
|
||||
return c.pbkdf2()
|
||||
case "":
|
||||
return nil, nil, fmt.Errorf("missing hasher algorithm")
|
||||
case HashNameArgon2, HashNameMd5:
|
||||
@ -207,3 +227,49 @@ func (c *HasherConfig) scrypt() (passwap.Hasher, []string, error) {
|
||||
}
|
||||
return scrypt.New(p), []string{scrypt.Prefix, scrypt.Prefix_Linux}, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) pbkdf2Params() (p pbkdf2.Params, _ HashMode, _ error) {
|
||||
var dst = struct {
|
||||
Rounds uint32 `mapstructure:"Rounds"`
|
||||
Hash HashMode `mapstructure:"Hash"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return p, "", fmt.Errorf("decode pbkdf2 params: %w", err)
|
||||
}
|
||||
switch dst.Hash {
|
||||
case HashModeSHA1:
|
||||
p = pbkdf2.RecommendedSHA1Params
|
||||
case HashModeSHA224:
|
||||
p = pbkdf2.RecommendedSHA224Params
|
||||
case HashModeSHA256:
|
||||
p = pbkdf2.RecommendedSHA256Params
|
||||
case HashModeSHA384:
|
||||
p = pbkdf2.RecommendedSHA384Params
|
||||
case HashModeSHA512:
|
||||
p = pbkdf2.RecommendedSHA512Params
|
||||
}
|
||||
p.Rounds = dst.Rounds
|
||||
return p, dst.Hash, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) pbkdf2() (passwap.Hasher, []string, error) {
|
||||
p, hash, err := c.pbkdf2Params()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
prefix := []string{pbkdf2.Prefix}
|
||||
switch hash {
|
||||
case HashModeSHA1:
|
||||
return pbkdf2.NewSHA1(p), prefix, nil
|
||||
case HashModeSHA224:
|
||||
return pbkdf2.NewSHA224(p), prefix, nil
|
||||
case HashModeSHA256:
|
||||
return pbkdf2.NewSHA256(p), prefix, nil
|
||||
case HashModeSHA384:
|
||||
return pbkdf2.NewSHA384(p), prefix, nil
|
||||
case HashModeSHA512:
|
||||
return pbkdf2.NewSHA512(p), prefix, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsuppored pbkdf2 hash mode: %s", hash)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -8,6 +11,7 @@ import (
|
||||
"github.com/zitadel/passwap/argon2"
|
||||
"github.com/zitadel/passwap/bcrypt"
|
||||
"github.com/zitadel/passwap/md5"
|
||||
"github.com/zitadel/passwap/pbkdf2"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
)
|
||||
|
||||
@ -238,6 +242,101 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
},
|
||||
wantPrefixes: []string{scrypt.Prefix, scrypt.Prefix_Linux, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, parse error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"cost": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, hash mode error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, sha1",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA1,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, sha224",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA224,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, sha256",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA256,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, sha384",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA384,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, sha512",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePBKDF2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA512,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5},
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -484,3 +583,116 @@ func TestHasherConfig_scryptParams(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_pbkdf2Params(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantP pbkdf2.Params
|
||||
wantHash HashMode
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "decode error",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sha1",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha1",
|
||||
},
|
||||
},
|
||||
wantP: pbkdf2.Params{
|
||||
Rounds: 12,
|
||||
KeyLen: sha1.Size,
|
||||
SaltLen: 16,
|
||||
},
|
||||
wantHash: HashModeSHA1,
|
||||
},
|
||||
{
|
||||
name: "sha224",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha224",
|
||||
},
|
||||
},
|
||||
wantP: pbkdf2.Params{
|
||||
Rounds: 12,
|
||||
KeyLen: sha256.Size224,
|
||||
SaltLen: 16,
|
||||
},
|
||||
wantHash: HashModeSHA224,
|
||||
},
|
||||
{
|
||||
name: "sha256",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha256",
|
||||
},
|
||||
},
|
||||
wantP: pbkdf2.Params{
|
||||
Rounds: 12,
|
||||
KeyLen: sha256.Size,
|
||||
SaltLen: 16,
|
||||
},
|
||||
wantHash: HashModeSHA256,
|
||||
},
|
||||
{
|
||||
name: "sha384",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha384",
|
||||
},
|
||||
},
|
||||
wantP: pbkdf2.Params{
|
||||
Rounds: 12,
|
||||
KeyLen: sha512.Size384,
|
||||
SaltLen: 16,
|
||||
},
|
||||
wantHash: HashModeSHA384,
|
||||
},
|
||||
{
|
||||
name: "sha512",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha512",
|
||||
},
|
||||
},
|
||||
wantP: pbkdf2.Params{
|
||||
Rounds: 12,
|
||||
KeyLen: sha512.Size,
|
||||
SaltLen: 16,
|
||||
},
|
||||
wantHash: HashModeSHA512,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
gotP, gotHash, err := c.pbkdf2Params()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantP, gotP)
|
||||
assert.Equal(t, tt.wantHash, gotHash)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user