diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 40b2fc8d07..3573242d59 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -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 diff --git a/go.mod b/go.mod index 18077ccc09..bd62f2bd35 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index e69bad2e38..2edc2dd911 100644 --- a/go.sum +++ b/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= diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index cf72a844fb..a5a293a449 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -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) + } +} diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index 2cc5aa80e7..b557ca4a5c 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -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) + }) + } +}