jwt: Cache the bcrypt password hash. (#3526)

Creds don't require secretKeyHash to be calculated
everytime, cache it instead and re-use.

This is an optimization for bcrypt.

Relevant results from the benchmark done locally, negative
value means improvement in this scenario.

```
benchmark                       old ns/op     new ns/op     delta
BenchmarkAuthenticateNode-4     160590992     80125647      -50.11%
BenchmarkAuthenticateWeb-4      160556692     80432144      -49.90%

benchmark                       old allocs     new allocs     delta
BenchmarkAuthenticateNode-4     87             75             -13.79%
BenchmarkAuthenticateWeb-4      87             75             -13.79%

benchmark                       old bytes     new bytes     delta
BenchmarkAuthenticateNode-4     15222         9785          -35.72%
BenchmarkAuthenticateWeb-4      15222         9785          -35.72%
```
This commit is contained in:
Harshavardhana 2017-01-26 16:51:51 -08:00 committed by GitHub
parent 152cdf1c05
commit 85f2b74cfd
13 changed files with 101 additions and 56 deletions

View File

@ -35,8 +35,8 @@ test_script:
# Unit tests # Unit tests
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running - ps: Add-AppveyorTest "Unit Tests" -Outcome Running
- mkdir build\coverage - mkdir build\coverage
- go test -timeout 15m -v -race github.com/minio/minio/cmd... - go test -timeout 15m -race github.com/minio/minio/cmd...
- go test -v -race github.com/minio/minio/pkg... - go test -race github.com/minio/minio/pkg...
- go test -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd - go test -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed - ps: Update-AppveyorTest "Unit Tests" -Outcome Passed

View File

@ -131,8 +131,8 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
} }
// Avoid setting new credentials when they are already passed // Avoid setting new credentials when they are already passed
// by the environnement // by the environment.
if globalEnvAccessKey != "" || globalEnvSecretKey != "" { if globalIsEnvCreds {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL) writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
return return
} }

View File

@ -294,11 +294,9 @@ func TestServiceSetCreds(t *testing.T) {
for i, testCase := range testCases { for i, testCase := range testCases {
// Set or unset environement keys // Set or unset environement keys
if !testCase.EnvKeysSet { if !testCase.EnvKeysSet {
globalEnvAccessKey = "" globalIsEnvCreds = false
globalEnvSecretKey = ""
} else { } else {
globalEnvAccessKey = testCase.Username globalIsEnvCreds = true
globalEnvSecretKey = testCase.Password
} }
// Construct setCreds request body // Construct setCreds request body

View File

@ -315,7 +315,12 @@ func TestIsReqAuthenticated(t *testing.T) {
} }
defer removeAll(path) defer removeAll(path)
serverConfig.SetCredential(credential{"myuser", "mypassword"}) creds, err := getCredential("myuser", "mypassword")
if err != nil {
t.Fatal(err)
}
serverConfig.SetCredential(creds)
// List of test cases for validating http request authentication. // List of test cases for validating http request authentication.
testCases := []struct { testCases := []struct {

View File

@ -63,8 +63,13 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
return err return err
} }
creds, err := getCredential(args.Creds.AccessKey, args.Creds.SecretKey)
if err != nil {
return err
}
// Update credentials in memory // Update credentials in memory
serverConfig.SetCredential(args.Creds) serverConfig.SetCredential(creds)
// Save credentials to config file // Save credentials to config file
if err := serverConfig.Save(); err != nil { if err := serverConfig.Save(); err != nil {

View File

@ -90,10 +90,12 @@ func initConfig() (bool, error) {
// Save config into file. // Save config into file.
return true, serverConfig.Save() return true, serverConfig.Save()
} }
configFile, err := getConfigFile() configFile, err := getConfigFile()
if err != nil { if err != nil {
return false, err return false, err
} }
if _, err = os.Stat(configFile); err != nil { if _, err = os.Stat(configFile); err != nil {
return false, err return false, err
} }
@ -103,7 +105,13 @@ func initConfig() (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
if err := qc.Load(configFile); err != nil {
if err = qc.Load(configFile); err != nil {
return false, err
}
srvCfg.Credential, err = getCredential(srvCfg.Credential.AccessKey, srvCfg.Credential.SecretKey)
if err != nil {
return false, err return false, err
} }
@ -112,6 +120,7 @@ func initConfig() (bool, error) {
// Save the loaded config globally. // Save the loaded config globally.
serverConfig = srvCfg serverConfig = srvCfg
serverConfigMu.Unlock() serverConfigMu.Unlock()
// Set the version properly after the unmarshalled json is loaded. // Set the version properly after the unmarshalled json is loaded.
serverConfig.Version = globalMinioConfigVersion serverConfig.Version = globalMinioConfigVersion
@ -337,6 +346,7 @@ func (s *serverConfigV13) SetCredential(creds credential) {
serverConfigMu.Lock() serverConfigMu.Lock()
defer serverConfigMu.Unlock() defer serverConfigMu.Unlock()
// Set updated credential.
s.Credential = creds s.Credential = creds
} }

View File

@ -19,6 +19,8 @@ package cmd
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"golang.org/x/crypto/bcrypt"
) )
const ( const (
@ -65,14 +67,31 @@ func isSecretKeyValid(secretKey string) bool {
// credential container for access and secret keys. // credential container for access and secret keys.
type credential struct { type credential struct {
AccessKey string `json:"accessKey"` AccessKey string `json:"accessKey,omitempty"`
SecretKey string `json:"secretKey"` SecretKey string `json:"secretKey,omitempty"`
SecretKeyHash []byte `json:"secretKeyHash,omitempty"`
} }
// Generate a bcrypt hashed key for input secret key.
func mustGetHashedSecretKey(secretKey string) []byte {
hashedSecretKey, err := bcrypt.GenerateFromPassword([]byte(secretKey), bcrypt.DefaultCost)
if err != nil {
panic(err)
}
return hashedSecretKey
}
// Initialize a new credential object.
func newCredential() credential { func newCredential() credential {
return credential{mustGetAccessKey(), mustGetSecretKey()} secretKey := mustGetSecretKey()
accessKey := mustGetAccessKey()
secretHash := mustGetHashedSecretKey(secretKey)
return credential{accessKey, secretKey, secretHash}
} }
// Converts accessKey and secretKeys into credential object which
// contains bcrypt secret key hash for future validation.
func getCredential(accessKey, secretKey string) (credential, error) { func getCredential(accessKey, secretKey string) (credential, error) {
if !isAccessKeyValid(accessKey) { if !isAccessKeyValid(accessKey) {
return credential{}, errInvalidAccessKeyLength return credential{}, errInvalidAccessKeyLength
@ -82,5 +101,6 @@ func getCredential(accessKey, secretKey string) (credential, error) {
return credential{}, errInvalidSecretKeyLength return credential{}, errInvalidSecretKeyLength
} }
return credential{accessKey, secretKey}, nil secretHash := mustGetHashedSecretKey(secretKey)
return credential{accessKey, secretKey, secretHash}, nil
} }

View File

@ -110,11 +110,8 @@ var (
// Minio server user agent string. // Minio server user agent string.
globalServerUserAgent = "Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")" globalServerUserAgent = "Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
// Global server's access key // Set to true if credentials were passed from env, default is false.
globalEnvAccessKey = "" globalIsEnvCreds = false
// Global server's secret key
globalEnvSecretKey = ""
// url.URL endpoints of disks that belong to the object storage. // url.URL endpoints of disks that belong to the object storage.
globalEndpoints = []*url.URL{} globalEndpoints = []*url.URL{}

View File

@ -65,8 +65,7 @@ func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string,
// Validate secret key. // Validate secret key.
// Using bcrypt to avoid timing attacks. // Using bcrypt to avoid timing attacks.
hashedSecretKey, _ := bcrypt.GenerateFromPassword([]byte(serverCred.SecretKey), bcrypt.DefaultCost) if bcrypt.CompareHashAndPassword(serverCred.SecretKeyHash, []byte(secretKey)) != nil {
if bcrypt.CompareHashAndPassword(hashedSecretKey, []byte(secretKey)) != nil {
return "", errAuthentication return "", errAuthentication
} }

View File

@ -75,10 +75,40 @@ func testAuthenticate(authType string, t *testing.T) {
} }
} }
func TestNodeAuthenticate(t *testing.T) { func TestAuthenticateNode(t *testing.T) {
testAuthenticate("node", t) testAuthenticate("node", t)
} }
func TestWebAuthenticate(t *testing.T) { func TestAuthenticateWeb(t *testing.T) {
testAuthenticate("web", t) testAuthenticate("web", t)
} }
func BenchmarkAuthenticateNode(b *testing.B) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(testPath)
creds := serverConfig.GetCredential()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
authenticateNode(creds.AccessKey, creds.SecretKey)
}
}
func BenchmarkAuthenticateWeb(b *testing.B) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
b.Fatalf("unable initialize config file, %s", err)
}
defer removeAll(testPath)
creds := serverConfig.GetCredential()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
authenticateWeb(creds.AccessKey, creds.SecretKey)
}
}

View File

@ -190,20 +190,14 @@ func minioInit(ctx *cli.Context) {
enableLoggers() enableLoggers()
// Fetch access keys from environment variables and update the config. // Fetch access keys from environment variables and update the config.
globalEnvAccessKey = os.Getenv("MINIO_ACCESS_KEY") accessKey := os.Getenv("MINIO_ACCESS_KEY")
globalEnvSecretKey = os.Getenv("MINIO_SECRET_KEY") secretKey := os.Getenv("MINIO_SECRET_KEY")
if globalEnvAccessKey != "" && globalEnvSecretKey != "" { if accessKey != "" && secretKey != "" {
creds, err := getCredential(accessKey, secretKey)
fatalIf(err, "Credentials are invalid, please set proper credentials `minio server --help`")
// Set new credentials. // Set new credentials.
serverConfig.SetCredential(credential{ serverConfig.SetCredential(creds)
AccessKey: globalEnvAccessKey,
SecretKey: globalEnvSecretKey,
})
}
if !isAccessKeyValid(serverConfig.GetCredential().AccessKey) {
fatalIf(errInvalidArgument, "Invalid access key. Accept only a string starting with a alphabetic and containing from 5 to 20 characters.")
}
if !isSecretKeyValid(serverConfig.GetCredential().SecretKey) {
fatalIf(errInvalidArgument, "Invalid secret key. Accept only a string containing from 8 to 40 characters.")
} }
// Init the error tracing module. // Init the error tracing module.

View File

@ -363,16 +363,18 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
} }
// As we already validated the authentication, we save given access/secret keys. // As we already validated the authentication, we save given access/secret keys.
cred, err := getCredential(args.AccessKey, args.SecretKey) creds, err := getCredential(args.AccessKey, args.SecretKey)
if err != nil { if err != nil {
return toJSONError(err) return toJSONError(err)
} }
// Notify all other Minio peers to update credentials // Notify all other Minio peers to update credentials
errsMap := updateCredsOnPeers(cred) errsMap := updateCredsOnPeers(creds)
// Update local credentials // Update local credentials
serverConfig.SetCredential(cred) serverConfig.SetCredential(creds)
// Persist updated credentials.
if err = serverConfig.Save(); err != nil { if err = serverConfig.Save(); err != nil {
errsMap[globalMinioAddr] = err errsMap[globalMinioAddr] = err
} }
@ -506,14 +508,7 @@ func (web *webAPIHandlers) Download(w http.ResponseWriter, r *http.Request) {
objectLock.RLock() objectLock.RLock()
defer objectLock.RUnlock() defer objectLock.RUnlock()
objInfo, err := objectAPI.GetObjectInfo(bucket, object) if err := objectAPI.GetObject(bucket, object, 0, -1, w); err != nil {
if err != nil {
writeWebErrorResponse(w, err)
return
}
offset := int64(0)
err = objectAPI.GetObject(bucket, object, offset, objInfo.Size, w)
if err != nil {
/// No need to print error, response writer already written to. /// No need to print error, response writer already written to.
return return
} }

View File

@ -1533,10 +1533,6 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code) t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code)
} }
resp := string(rec.Body.Bytes())
if !strings.Contains(resp, "We encountered an internal error, please try again.") {
t.Fatalf("Unexpected error message, expected: `Invalid token`, found: `%s`", resp)
}
// Test authorization of Web.Upload // Test authorization of Web.Upload
content := []byte("temporary file's content") content := []byte("temporary file's content")
@ -1553,8 +1549,4 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code) t.Fatalf("Expected the response status to be 200, but instead found `%d`", rec.Code)
} }
resp = string(rec.Body.Bytes())
if !strings.Contains(resp, "We encountered an internal error, please try again.") {
t.Fatalf("Unexpected error message, expected: `Invalid token`, found: `%s`", resp)
}
} }