From f30c1730004530fe88dcbf7c43c099468459e0c6 Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Thu, 30 May 2024 10:55:48 +0200 Subject: [PATCH] feat: add support for verifying argon2i and argon2id passwords --- go.mod | 6 +- go.sum | 6 ++ internal/crypto/password.go | 113 +++++++++++++++++++++++++++++++ internal/crypto/password_test.go | 21 ++++++ 4 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 internal/crypto/password_test.go diff --git a/go.mod b/go.mod index 2feeaebfb..925307980 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.23.0 golang.org/x/oauth2 v0.7.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) @@ -135,8 +135,8 @@ require ( golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index 2e5cb69e0..79981598f 100644 --- a/go.sum +++ b/go.sum @@ -573,6 +573,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -729,6 +731,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -749,6 +753,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/crypto/password.go b/internal/crypto/password.go index d494eaab4..6341e4d2d 100644 --- a/internal/crypto/password.go +++ b/internal/crypto/password.go @@ -2,12 +2,18 @@ package crypto import ( "context" + "crypto/subtle" + "encoding/base64" "errors" "fmt" + "regexp" + "strconv" + "strings" "github.com/supabase/auth/internal/observability" "go.opentelemetry.io/otel/attribute" + "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" ) @@ -42,10 +48,117 @@ var ( compareHashAndPasswordCompletedCounter = observability.ObtainMetricCounter("gotrue_compare_hash_and_password_completed", "Number of completed CompareHashAndPassword hashing attempts") ) +var ErrArgon2MismatchedHashAndPassword = errors.New("crypto: argon2 hash and password mismatch") + +// argon2HashRegexp https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding +var argon2HashRegexp = regexp.MustCompile("^[$](?Pargon2(d|i|id))[$]v=(?P(16|19))[$]m=(?P[0-9]+),t=(?P[0-9]+),p=(?P

[0-9]+)(,keyid=(?P[^,]+))?(,data=(?P[^$]+))?[$](?P[^$]+)[$](?P.+)$") + +func compareHashAndPasswordArgon2(ctx context.Context, hash, password string) error { + submatch := argon2HashRegexp.FindStringSubmatchIndex(hash) + + if submatch == nil { + return errors.New("crypto: incorrect argon2 hash format") + } + + alg := string(argon2HashRegexp.ExpandString(nil, "$alg", hash, submatch)) + v := string(argon2HashRegexp.ExpandString(nil, "$v", hash, submatch)) + m := string(argon2HashRegexp.ExpandString(nil, "$m", hash, submatch)) + t := string(argon2HashRegexp.ExpandString(nil, "$t", hash, submatch)) + p := string(argon2HashRegexp.ExpandString(nil, "$p", hash, submatch)) + keyid := string(argon2HashRegexp.ExpandString(nil, "$keyid", hash, submatch)) + data := string(argon2HashRegexp.ExpandString(nil, "$data", hash, submatch)) + saltB64 := string(argon2HashRegexp.ExpandString(nil, "$salt", hash, submatch)) + hashB64 := string(argon2HashRegexp.ExpandString(nil, "$hash", hash, submatch)) + + if alg != "argon2i" && alg != "argon2id" { + return fmt.Errorf("crypto: argon2 hash uses unsupported algorithm %q only argon2i and argon2id supported", alg) + } + + if v != "19" { + return fmt.Errorf("crypto: argon2 hash uses unsupported version %q only %d is supported", v, argon2.Version) + } + + if data != "" { + return fmt.Errorf("crypto: argon2 hashes with the data parameter not supported") + } + + if keyid != "" { + return fmt.Errorf("crypto: argon2 hashes with the keyid parameter not supported") + } + + memory, err := strconv.ParseUint(m, 10, 32) + if err != nil { + return fmt.Errorf("crypto: argon2 hash has invalid m parameter %q %w", m, err) + } + + time, err := strconv.ParseUint(t, 10, 32) + if err != nil { + return fmt.Errorf("crypto: argon2 hash has invalid t parameter %q %w", t, err) + } + + threads, err := strconv.ParseUint(p, 10, 8) + if err != nil { + return fmt.Errorf("crypto: argon2 hash has invalid p parameter %q %w", p, err) + } + + rawHash, err := base64.RawStdEncoding.DecodeString(hashB64) + if err != nil { + return fmt.Errorf("crypto: argon2 hash has invalid base64 in the hash section %w", err) + } + + salt, err := base64.RawStdEncoding.DecodeString(saltB64) + if err != nil { + return fmt.Errorf("crypto: argon2 hash has invalid base64 in the salt section %w", err) + } + + var match bool + var derivedKey []byte + + attributes := []attribute.KeyValue{ + attribute.String("alg", alg), + attribute.String("v", v), + attribute.Int64("m", int64(memory)), + attribute.Int64("t", int64(time)), + attribute.Int("p", int(threads)), + attribute.Int("len", len(rawHash)), + } + + compareHashAndPasswordSubmittedCounter.Add(ctx, 1, attributes...) + defer func() { + attributes = append(attributes, attribute.Bool( + "match", + match, + )) + + compareHashAndPasswordCompletedCounter.Add(ctx, 1, attributes...) + }() + + switch alg { + case "argon2i": + derivedKey = argon2.Key([]byte(password), salt, uint32(time), uint32(memory)*1024, uint8(threads), uint32(len(rawHash))) + + case "argon2id": + derivedKey = argon2.IDKey([]byte(password), salt, uint32(time), uint32(memory)*1024, uint8(threads), uint32(len(rawHash))) + } + + match = subtle.ConstantTimeCompare(derivedKey, rawHash) == 0 + + if !match { + return ErrArgon2MismatchedHashAndPassword + } + + return nil +} + // CompareHashAndPassword compares the hash and // password, returns nil if equal otherwise an error. Context can be used to // cancel the hashing if the algorithm supports it. func CompareHashAndPassword(ctx context.Context, hash, password string) error { + if strings.HasPrefix(hash, "$argon2") { + return compareHashAndPasswordArgon2(ctx, hash, password) + } + + // assume bcrypt hashCost, err := bcrypt.Cost([]byte(hash)) if err != nil { return err diff --git a/internal/crypto/password_test.go b/internal/crypto/password_test.go new file mode 100644 index 000000000..c3091975b --- /dev/null +++ b/internal/crypto/password_test.go @@ -0,0 +1,21 @@ +package crypto + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArgon2(t *testing.T) { + // all of these hash the `test` string with various parameters + + examples := []string{ + "$argon2i$v=19$m=16,t=2,p=1$bGJRWThNOHJJTVBSdHl2dQ$NfEnUOuUpb7F2fQkgFUG4g", + "$argon2id$v=19$m=32,t=3,p=2$SFVpOWJ0eXhjRzVkdGN1RQ$RXnb8rh7LaDcn07xsssqqulZYXOM/EUCEFMVcAcyYVk", + } + + for _, example := range examples { + assert.NoError(t, CompareHashAndPassword(context.Background(), example, "test")) + } +}