diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 23d0ad8030..442a5ab324 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -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: diff --git a/docs/docs/concepts/architecture/secrets.md b/docs/docs/concepts/architecture/secrets.md index f8f195114b..1f36802754 100644 --- a/docs/docs/concepts/architecture/secrets.md +++ b/docs/docs/concepts/architecture/secrets.md @@ -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 diff --git a/internal/crypto/passwap.go b/internal/crypto/passwap.go index e14c2dfaaf..e4f3313342 100644 --- a/internal/crypto/passwap.go +++ b/internal/crypto/passwap.go @@ -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 } } diff --git a/internal/crypto/passwap_test.go b/internal/crypto/passwap_test.go index b872b0e298..dfcafd1406 100644 --- a/internal/crypto/passwap_test.go +++ b/internal/crypto/passwap_test.go @@ -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) + }) + } +}