diff --git a/go.mod b/go.mod index 7405b71..e4c4f56 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/labstack/echo/v4 v4.11.1 github.com/labstack/gommon v0.4.0 github.com/lib/pq v1.10.9 + github.com/matthewhartstonge/argon2 v0.3.3 github.com/microcosm-cc/bluemonday v1.0.25 github.com/philandstuff/dhall-golang/v6 v6.0.2 github.com/xiaoqidun/entps v0.0.0-20230712154733-ff3582a22e82 diff --git a/go.sum b/go.sum index 90d8cff..abc1519 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406 h1:+OUpk+IVvmKU github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matthewhartstonge/argon2 v0.3.3 h1:38/hupgfzqO2UGxqXqmSqErE8KJvQnIxWWg7IXUqWgQ= +github.com/matthewhartstonge/argon2 v0.3.3/go.mod h1:W2fhVs3+4FGxqDiap9SxxwNF/0SOVYcITpqDZe8RrhY= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= diff --git a/modules/password/argon.go b/modules/password/argon.go new file mode 100644 index 0000000..f7d12ef --- /dev/null +++ b/modules/password/argon.go @@ -0,0 +1,78 @@ +// Copyright 2023 wanderer +// SPDX-License-Identifier: AGPL-3.0-only + +package password + +import ( + "time" + + "github.com/matthewhartstonge/argon2" + "golang.org/x/exp/rand" +) + +var ( + // argon provides the package with current config. the one currently set on + // init is the memory-hard argon2.RecommendedConfig(). + argon argon2.Config + // r stores a seeded *rand.Rand so that the generated salts are more close + // to non-deterministic. + r = rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) +) + +// Argon accepts a password string and returns hash and salt byte slices, or an +// error. +func Argon(password string) (*argon2.Raw, error) { + salt := make([]byte, argon.SaltLength) + + err := genSalt(salt) + if err != nil { + return nil, err + } + + hash, err := argon.Hash([]byte(password), salt) + if err != nil { + return nil, err + } + + return &hash, nil +} + +// ArgonDecode attempts to decode the given byte slice into an argon raw +// struct. +func ArgonDecode(digest []byte) (*argon2.Raw, error) { + hash, err := argon2.Decode(digest) + if err != nil { + return nil, err + } + + return &hash, nil +} + +// ArgonVerify checks that the provided password matches the one used to create +// the provided digest, returns a bool and an error. +func ArgonVerify(password string, digest []byte) (bool, error) { + return argon2.VerifyEncoded([]byte(password), digest) +} + +// genSalt fills slice b with len(b) random bytes. +func genSalt(b []byte) error { + _, err := r.Read(b) + if err != nil { + return err + } + + return nil +} + +func init() { + // TODO: hide this behind a config flag someday. + noop := false + + if noop { + // 64MiB peak memory usage. + argon = argon2.DefaultConfig() + } else { + // 2GiB peak memory usage. + argon = argon2.RecommendedDefaults() + } +} diff --git a/modules/password/argon_test.go b/modules/password/argon_test.go new file mode 100644 index 0000000..7ab840c --- /dev/null +++ b/modules/password/argon_test.go @@ -0,0 +1,76 @@ +// Copyright 2023 wanderer +// SPDX-License-Identifier: AGPL-3.0-only + +package password + +import ( + "testing" + + "github.com/matthewhartstonge/argon2" +) + +func TestArgon(t *testing.T) { + // set lighter defaults for the tests. + argon = argon2.MemoryConstrainedDefaults() + + passwd := "password0123#@:" + + raw, err := Argon(passwd) + if err != nil { + t.Error(err) + } + + // save the salt generated when hashing the password using our own func, + // it'll be reused when reproducing manual hash creation. + salt := raw.Salt + + manualRaw, err := argon.Hash([]byte(passwd), salt) + if err != nil { + t.Errorf("failed to create a hash: %q", err) + } + + want := raw.Encode() + got := manualRaw.Encode() + + t.Logf("want: %q, got: %q", want, got) + + if len(want) != len(got) { + t.Errorf("password hashes differ, want: %q, got: %q", want, got) + } + + for i := range got { + if got[i] != want[i] { + t.Logf("password hashes differ, want: %q, got: %q", want, got) + break + } + } + + if _, err := ArgonVerify(passwd, want); err != nil { + t.Errorf("passwords don't equal: %q", err) + } else { + t.Log("passwords equal") + } + + if _, err := ArgonVerify(passwd, got); err != nil { + t.Errorf("passwords don't equal for the manually created hash: %q", err) + } else { + t.Log("passwords equal for the manually created hash") + } + + dw, err := ArgonDecode(want) + if err != nil { + t.Errorf("could not decode the digest: %q", err) + } + + dg, err := ArgonDecode(got) + if err != nil { + t.Errorf("could not decode the digest: %q", err) + } + + for i := range dw.Hash { + if dw.Hash[i] != dg.Hash[i] { + t.Errorf("hash values don't equal: want: %q, got: %q", dw.Hash, dg.Hash) + break + } + } +}