mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 15:49:35 +00:00
feat(crypto): support for SHA2 and PHPass password hashes (#9809)
# Which Problems Are Solved
- Allow users to use SHA-256 and SHA-512 hashing algorithms. These
algorithms are used by Linux's crypt(3) function.
- Allow users to import passwords using the PHPass algorithm. This
algorithm is used by older PHP systems, WordPress in particular.
# How the Problems Are Solved
- Upgrade passwap to
[v0.9.0](https://github.com/zitadel/passwap/releases/tag/v0.9.0)
- Add sha2 and phpass as a new verifier option in defaults.yaml
# Additional Changes
- Updated docs to explain the two algorithms
# Additional Context
Implements the changes in the passwap library from
https://github.com/zitadel/passwap/pull/59 and
https://github.com/zitadel/passwap/pull/60
(cherry picked from commit 38013d0e84
)
This commit is contained in:

committed by
Livio Spring

parent
b19fd84760
commit
603799f409
@@ -643,7 +643,7 @@ SystemDefaults:
|
||||
# or cost are automatically re-hashed using this config,
|
||||
# upon password validation or update.
|
||||
Hasher:
|
||||
# Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2"
|
||||
# Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2", "sha2"
|
||||
# Depending on the algorithm, different configuration options take effect.
|
||||
Algorithm: bcrypt # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
|
||||
# Cost takes effect for the algorithms bcrypt and scrypt
|
||||
@@ -654,10 +654,11 @@ SystemDefaults:
|
||||
Memory: 32768 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_MEMORY
|
||||
# Threads takes effect for the algorithms argon2i and argon2id
|
||||
Threads: 4 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_THREADS
|
||||
# Rounds takes effect for the algorithm pbkdf2
|
||||
# Rounds takes effect for the algorithm pbkdf2 and sha2
|
||||
Rounds: 290000 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ROUNDS
|
||||
# Hash takes effect for the algorithm pbkdf2
|
||||
# Can be "sha1", "sha224", "sha256", "sha384" or "sha512"
|
||||
# Hash takes effect for the algorithm pbkdf2 and sha2
|
||||
# Can be "sha1", "sha224", "sha256", "sha384" or "sha512" for pbkdf2
|
||||
# Can be "sha256" or "sha512" for sha2
|
||||
Hash: sha256 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_HASH
|
||||
|
||||
# Verifiers enable the possibility of verifying
|
||||
@@ -679,6 +680,8 @@ SystemDefaults:
|
||||
# - "md5" # md5Crypt with salt and password shuffling.
|
||||
# - "md5plain" # md5 digest of a password without salt
|
||||
# - "md5salted" # md5 digest of a salted password
|
||||
# - "phpass"
|
||||
# - "sha2" # crypt(3) SHA-256 and SHA-512
|
||||
# - "scrypt"
|
||||
# - "pbkdf2" # verifier for all pbkdf2 hash modes.
|
||||
SecretHasher:
|
||||
|
@@ -71,6 +71,8 @@ The following hash algorithms are supported:
|
||||
- md5: implementation of md5Crypt with salt and password shuffling [^2]
|
||||
- md5plain: md5 digest of a password without salt [^2]
|
||||
- md5salted: md5 digest of a salted password [^2]
|
||||
- phpass: md5 digest with PHPass algorithm (used in WordPress) [^2]
|
||||
- sha2: implementation of crypt(3) SHA-256 & SHA-512
|
||||
- scrypt
|
||||
- pbkdf2
|
||||
|
||||
|
@@ -14,7 +14,9 @@ import (
|
||||
"github.com/zitadel/passwap/md5plain"
|
||||
"github.com/zitadel/passwap/md5salted"
|
||||
"github.com/zitadel/passwap/pbkdf2"
|
||||
"github.com/zitadel/passwap/phpass"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
"github.com/zitadel/passwap/sha2"
|
||||
"github.com/zitadel/passwap/verifier"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
@@ -51,6 +53,8 @@ const (
|
||||
HashNameMd5 HashName = "md5" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNameMd5Plain HashName = "md5plain" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNameMd5Salted HashName = "md5salted" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNamePHPass HashName = "phpass" // verify only, as hashing with md5 is insecure and deprecated
|
||||
HashNameSha2 HashName = "sha2" // hash and verify
|
||||
HashNameScrypt HashName = "scrypt" // hash and verify
|
||||
HashNamePBKDF2 HashName = "pbkdf2" // hash and verify
|
||||
)
|
||||
@@ -125,6 +129,14 @@ var knowVerifiers = map[HashName]prefixVerifier{
|
||||
prefixes: []string{md5salted.Prefix},
|
||||
verifier: md5salted.Verifier,
|
||||
},
|
||||
HashNameSha2: {
|
||||
prefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier},
|
||||
verifier: sha2.Verifier,
|
||||
},
|
||||
HashNamePHPass: {
|
||||
prefixes: []string{phpass.IdentifierP, phpass.IdentifierH},
|
||||
verifier: phpass.Verifier,
|
||||
},
|
||||
}
|
||||
|
||||
func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
|
||||
@@ -158,9 +170,11 @@ func (c *HasherConfig) buildHasher() (hasher passwap.Hasher, prefixes []string,
|
||||
return c.scrypt()
|
||||
case HashNamePBKDF2:
|
||||
return c.pbkdf2()
|
||||
case HashNameSha2:
|
||||
return c.sha2()
|
||||
case "":
|
||||
return nil, nil, fmt.Errorf("missing hasher algorithm")
|
||||
case HashNameArgon2, HashNameMd5:
|
||||
case HashNameArgon2, HashNameMd5, HashNameMd5Plain, HashNameMd5Salted, HashNamePHPass:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid algorithm %q", c.Algorithm)
|
||||
@@ -296,6 +310,44 @@ func (c *HasherConfig) pbkdf2() (passwap.Hasher, []string, error) {
|
||||
case HashModeSHA512:
|
||||
return pbkdf2.NewSHA512(p), prefix, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsuppored pbkdf2 hash mode: %s", hash)
|
||||
return nil, nil, fmt.Errorf("unsupported pbkdf2 hash mode: %s", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HasherConfig) sha2Params() (use512 bool, rounds int, err error) {
|
||||
var dst = struct {
|
||||
Rounds uint32 `mapstructure:"Rounds"`
|
||||
Hash HashMode `mapstructure:"Hash"`
|
||||
}{}
|
||||
if err := c.decodeParams(&dst); err != nil {
|
||||
return false, 0, fmt.Errorf("decode sha2 params: %w", err)
|
||||
}
|
||||
switch dst.Hash {
|
||||
case HashModeSHA256:
|
||||
use512 = false
|
||||
case HashModeSHA512:
|
||||
use512 = true
|
||||
case HashModeSHA1, HashModeSHA224, HashModeSHA384:
|
||||
fallthrough
|
||||
default:
|
||||
return false, 0, fmt.Errorf("cannot use %s with sha2", dst.Hash)
|
||||
}
|
||||
if dst.Rounds > sha2.RoundsMax {
|
||||
return false, 0, fmt.Errorf("rounds with sha2 cannot be larger than %d", sha2.RoundsMax)
|
||||
} else {
|
||||
rounds = int(dst.Rounds)
|
||||
}
|
||||
return use512, rounds, nil
|
||||
}
|
||||
|
||||
func (c *HasherConfig) sha2() (passwap.Hasher, []string, error) {
|
||||
use512, rounds, err := c.sha2Params()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if use512 {
|
||||
return sha2.New512(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil
|
||||
} else {
|
||||
return sha2.New256(rounds), []string{sha2.Sha256Identifier, sha2.Sha512Identifier}, nil
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/zitadel/passwap/md5salted"
|
||||
"github.com/zitadel/passwap/pbkdf2"
|
||||
"github.com/zitadel/passwap/scrypt"
|
||||
"github.com/zitadel/passwap/sha2"
|
||||
)
|
||||
|
||||
func TestPasswordHasher_EncodingSupported(t *testing.T) {
|
||||
@@ -78,7 +79,9 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
HashNameBcrypt,
|
||||
HashNameMd5,
|
||||
HashNameMd5Salted,
|
||||
HashNamePHPass,
|
||||
HashNameScrypt,
|
||||
HashNameSha2,
|
||||
"foobar",
|
||||
},
|
||||
Hasher: HasherConfig{
|
||||
@@ -142,6 +145,15 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid phpass",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNamePHPass,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid argon2",
|
||||
fields: fields{
|
||||
@@ -357,6 +369,59 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
|
||||
},
|
||||
wantPrefixes: []string{pbkdf2.Prefix, argon2.Prefix, bcrypt.Prefix, md5.Prefix, md5salted.Prefix},
|
||||
},
|
||||
{
|
||||
name: "sha2, parse error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameSha2,
|
||||
Params: map[string]any{
|
||||
"cost": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "pbkdf2, hash mode error",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameSha2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sha2, sha256",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameSha2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA256,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain},
|
||||
},
|
||||
wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
{
|
||||
name: "sha2, sha512",
|
||||
fields: fields{
|
||||
Hasher: HasherConfig{
|
||||
Algorithm: HashNameSha2,
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": HashModeSHA512,
|
||||
},
|
||||
},
|
||||
Verifiers: []HashName{HashNameArgon2, HashNameBcrypt, HashNameMd5, HashNameMd5Plain},
|
||||
},
|
||||
wantPrefixes: []string{sha2.Sha256Identifier, sha2.Sha512Identifier, argon2.Prefix, bcrypt.Prefix, md5.Prefix},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -723,3 +788,93 @@ func TestHasherConfig_pbkdf2Params(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasherConfig_sha2Params(t *testing.T) {
|
||||
type fields struct {
|
||||
Params map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want512 bool
|
||||
wantRounds int
|
||||
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",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sha224",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha224",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sha256",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 5000,
|
||||
"Hash": "sha256",
|
||||
},
|
||||
},
|
||||
want512: false,
|
||||
wantRounds: 5000,
|
||||
},
|
||||
{
|
||||
name: "sha384",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 12,
|
||||
"Hash": "sha384",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sha512",
|
||||
fields: fields{
|
||||
Params: map[string]any{
|
||||
"Rounds": 15000,
|
||||
"Hash": "sha512",
|
||||
},
|
||||
},
|
||||
want512: true,
|
||||
wantRounds: 15000,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &HasherConfig{
|
||||
Params: tt.fields.Params,
|
||||
}
|
||||
got512, gotRounds, err := c.sha2Params()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want512, got512)
|
||||
assert.Equal(t, tt.wantRounds, gotRounds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user