Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for verifying argon2i and argon2id passwords #1597

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
113 changes: 113 additions & 0 deletions internal/crypto/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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("^[$](?P<alg>argon2(d|i|id))[$]v=(?P<v>(16|19))[$]m=(?P<m>[0-9]+),t=(?P<t>[0-9]+),p=(?P<p>[0-9]+)(,keyid=(?P<keyid>[^,]+))?(,data=(?P<data>[^$]+))?[$](?P<salt>[^$]+)[$](?P<hash>.+)$")

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
Expand Down
21 changes: 21 additions & 0 deletions internal/crypto/password_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
}
Loading