From 5935e8127da6328739dd5364bcd8c0f47b49a6c6 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Sun, 8 Jan 2023 09:08:36 -0800 Subject: [PATCH] Hash passwords --- go.mod | 2 +- internal/data/hash.go | 19 ++ internal/data/users.go | 16 +- internal/data/users_test.go | 12 +- vendor/golang.org/x/crypto/bcrypt/base64.go | 35 +++ vendor/golang.org/x/crypto/bcrypt/bcrypt.go | 295 ++++++++++++++++++++ vendor/modules.txt | 1 + 7 files changed, 368 insertions(+), 12 deletions(-) create mode 100644 internal/data/hash.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/base64.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/bcrypt.go diff --git a/go.mod b/go.mod index 539a471..ac6b8b7 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/muesli/termenv v0.12.0 github.com/rs/zerolog v1.27.0 github.com/spf13/viper v1.12.0 + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b inet.af/netaddr v0.0.0-20220811202034-502d2d690317 ) @@ -50,7 +51,6 @@ require ( github.com/subosito/gotenv v1.3.0 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect - golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 // indirect golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/internal/data/hash.go b/internal/data/hash.go new file mode 100644 index 0000000..7b3081f --- /dev/null +++ b/internal/data/hash.go @@ -0,0 +1,19 @@ +package data + +import ( + "golang.org/x/crypto/bcrypt" + _ "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/internal/data/users.go b/internal/data/users.go index 4aca92c..ecbf0cc 100644 --- a/internal/data/users.go +++ b/internal/data/users.go @@ -23,6 +23,17 @@ type UserPass struct { Password string `json:"password"` } +func NewUserPass(username, password string) *UserPass { + hash, err := HashPassword(password) + if err != nil { + panic(err) + } + return &UserPass{ + Username: username, + Password: hash, + } +} + func (u *UserPass) Name() string { return "password" } @@ -37,9 +48,7 @@ func (u *UserPass) Authenticate() error { for _, method := range user.authMethods { if method.Name() == "password" { userPass := method.(*UserPass) - if userPass.Password == u.Password { - return nil - } + CheckPasswordHash(u.Password, userPass.Password) } } return ErrAccessDenied @@ -61,6 +70,7 @@ func (p PubKey) Authenticate() error { FakeCycle() return ErrAccessDenied } + spew.Dump(u) for _, method := range u.authMethods { if method.Name() == "publickey" { pubKey := method.(*PubKey) diff --git a/internal/data/users_test.go b/internal/data/users_test.go index bff67c6..77f466b 100644 --- a/internal/data/users_test.go +++ b/internal/data/users_test.go @@ -3,8 +3,6 @@ package data import ( "os" "testing" - - "github.com/davecgh/go-spew/spew" ) func TestUsers(t *testing.T) { @@ -22,7 +20,7 @@ func TestUsers(t *testing.T) { if _, err := GetUser("test"); err == nil { t.Fatal("expected error getting user with no auth method") } - if err := NewUser("test", &UserPass{Username: "test", Password: "test"}); err != nil { + if err := NewUser("test", NewUserPass("test", "test")); err != nil { t.Fatal(err) } tu, err := GetUser("test") @@ -64,8 +62,6 @@ func TestUsers(t *testing.T) { Pub: []byte("test"), } if err = auth.Authenticate(); err != nil { - spew.Dump(user) - spew.Dump(auth) t.Fatalf("expected auth to succeed, got: %v", err) } auth.Pub = []byte("test2") @@ -84,7 +80,7 @@ func TestUsers(t *testing.T) { if err = user.DelPubKey([]byte("test")); err != nil { t.Fatal(err) } - auth := &UserPass{Username: "test", Password: "test"} + auth := NewUserPass("test", "test") if err := auth.Authenticate(); err != nil { t.Fatalf("expected userpass to still be there after deleting public key, got: %v", err) } @@ -97,11 +93,11 @@ func TestUsers(t *testing.T) { if err = user.ChangePassword("test2"); err != nil { t.Fatal(err) } - auth := &UserPass{Username: "test", Password: "test"} + auth := NewUserPass("test", "test") if err = auth.Authenticate(); err == nil { t.Fatal("expected auth to fail using old password") } - auth.Password = "test2" + auth = NewUserPass("test", "test2") if err = auth.Authenticate(); err != nil { t.Fatalf("expected auth to succeed using new password, got: %v", err) } diff --git a/vendor/golang.org/x/crypto/bcrypt/base64.go b/vendor/golang.org/x/crypto/bcrypt/base64.go new file mode 100644 index 0000000..fc31160 --- /dev/null +++ b/vendor/golang.org/x/crypto/bcrypt/base64.go @@ -0,0 +1,35 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bcrypt + +import "encoding/base64" + +const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +var bcEncoding = base64.NewEncoding(alphabet) + +func base64Encode(src []byte) []byte { + n := bcEncoding.EncodedLen(len(src)) + dst := make([]byte, n) + bcEncoding.Encode(dst, src) + for dst[n-1] == '=' { + n-- + } + return dst[:n] +} + +func base64Decode(src []byte) ([]byte, error) { + numOfEquals := 4 - (len(src) % 4) + for i := 0; i < numOfEquals; i++ { + src = append(src, '=') + } + + dst := make([]byte, bcEncoding.DecodedLen(len(src))) + n, err := bcEncoding.Decode(dst, src) + if err != nil { + return nil, err + } + return dst[:n], nil +} diff --git a/vendor/golang.org/x/crypto/bcrypt/bcrypt.go b/vendor/golang.org/x/crypto/bcrypt/bcrypt.go new file mode 100644 index 0000000..aeb73f8 --- /dev/null +++ b/vendor/golang.org/x/crypto/bcrypt/bcrypt.go @@ -0,0 +1,295 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing +// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf +package bcrypt // import "golang.org/x/crypto/bcrypt" + +// The code is a port of Provos and Mazières's C implementation. +import ( + "crypto/rand" + "crypto/subtle" + "errors" + "fmt" + "io" + "strconv" + + "golang.org/x/crypto/blowfish" +) + +const ( + MinCost int = 4 // the minimum allowable cost as passed in to GenerateFromPassword + MaxCost int = 31 // the maximum allowable cost as passed in to GenerateFromPassword + DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword +) + +// The error returned from CompareHashAndPassword when a password and hash do +// not match. +var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password") + +// The error returned from CompareHashAndPassword when a hash is too short to +// be a bcrypt hash. +var ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password") + +// The error returned from CompareHashAndPassword when a hash was created with +// a bcrypt algorithm newer than this implementation. +type HashVersionTooNewError byte + +func (hv HashVersionTooNewError) Error() string { + return fmt.Sprintf("crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'", byte(hv), majorVersion) +} + +// The error returned from CompareHashAndPassword when a hash starts with something other than '$' +type InvalidHashPrefixError byte + +func (ih InvalidHashPrefixError) Error() string { + return fmt.Sprintf("crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'", byte(ih)) +} + +type InvalidCostError int + +func (ic InvalidCostError) Error() string { + return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) +} + +const ( + majorVersion = '2' + minorVersion = 'a' + maxSaltSize = 16 + maxCryptedHashSize = 23 + encodedSaltSize = 22 + encodedHashSize = 31 + minHashSize = 59 +) + +// magicCipherData is an IV for the 64 Blowfish encryption calls in +// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes. +var magicCipherData = []byte{ + 0x4f, 0x72, 0x70, 0x68, + 0x65, 0x61, 0x6e, 0x42, + 0x65, 0x68, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x53, + 0x63, 0x72, 0x79, 0x44, + 0x6f, 0x75, 0x62, 0x74, +} + +type hashed struct { + hash []byte + salt []byte + cost int // allowed range is MinCost to MaxCost + major byte + minor byte +} + +// GenerateFromPassword returns the bcrypt hash of the password at the given +// cost. If the cost given is less than MinCost, the cost will be set to +// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package, +// to compare the returned hashed password with its cleartext version. +func GenerateFromPassword(password []byte, cost int) ([]byte, error) { + p, err := newFromPassword(password, cost) + if err != nil { + return nil, err + } + return p.Hash(), nil +} + +// CompareHashAndPassword compares a bcrypt hashed password with its possible +// plaintext equivalent. Returns nil on success, or an error on failure. +func CompareHashAndPassword(hashedPassword, password []byte) error { + p, err := newFromHash(hashedPassword) + if err != nil { + return err + } + + otherHash, err := bcrypt(password, p.cost, p.salt) + if err != nil { + return err + } + + otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor} + if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 { + return nil + } + + return ErrMismatchedHashAndPassword +} + +// Cost returns the hashing cost used to create the given hashed +// password. When, in the future, the hashing cost of a password system needs +// to be increased in order to adjust for greater computational power, this +// function allows one to establish which passwords need to be updated. +func Cost(hashedPassword []byte) (int, error) { + p, err := newFromHash(hashedPassword) + if err != nil { + return 0, err + } + return p.cost, nil +} + +func newFromPassword(password []byte, cost int) (*hashed, error) { + if cost < MinCost { + cost = DefaultCost + } + p := new(hashed) + p.major = majorVersion + p.minor = minorVersion + + err := checkCost(cost) + if err != nil { + return nil, err + } + p.cost = cost + + unencodedSalt := make([]byte, maxSaltSize) + _, err = io.ReadFull(rand.Reader, unencodedSalt) + if err != nil { + return nil, err + } + + p.salt = base64Encode(unencodedSalt) + hash, err := bcrypt(password, p.cost, p.salt) + if err != nil { + return nil, err + } + p.hash = hash + return p, err +} + +func newFromHash(hashedSecret []byte) (*hashed, error) { + if len(hashedSecret) < minHashSize { + return nil, ErrHashTooShort + } + p := new(hashed) + n, err := p.decodeVersion(hashedSecret) + if err != nil { + return nil, err + } + hashedSecret = hashedSecret[n:] + n, err = p.decodeCost(hashedSecret) + if err != nil { + return nil, err + } + hashedSecret = hashedSecret[n:] + + // The "+2" is here because we'll have to append at most 2 '=' to the salt + // when base64 decoding it in expensiveBlowfishSetup(). + p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2) + copy(p.salt, hashedSecret[:encodedSaltSize]) + + hashedSecret = hashedSecret[encodedSaltSize:] + p.hash = make([]byte, len(hashedSecret)) + copy(p.hash, hashedSecret) + + return p, nil +} + +func bcrypt(password []byte, cost int, salt []byte) ([]byte, error) { + cipherData := make([]byte, len(magicCipherData)) + copy(cipherData, magicCipherData) + + c, err := expensiveBlowfishSetup(password, uint32(cost), salt) + if err != nil { + return nil, err + } + + for i := 0; i < 24; i += 8 { + for j := 0; j < 64; j++ { + c.Encrypt(cipherData[i:i+8], cipherData[i:i+8]) + } + } + + // Bug compatibility with C bcrypt implementations. We only encode 23 of + // the 24 bytes encrypted. + hsh := base64Encode(cipherData[:maxCryptedHashSize]) + return hsh, nil +} + +func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) { + csalt, err := base64Decode(salt) + if err != nil { + return nil, err + } + + // Bug compatibility with C bcrypt implementations. They use the trailing + // NULL in the key string during expansion. + // We copy the key to prevent changing the underlying array. + ckey := append(key[:len(key):len(key)], 0) + + c, err := blowfish.NewSaltedCipher(ckey, csalt) + if err != nil { + return nil, err + } + + var i, rounds uint64 + rounds = 1 << cost + for i = 0; i < rounds; i++ { + blowfish.ExpandKey(ckey, c) + blowfish.ExpandKey(csalt, c) + } + + return c, nil +} + +func (p *hashed) Hash() []byte { + arr := make([]byte, 60) + arr[0] = '$' + arr[1] = p.major + n := 2 + if p.minor != 0 { + arr[2] = p.minor + n = 3 + } + arr[n] = '$' + n++ + copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost))) + n += 2 + arr[n] = '$' + n++ + copy(arr[n:], p.salt) + n += encodedSaltSize + copy(arr[n:], p.hash) + n += encodedHashSize + return arr[:n] +} + +func (p *hashed) decodeVersion(sbytes []byte) (int, error) { + if sbytes[0] != '$' { + return -1, InvalidHashPrefixError(sbytes[0]) + } + if sbytes[1] > majorVersion { + return -1, HashVersionTooNewError(sbytes[1]) + } + p.major = sbytes[1] + n := 3 + if sbytes[2] != '$' { + p.minor = sbytes[2] + n++ + } + return n, nil +} + +// sbytes should begin where decodeVersion left off. +func (p *hashed) decodeCost(sbytes []byte) (int, error) { + cost, err := strconv.Atoi(string(sbytes[0:2])) + if err != nil { + return -1, err + } + err = checkCost(cost) + if err != nil { + return -1, err + } + p.cost = cost + return 3, nil +} + +func (p *hashed) String() string { + return fmt.Sprintf("&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}", string(p.hash), p.salt, p.cost, p.major, p.minor) +} + +func checkCost(cost int) error { + if cost < MinCost || cost > MaxCost { + return InvalidCostError(cost) + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a4fb99e..32e0c4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -173,6 +173,7 @@ go4.org/intern go4.org/unsafe/assume-no-moving-gc # golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d ## explicit; go 1.17 +golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish golang.org/x/crypto/chacha20 golang.org/x/crypto/curve25519