Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: user passwords cleanup#1202

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
coadler merged 3 commits intomainfromcolin/password-cleanup
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes fromall 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
70 changes: 70 additions & 0 deletionscoderd/userpassword/hashing_bench_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
package userpassword_test

import (
"crypto/sha256"
"testing"

"github.com/coder/coder/cryptorand"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
)

var (
salt = []byte(must(cryptorand.String(16)))
secret = []byte(must(cryptorand.String(24)))

resBcrypt []byte
resPbkdf2 []byte
)

func BenchmarkBcryptMinCost(b *testing.B) {
var r []byte
b.ReportAllocs()

for i := 0; i < b.N; i++ {
r, _ = bcrypt.GenerateFromPassword(secret, bcrypt.MinCost)
}

resBcrypt = r
}

func BenchmarkPbkdf2MinCost(b *testing.B) {
var r []byte
b.ReportAllocs()

for i := 0; i < b.N; i++ {
r = pbkdf2.Key(secret, salt, 1024, 64, sha256.New)
}

resPbkdf2 = r
}

func BenchmarkBcryptDefaultCost(b *testing.B) {
var r []byte
b.ReportAllocs()

for i := 0; i < b.N; i++ {
r, _ = bcrypt.GenerateFromPassword(secret, bcrypt.DefaultCost)
}

resBcrypt = r
}

func BenchmarkPbkdf2(b *testing.B) {
var r []byte
b.ReportAllocs()

for i := 0; i < b.N; i++ {
r = pbkdf2.Key(secret, salt, 65536, 64, sha256.New)
}

resPbkdf2 = r
}

func must(s string, err error) string {
if err != nil {
panic(err)
}

return s
}
79 changes: 62 additions & 17 deletionscoderd/userpassword/userpassword.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,25 +6,67 @@ import (
"crypto/subtle"
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"

"golang.org/x/crypto/pbkdf2"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
)

const (
// This is the length of our output hash.
// bcrypt has a hash size of 59, so we rounded up to a power of 8.
var (
// The base64 encoder used when producing the string representation of
// hashes.
base64Encoding = base64.RawStdEncoding

// The number of iterations to use when generating the hash. This was chosen
// to make it about as fast as bcrypt hashes. Increasing this causes hashes
// to take longer to compute.
defaultHashIter = 65535

// This is the length of our output hash. bcrypt has a hash size of up to
// 60, so we rounded up to a power of 8.
hashLength = 64

// The scheme to include in our hashed password.
hashScheme = "pbkdf2-sha256"

// A salt size of 16 is the default in passlib. A minimum of 8 can be safely
// used.
defaultSaltSize = 16

// The simulated hash is used when trying to simulate password checks for
// users that don't exist.
simulatedHash, _ = Hash("hunter2")
)

// Compare checks the equality of passwords from a hashed pbkdf2 string.
// This uses pbkdf2 to ensure FIPS 140-2 compliance. See:
// Make password hashing much faster in tests.
func init() {
args := os.Args[1:]

// Ensure this can never be enabled if running in server mode.
if slices.Contains(args, "server") {
return
}

for _, flag := range args {
if strings.HasPrefix(flag, "-test.") {
defaultHashIter = 1
return
}
}
}

// Compare checks the equality of passwords from a hashed pbkdf2 string. This
// uses pbkdf2 to ensure FIPS 140-2 compliance. See:
// https://csrc.nist.gov/csrc/media/templates/cryptographic-module-validation-program/documents/security-policies/140sp2261.pdf
func Compare(hashed string, password string) (bool, error) {
// If the hased password provided is empty, simulate comparing a real hash.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this so attackers can't use a timing attack to check if the user has no password set?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It's for users that don't exist, so that there's no difference in response times based on if the user exists or not.

kylecarbs reacted with thumbs up emoji
if hashed == "" {
hashed = simulatedHash
}

if len(hashed) < hashLength {
return false, xerrors.Errorf("hash too short: %d", len(hashed))
}
Expand All@@ -42,37 +84,40 @@ func Compare(hashed string, password string) (bool, error) {
if err != nil {
return false, xerrors.Errorf("parse iter from hash: %w", err)
}
salt, err :=base64.RawStdEncoding.DecodeString(parts[3])
salt, err :=base64Encoding.DecodeString(parts[3])
if err != nil {
return false, xerrors.Errorf("decode salt: %w", err)
}

if subtle.ConstantTimeCompare([]byte(hashWithSaltAndIter(password, salt, iter)), []byte(hashed)) != 1 {
return false, nil
}

return true, nil
}

// Hash generates a hash using pbkdf2.
// See the Compare() comment for rationale.
func Hash(password string) (string, error) {
// bcrypt uses a salt size of 16 bytes.
salt := make([]byte, 16)
salt := make([]byte, defaultSaltSize)
_, err := rand.Read(salt)
if err != nil {
return "", xerrors.Errorf("read random bytes for salt: %w", err)
}
// The default hash iteration is 1024 for speed.
// As this is increased, the password is hashed more.
return hashWithSaltAndIter(password, salt, 1024), nil

return hashWithSaltAndIter(password, salt, defaultHashIter), nil
}

// Produces a string representation of the hash.
func hashWithSaltAndIter(password string, salt []byte, iter int) string {
hash := pbkdf2.Key([]byte(password), salt, iter, hashLength, sha256.New)
hash = []byte(base64.RawStdEncoding.EncodeToString(hash))
salt = []byte(base64.RawStdEncoding.EncodeToString(salt))
// This format is similar to bcrypt. See:
// https://en.wikipedia.org/wiki/Bcrypt#Description
return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, salt, hash)
var (
hash = pbkdf2.Key([]byte(password), salt, iter, hashLength, sha256.New)
encHash = make([]byte, base64Encoding.EncodedLen(len(hash)))
encSalt = make([]byte, base64Encoding.EncodedLen(len(salt)))
)

base64Encoding.Encode(encHash, hash)
base64Encoding.Encode(encSalt, salt)

return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, encSalt, encHash)
}
12 changes: 5 additions & 7 deletionscoderd/users.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -419,21 +419,19 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
if !httpapi.Read(rw, r, &loginWithPassword) {
return
}

user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Email: loginWithPassword.Email,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "invalid email or password",
})
return
}
if err != nil {
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err.Error()),
})
return
}

// If the user doesn't exist, it will be a default struct.

equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp