From 13c9b9378f5988bb3418fb59f9726a7b36d2193d Mon Sep 17 00:00:00 2001 From: shuofan Date: Tue, 28 Sep 2021 13:48:23 +0800 Subject: [PATCH] Add a custom hasher for old users migration --- .docker/Dockerfile-build | 2 +- driver/config/.schema/config.schema.json | 2 +- driver/registry_default.go | 5 +++- hash/hash_comparator.go | 24 +++++++++++++++ hash/hasher_custom.go | 38 ++++++++++++++++++++++++ hash/hasher_test.go | 23 ++++++++++++++ 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 hash/hasher_custom.go diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build index 3d163d4a9c0e..8054d6df624e 100644 --- a/.docker/Dockerfile-build +++ b/.docker/Dockerfile-build @@ -1,4 +1,4 @@ -FROM golang:1.16-alpine AS builder +FROM golang:1.17-alpine AS builder RUN apk -U --no-cache add build-base git gcc bash diff --git a/driver/config/.schema/config.schema.json b/driver/config/.schema/config.schema.json index 6b5ffc5c9097..4fb74637467b 100644 --- a/driver/config/.schema/config.schema.json +++ b/driver/config/.schema/config.schema.json @@ -1570,7 +1570,7 @@ "description": "One of the values: argon2, bcrypt", "type": "string", "default": "bcrypt", - "enum": ["argon2", "bcrypt"] + "enum": ["argon2", "bcrypt", "custom"] }, "argon2": { "title": "Configuration for the Argon2id hasher.", diff --git a/driver/registry_default.go b/driver/registry_default.go index 6b773527fa83..99775935463b 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -361,8 +361,11 @@ func (m *RegistryDefault) SessionHandler() *session.Handler { func (m *RegistryDefault) Hasher() hash.Hasher { if m.passwordHasher == nil { - if m.c.HasherPasswordHashingAlgorithm() == "bcrypt" { + hashingAlgorithm := m.c.HasherPasswordHashingAlgorithm() + if hashingAlgorithm == "bcrypt" { m.passwordHasher = hash.NewHasherBcrypt(m) + } else if hashingAlgorithm == "custom" { + m.passwordHasher = hash.NewHasherCustom(m) } else { m.passwordHasher = hash.NewHasherArgon2(m) } diff --git a/hash/hash_comparator.go b/hash/hash_comparator.go index 5c1ea51913a1..275b1e017791 100644 --- a/hash/hash_comparator.go +++ b/hash/hash_comparator.go @@ -2,8 +2,10 @@ package hash import ( "context" + "crypto/sha512" "crypto/subtle" "encoding/base64" + "encoding/hex" "fmt" "regexp" "strings" @@ -22,6 +24,8 @@ func Compare(ctx context.Context, password []byte, hash []byte) error { return CompareBcrypt(ctx, password, hash) } else if IsArgon2idHash(hash) { return CompareArgon2id(ctx, password, hash) + } else if IsCustomHash(hash) { + return CompareCustom(ctx, password, hash) } else { return ErrUnknownHashAlgorithm } @@ -60,6 +64,21 @@ func CompareArgon2id(_ context.Context, password []byte, hash []byte) error { return ErrMismatchedHashAndPassword } +func CompareCustom(_ context.Context, password []byte, hash []byte) error { + parts := strings.Split(string(hash), "@") + if len(parts) < 2 { + return ErrInvalidHash + } + salt, realPass := parts[0], parts[1] + sha := sha512.New() + sha.Write([]byte(string(password) + salt)) + encoded := hex.EncodeToString(sha.Sum(nil))[:20] + if subtle.ConstantTimeCompare([]byte(encoded), []byte(realPass)) == 1 { + return nil + } + return ErrMismatchedHashAndPassword +} + func IsBcryptHash(hash []byte) bool { res, _ := regexp.Match("^\\$2[abzy]?\\$", hash) return res @@ -70,6 +89,11 @@ func IsArgon2idHash(hash []byte) bool { return res } +func IsCustomHash(hash []byte) bool { + res, _ := regexp.Match("^.{4}@.{20}$", hash) + return res +} + func decodeArgon2idHash(encodedHash string) (p *config.Argon2, salt, hash []byte, err error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { diff --git a/hash/hasher_custom.go b/hash/hasher_custom.go new file mode 100644 index 000000000000..247ac8813717 --- /dev/null +++ b/hash/hasher_custom.go @@ -0,0 +1,38 @@ +package hash + +import ( + "context" + "crypto/md5" + "crypto/sha512" + "encoding/hex" + "io" + "strconv" + "strings" + "time" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/x" +) + +type Custom struct { + c CustomConfiguration +} + +type CustomConfiguration interface { + config.Provider +} + +func NewHasherCustom(c CustomConfiguration) *Custom { + return &Custom{c: c} +} + +func (h *Custom) Generate(ctx context.Context, password []byte) ([]byte, error) { + hash := md5.New() + io.WriteString(hash, x.NewUUID().String()+strconv.FormatInt(time.Now().UnixMilli(), 10)) + salt := hex.EncodeToString(hash.Sum(nil))[:4] + + sha := sha512.New() + sha.Write([]byte(string(password) + salt)) + encodedPass := hex.EncodeToString(sha.Sum(nil))[:20] + return []byte(strings.Join([]string{salt, encodedPass}, "@")), nil +} diff --git a/hash/hasher_test.go b/hash/hasher_test.go index 17848b607e0c..6c57929471a6 100644 --- a/hash/hasher_test.go +++ b/hash/hasher_test.go @@ -148,4 +148,27 @@ func TestCompare(t *testing.T) { assert.Nil(t, hash.Compare(context.Background(), []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4"))) assert.Nil(t, hash.CompareArgon2id(context.Background(), []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp4"))) assert.Error(t, hash.Compare(context.Background(), []byte("test"), []byte("$argon2id$v=19$m=32,t=5,p=4$cm94YnRVOW5jZzFzcVE4bQ$fBxypOL0nP/zdPE71JtAV71i487LbX3fJI5PoTN6Lp5"))) + + assert.Nil(t, hash.CompareCustom(context.Background(), []byte("test"), []byte("f1d2@5cb452466722347b4e52"))) + assert.Nil(t, hash.CompareCustom(context.Background(), []byte("test"), []byte("f451@b441460d45f3bd05e7ac"))) + assert.Error(t, hash.CompareCustom(context.Background(), []byte("test"), []byte("f4c6@ed3c22ffd294a6a98d67"))) +} + +func TestComparatorCustomFail(t *testing.T) { + for k, pw := range [][]byte{ + mkpw(t, 8), + mkpw(t, 16), + mkpw(t, 32), + mkpw(t, 64), + mkpw(t, 72), + } { + t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { + mod := make([]byte, len(pw)) + copy(mod, pw) + mod[len(pw)-1] = ^pw[len(pw)-1] + + err := hash.CompareCustom(context.Background(), pw, mod) + assert.Error(t, err) + }) + } }