- Notifications
You must be signed in to change notification settings - Fork928
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
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
) | ||
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") | ||
) | ||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
if hashed == "" { | ||
hashed = simulatedHash | ||
} | ||
if len(hashed) < hashLength { | ||
return false, xerrors.Errorf("hash too short: %d", len(hashed)) | ||
} | ||
@@ -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 :=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) { | ||
salt := make([]byte, defaultSaltSize) | ||
_, err := rand.Read(salt) | ||
if err != nil { | ||
return "", xerrors.Errorf("read random bytes for salt: %w", err) | ||
} | ||
return hashWithSaltAndIter(password, salt, defaultHashIter), nil | ||
} | ||
// Produces a string representation of the hash. | ||
func hashWithSaltAndIter(password string, salt []byte, iter int) string { | ||
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) | ||
} |