From 8a204f59e723a1294276163435a6b51462f5fdad Mon Sep 17 00:00:00 2001 From: BadZen Date: Tue, 21 Apr 2015 19:57:12 +0000 Subject: [PATCH 01/13] Implementation of a basic authentication scheme using standard .htpasswd files Signed-off-by: BadZen Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 112 +++++++++++++++++++++++++++++ registry/auth/basic/access_test.go | 100 ++++++++++++++++++++++++++ registry/auth/basic/htpasswd.go | 49 +++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 registry/auth/basic/access.go create mode 100644 registry/auth/basic/access_test.go create mode 100644 registry/auth/basic/htpasswd.go diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go new file mode 100644 index 00000000..1833296a --- /dev/null +++ b/registry/auth/basic/access.go @@ -0,0 +1,112 @@ +// Package basic provides a simple authentication scheme that checks for the +// user credential hash in an htpasswd formatted file in a configuration-determined +// location. +// +// The use of SHA hashes (htpasswd -s) is enforced since MD5 is insecure and simple +// system crypt() may be as well. +// +// This authentication method MUST be used under TLS, as simple token-replay attack is possible. + +package basic + +import ( + "encoding/base64" + "errors" + "fmt" + "net/http" + "strings" + + ctxu "github.com/docker/distribution/context" + "github.com/docker/distribution/registry/auth" + "golang.org/x/net/context" +) + +type accessController struct { + realm string + htpasswd *HTPasswd +} + +type challenge struct { + realm string + err error +} + +var _ auth.AccessController = &accessController{} +var ( + ErrPasswordRequired = errors.New("authorization credential required") + ErrInvalidCredential = errors.New("invalid authorization credential") +) + +func newAccessController(options map[string]interface{}) (auth.AccessController, error) { + realm, present := options["realm"] + if _, ok := realm.(string); !present || !ok { + return nil, fmt.Errorf(`"realm" must be set for basic access controller`) + } + + path, present := options["path"] + if _, ok := path.(string); !present || !ok { + return nil, fmt.Errorf(`"path" must be set for basic access controller`) + } + + return &accessController{realm: realm.(string), htpasswd: NewHTPasswd(path.(string))}, nil +} + +func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { + req, err := ctxu.GetRequest(ctx) + if err != nil { + return nil, err + } + + authHeader := req.Header.Get("Authorization") + + if authHeader == "" { + challenge := challenge{ + realm: ac.realm, + } + return nil, &challenge + } + + parts := strings.Split(req.Header.Get("Authorization"), " ") + + challenge := challenge{ + realm: ac.realm, + } + + if len(parts) != 2 || strings.ToLower(parts[0]) != "basic" { + challenge.err = ErrPasswordRequired + return nil, &challenge + } + + text, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + challenge.err = ErrInvalidCredential + return nil, &challenge + } + + credential := strings.Split(string(text), ":") + if len(credential) != 2 { + challenge.err = ErrInvalidCredential + return nil, &challenge + } + + if res, _ := ac.htpasswd.AuthenticateUser(credential[0], credential[1]); !res { + challenge.err = ErrInvalidCredential + return nil, &challenge + } + + return auth.WithUser(ctx, auth.UserInfo{Name: credential[0]}), nil +} + +func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { + header := fmt.Sprintf("Realm realm=%q", ch.realm) + w.Header().Set("WWW-Authenticate", header) + w.WriteHeader(http.StatusUnauthorized) +} + +func (ch *challenge) Error() string { + return fmt.Sprintf("basic authentication challenge: %#v", ch) +} + +func init() { + auth.Register("basic", auth.InitFunc(newAccessController)) +} diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go new file mode 100644 index 00000000..d82573b9 --- /dev/null +++ b/registry/auth/basic/access_test.go @@ -0,0 +1,100 @@ +package basic + +import ( + "encoding/base64" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/docker/distribution/registry/auth" + "golang.org/x/net/context" +) + +func TestBasicAccessController(t *testing.T) { + + testRealm := "The-Shire" + testUser := "bilbo" + testHtpasswdContent := "bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs=" + + tempFile, err := ioutil.TempFile("", "htpasswd-test") + if err != nil { + t.Fatal("could not create temporary htpasswd file") + } + if _, err = tempFile.WriteString(testHtpasswdContent); err != nil { + t.Fatal("could not write temporary htpasswd file") + } + + options := map[string]interface{}{ + "realm": testRealm, + "path": tempFile.Name(), + } + + accessController, err := newAccessController(options) + if err != nil { + t.Fatal("error creating access controller") + } + + tempFile.Close() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(nil, "http.request", r) + authCtx, err := accessController.Authorized(ctx) + if err != nil { + switch err := err.(type) { + case auth.Challenge: + err.ServeHTTP(w, r) + return + default: + t.Fatalf("unexpected error authorizing request: %v", err) + } + } + + userInfo, ok := authCtx.Value("auth.user").(auth.UserInfo) + if !ok { + t.Fatal("basic accessController did not set auth.user context") + } + + if userInfo.Name != testUser { + t.Fatalf("expected user name %q, got %q", testUser, userInfo.Name) + } + + w.WriteHeader(http.StatusNoContent) + })) + + client := &http.Client{ + CheckRedirect: nil, + } + + req, _ := http.NewRequest("GET", server.URL, nil) + resp, err := client.Do(req) + + if err != nil { + t.Fatalf("unexpected error during GET: %v", err) + } + defer resp.Body.Close() + + // Request should not be authorized + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) + } + + req, _ = http.NewRequest("GET", server.URL, nil) + + sekrit := "bilbo:baggins" + credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) + + req.Header.Set("Authorization", credential) + resp, err = client.Do(req) + + if err != nil { + t.Fatalf("unexpected error during GET: %v", err) + } + defer resp.Body.Close() + + // Request should be authorized + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected non-success response status: %v != %v", resp.StatusCode, http.StatusNoContent) + } + +} diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go new file mode 100644 index 00000000..6833bc5c --- /dev/null +++ b/registry/auth/basic/htpasswd.go @@ -0,0 +1,49 @@ +package basic + +import ( + "crypto/sha1" + "encoding/base64" + "encoding/csv" + "errors" + "os" +) + +var ErrSHARequired = errors.New("htpasswd file must use SHA (htpasswd -s)") + +type HTPasswd struct { + path string + reader *csv.Reader +} + +func NewHTPasswd(htpath string) *HTPasswd { + return &HTPasswd{path: htpath} +} + +func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { + + // Hash the credential. + sha := sha1.New() + sha.Write([]byte(pwd)) + hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) + + // Open the file. + in, err := os.Open(htpasswd.path) + if err != nil { + return false, err + } + + // Parse the contents of the standard .htpasswd until we hit the end or find a match. + reader := csv.NewReader(in) + reader.Comma = ':' + reader.Comment = '#' + reader.TrimLeadingSpace = true + for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() { + if entry[0] == user { + if len(entry[1]) < 6 || entry[1][0:5] != "{SHA}" { + return false, ErrSHARequired + } + return entry[1][5:] == hash, nil + } + } + return false, nil +} From 0ecaa7f40a125c75937f1a43b1b0e9303e40226a Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Wed, 22 Apr 2015 14:35:59 +0000 Subject: [PATCH 02/13] Fixed WWW-Authenticate: header, added example config and import into main, fixed golint warnings Signed-off-by: Dave Trombley --- cmd/registry/config.yml | 4 ++++ cmd/registry/main.go | 1 + registry/auth/basic/access.go | 5 +++-- registry/auth/basic/htpasswd.go | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/registry/config.yml b/cmd/registry/config.yml index 6d41cc8f..abc868d9 100644 --- a/cmd/registry/config.yml +++ b/cmd/registry/config.yml @@ -26,6 +26,10 @@ storage: maintenance: uploadpurging: enabled: false +auth: + basic: + realm: test-realm + path: /tmp/registry-dev/.htpasswd http: addr: :5000 secret: asecretforlocaldevelopment diff --git a/cmd/registry/main.go b/cmd/registry/main.go index 49132bf1..8c591bad 100644 --- a/cmd/registry/main.go +++ b/cmd/registry/main.go @@ -18,6 +18,7 @@ import ( "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" _ "github.com/docker/distribution/health" + _ "github.com/docker/distribution/registry/auth/basic" _ "github.com/docker/distribution/registry/auth/silly" _ "github.com/docker/distribution/registry/auth/token" "github.com/docker/distribution/registry/handlers" diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 1833296a..76f036c0 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -6,7 +6,6 @@ // system crypt() may be as well. // // This authentication method MUST be used under TLS, as simple token-replay attack is possible. - package basic import ( @@ -33,7 +32,9 @@ type challenge struct { var _ auth.AccessController = &accessController{} var ( + // ErrPasswordRequired - returned when no auth token is given. ErrPasswordRequired = errors.New("authorization credential required") + // ErrInvalidCredential - returned when the auth token does not authenticate correctly. ErrInvalidCredential = errors.New("invalid authorization credential") ) @@ -98,7 +99,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut } func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { - header := fmt.Sprintf("Realm realm=%q", ch.realm) + header := fmt.Sprintf("Basic realm=%q", ch.realm) w.Header().Set("WWW-Authenticate", header) w.WriteHeader(http.StatusUnauthorized) } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 6833bc5c..36eca347 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -8,17 +8,22 @@ import ( "os" ) +// ErrSHARequired - returned in error field of challenge when the htpasswd was not made using SHA1 algorithm. +// (SHA1 is considered obsolete but the alternative for htpasswd is MD5, or system crypt...) var ErrSHARequired = errors.New("htpasswd file must use SHA (htpasswd -s)") +// HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it. type HTPasswd struct { path string reader *csv.Reader } +// NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file. func NewHTPasswd(htpath string) *HTPasswd { return &HTPasswd{path: htpath} } +// AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file. func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { // Hash the credential. From c4849bb99adb85e371e224d11373e36290f29cae Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Wed, 22 Apr 2015 15:13:48 +0000 Subject: [PATCH 03/13] Aligned formatting with gofmt Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 76f036c0..dd792374 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -33,8 +33,8 @@ type challenge struct { var _ auth.AccessController = &accessController{} var ( // ErrPasswordRequired - returned when no auth token is given. - ErrPasswordRequired = errors.New("authorization credential required") - // ErrInvalidCredential - returned when the auth token does not authenticate correctly. + ErrPasswordRequired = errors.New("authorization credential required") + // ErrInvalidCredential - returned when the auth token does not authenticate correctly. ErrInvalidCredential = errors.New("invalid authorization credential") ) From c50dfb7dae8f1a9be9649bd065fce4f5e37b27c2 Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Thu, 4 Jun 2015 11:46:34 -0400 Subject: [PATCH 04/13] Added support for bcrypt, plaintext; extension points for other htpasswd hash methods. Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 38 ++++-------- registry/auth/basic/access_test.go | 48 +++++++++------ registry/auth/basic/htpasswd.go | 95 ++++++++++++++++++++++++++---- 3 files changed, 123 insertions(+), 58 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index dd792374..81a22b40 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -9,11 +9,9 @@ package basic import ( - "encoding/base64" "errors" "fmt" "net/http" - "strings" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" @@ -58,8 +56,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, err } - authHeader := req.Header.Get("Authorization") - + authHeader := req.Header.Get("Authorization") if authHeader == "" { challenge := challenge{ realm: ac.realm, @@ -67,35 +64,20 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, &challenge } - parts := strings.Split(req.Header.Get("Authorization"), " ") - - challenge := challenge{ - realm: ac.realm, + user, pass, ok := req.BasicAuth() + if !ok { + return nil, errors.New("Invalid Authorization header") } - - if len(parts) != 2 || strings.ToLower(parts[0]) != "basic" { - challenge.err = ErrPasswordRequired - return nil, &challenge - } - - text, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { + + if res, _ := ac.htpasswd.AuthenticateUser(user, pass); !res { + challenge := challenge{ + realm: ac.realm, + } challenge.err = ErrInvalidCredential return nil, &challenge } - credential := strings.Split(string(text), ":") - if len(credential) != 2 { - challenge.err = ErrInvalidCredential - return nil, &challenge - } - - if res, _ := ac.htpasswd.AuthenticateUser(credential[0], credential[1]); !res { - challenge.err = ErrInvalidCredential - return nil, &challenge - } - - return auth.WithUser(ctx, auth.UserInfo{Name: credential[0]}), nil + return auth.WithUser(ctx, auth.UserInfo{Name: user}), nil } func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go index d82573b9..b731675e 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/basic/access_test.go @@ -14,8 +14,13 @@ import ( func TestBasicAccessController(t *testing.T) { testRealm := "The-Shire" - testUser := "bilbo" - testHtpasswdContent := "bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs=" + testUsers := []string{"bilbo","frodo","MiShil","DeokMan"} + testPasswords := []string{"baggins","baggins","새주","공주님"} + testHtpasswdContent := `bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= + frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W + MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 + DeokMan:공주님` + tempFile, err := ioutil.TempFile("", "htpasswd-test") if err != nil { @@ -36,7 +41,9 @@ func TestBasicAccessController(t *testing.T) { } tempFile.Close() - + + var userNumber = 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(nil, "http.request", r) authCtx, err := accessController.Authorized(ctx) @@ -55,8 +62,8 @@ func TestBasicAccessController(t *testing.T) { t.Fatal("basic accessController did not set auth.user context") } - if userInfo.Name != testUser { - t.Fatalf("expected user name %q, got %q", testUser, userInfo.Name) + if userInfo.Name != testUsers[userNumber] { + t.Fatalf("expected user name %q, got %q", testUsers[userNumber], userInfo.Name) } w.WriteHeader(http.StatusNoContent) @@ -79,22 +86,25 @@ func TestBasicAccessController(t *testing.T) { t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } - req, _ = http.NewRequest("GET", server.URL, nil) + for i := 0; i < len(testUsers); i++ { + userNumber = i + req, _ = http.NewRequest("GET", server.URL, nil) + sekrit := testUsers[i]+":"+testPasswords[i] + credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) - sekrit := "bilbo:baggins" - credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) + req.Header.Set("Authorization", credential) + resp, err = client.Do(req) + + if err != nil { + t.Fatalf("unexpected error during GET: %v", err) + } + defer resp.Body.Close() - req.Header.Set("Authorization", credential) - resp, err = client.Do(req) - - if err != nil { - t.Fatalf("unexpected error during GET: %v", err) - } - defer resp.Body.Close() - - // Request should be authorized - if resp.StatusCode != http.StatusNoContent { - t.Fatalf("unexpected non-success response status: %v != %v", resp.StatusCode, http.StatusNoContent) + // Request should be authorized + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected non-success response status: %v != %v for %s %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i], credential) + } } + } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 36eca347..69dae9d8 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -6,11 +6,14 @@ import ( "encoding/csv" "errors" "os" + "regexp" + "strings" + + "golang.org/x/crypto/bcrypt" ) -// ErrSHARequired - returned in error field of challenge when the htpasswd was not made using SHA1 algorithm. -// (SHA1 is considered obsolete but the alternative for htpasswd is MD5, or system crypt...) -var ErrSHARequired = errors.New("htpasswd file must use SHA (htpasswd -s)") +// AuthenticationFailureErr - a generic error message for authentication failure to be presented to agent. +var AuthenticationFailureErr = errors.New("Bad username or password") // HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it. type HTPasswd struct { @@ -18,18 +21,57 @@ type HTPasswd struct { reader *csv.Reader } +// AuthType represents a particular hash function used in the htpasswd file. +type AuthType int +const ( + PlainText AuthType = iota + SHA1 + ApacheMD5 + BCrypt + Crypt +) + +// String returns a text representation of the AuthType +func (at AuthType) String() string { + switch(at) { + case PlainText: return "plaintext" + case SHA1: return "sha1" + case ApacheMD5: return "md5" + case BCrypt: return "bcrypt" + case Crypt: return "system crypt" + } + return "unknown" +} + + // NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file. func NewHTPasswd(htpath string) *HTPasswd { return &HTPasswd{path: htpath} } +var bcryptPrefixRegexp *regexp.Regexp = regexp.MustCompile(`^\$2[ab]?y\$`) + +// GetAuthCredentialType - Inspect an htpasswd file credential and guess the encryption algorithm used. +func GetAuthCredentialType(cred string) AuthType { + if strings.HasPrefix(cred, "{SHA}") { + return SHA1 + } + if strings.HasPrefix(cred, "$apr1$") { + return ApacheMD5 + } + if bcryptPrefixRegexp.MatchString(cred) { + return BCrypt + } + // There's just not a great way to distinguish between these next two... + if len(cred) == 13 { + return Crypt + } + return PlainText +} + // AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file. func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { - // Hash the credential. - sha := sha1.New() - sha.Write([]byte(pwd)) - hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) // Open the file. in, err := os.Open(htpasswd.path) @@ -43,12 +85,43 @@ func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error reader.Comment = '#' reader.TrimLeadingSpace = true for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() { + if readerr != nil { + return false, readerr + } + if len(entry) == 0 { + continue + } if entry[0] == user { - if len(entry[1]) < 6 || entry[1][0:5] != "{SHA}" { - return false, ErrSHARequired + credential := entry[1] + credType := GetAuthCredentialType(credential) + switch(credType) { + case SHA1: { + sha := sha1.New() + sha.Write([]byte(pwd)) + hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) + return entry[1][5:] == hash, nil + } + case ApacheMD5: { + return false, errors.New(ApacheMD5.String()+" htpasswd hash function not yet supported") + } + case BCrypt: { + err := bcrypt.CompareHashAndPassword([]byte(credential),[]byte(pwd)) + if err != nil { + return false, err + } + return true, nil + } + case Crypt: { + return false, errors.New(Crypt.String()+" htpasswd hash function not yet supported") + } + case PlainText: { + if pwd == credential { + return true, nil + } + return false, AuthenticationFailureErr + } } - return entry[1][5:] == hash, nil } } - return false, nil + return false, AuthenticationFailureErr } From 04f6a4811d044454b2330e41aac8f5abab8ee6c2 Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Thu, 4 Jun 2015 12:02:13 -0400 Subject: [PATCH 05/13] Fixed golint, gofmt warning advice. Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 4 +- registry/auth/basic/access_test.go | 14 +++---- registry/auth/basic/htpasswd.go | 64 ++++++++++++++++++------------ 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 81a22b40..0b3e2788 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -56,7 +56,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, err } - authHeader := req.Header.Get("Authorization") + authHeader := req.Header.Get("Authorization") if authHeader == "" { challenge := challenge{ realm: ac.realm, @@ -68,7 +68,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut if !ok { return nil, errors.New("Invalid Authorization header") } - + if res, _ := ac.htpasswd.AuthenticateUser(user, pass); !res { challenge := challenge{ realm: ac.realm, diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go index b731675e..62699a63 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/basic/access_test.go @@ -14,13 +14,12 @@ import ( func TestBasicAccessController(t *testing.T) { testRealm := "The-Shire" - testUsers := []string{"bilbo","frodo","MiShil","DeokMan"} - testPasswords := []string{"baggins","baggins","새주","공주님"} + testUsers := []string{"bilbo", "frodo", "MiShil", "DeokMan"} + testPasswords := []string{"baggins", "baggins", "새주", "공주님"} testHtpasswdContent := `bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 DeokMan:공주님` - tempFile, err := ioutil.TempFile("", "htpasswd-test") if err != nil { @@ -41,9 +40,9 @@ func TestBasicAccessController(t *testing.T) { } tempFile.Close() - + var userNumber = 0 - + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(nil, "http.request", r) authCtx, err := accessController.Authorized(ctx) @@ -89,12 +88,12 @@ func TestBasicAccessController(t *testing.T) { for i := 0; i < len(testUsers); i++ { userNumber = i req, _ = http.NewRequest("GET", server.URL, nil) - sekrit := testUsers[i]+":"+testPasswords[i] + sekrit := testUsers[i] + ":" + testPasswords[i] credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) req.Header.Set("Authorization", credential) resp, err = client.Do(req) - + if err != nil { t.Fatalf("unexpected error during GET: %v", err) } @@ -105,6 +104,5 @@ func TestBasicAccessController(t *testing.T) { t.Fatalf("unexpected non-success response status: %v != %v for %s %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i], credential) } } - } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 69dae9d8..89e4b749 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -8,12 +8,12 @@ import ( "os" "regexp" "strings" - + "golang.org/x/crypto/bcrypt" ) // AuthenticationFailureErr - a generic error message for authentication failure to be presented to agent. -var AuthenticationFailureErr = errors.New("Bad username or password") +var ErrAuthenticationFailure = errors.New("Bad username or password") // HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it. type HTPasswd struct { @@ -22,34 +22,44 @@ type HTPasswd struct { } // AuthType represents a particular hash function used in the htpasswd file. -type AuthType int +type AuthType int + const ( - PlainText AuthType = iota + // PlainText - Plain-text password storage (htpasswd -p) + PlainText AuthType = iota + // SHA1 - sha hashed password storage (htpasswd -s) SHA1 + // ApacheMD5 - apr iterated md5 hashing (htpasswd -m) ApacheMD5 + // BCrypt - BCrypt adapative password hashing (htpasswd -B) BCrypt + // Crypt - System crypt() hashes. (htpasswd -d) Crypt ) // String returns a text representation of the AuthType func (at AuthType) String() string { - switch(at) { - case PlainText: return "plaintext" - case SHA1: return "sha1" - case ApacheMD5: return "md5" - case BCrypt: return "bcrypt" - case Crypt: return "system crypt" + switch at { + case PlainText: + return "plaintext" + case SHA1: + return "sha1" + case ApacheMD5: + return "md5" + case BCrypt: + return "bcrypt" + case Crypt: + return "system crypt" } return "unknown" } - // NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file. func NewHTPasswd(htpath string) *HTPasswd { return &HTPasswd{path: htpath} } -var bcryptPrefixRegexp *regexp.Regexp = regexp.MustCompile(`^\$2[ab]?y\$`) +var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`) // GetAuthCredentialType - Inspect an htpasswd file credential and guess the encryption algorithm used. func GetAuthCredentialType(cred string) AuthType { @@ -72,7 +82,6 @@ func GetAuthCredentialType(cred string) AuthType { // AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file. func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { - // Open the file. in, err := os.Open(htpasswd.path) if err != nil { @@ -94,34 +103,39 @@ func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error if entry[0] == user { credential := entry[1] credType := GetAuthCredentialType(credential) - switch(credType) { - case SHA1: { + switch credType { + case SHA1: + { sha := sha1.New() sha.Write([]byte(pwd)) hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) return entry[1][5:] == hash, nil } - case ApacheMD5: { - return false, errors.New(ApacheMD5.String()+" htpasswd hash function not yet supported") + case ApacheMD5: + { + return false, errors.New(ApacheMD5.String() + " htpasswd hash function not yet supported") } - case BCrypt: { - err := bcrypt.CompareHashAndPassword([]byte(credential),[]byte(pwd)) + case BCrypt: + { + err := bcrypt.CompareHashAndPassword([]byte(credential), []byte(pwd)) if err != nil { return false, err } return true, nil } - case Crypt: { - return false, errors.New(Crypt.String()+" htpasswd hash function not yet supported") + case Crypt: + { + return false, errors.New(Crypt.String() + " htpasswd hash function not yet supported") } - case PlainText: { + case PlainText: + { if pwd == credential { return true, nil - } - return false, AuthenticationFailureErr + } + return false, ErrAuthenticationFailure } } } } - return false, AuthenticationFailureErr + return false, ErrAuthenticationFailure } From d4f2260e04bf083570775033a1620b6c273bd12b Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Sat, 6 Jun 2015 01:32:55 -0400 Subject: [PATCH 06/13] Added dependency to golang.org/x/crypto/bcrypt Signed-off-by: Dave Trombley --- Godeps/Godeps.json | 8 + .../src/golang.org/x/crypto/bcrypt/base64.go | 35 +++ .../src/golang.org/x/crypto/bcrypt/bcrypt.go | 294 ++++++++++++++++++ .../golang.org/x/crypto/bcrypt/bcrypt_test.go | 226 ++++++++++++++ .../src/golang.org/x/crypto/blowfish/block.go | 159 ++++++++++ .../x/crypto/blowfish/blowfish_test.go | 274 ++++++++++++++++ .../golang.org/x/crypto/blowfish/cipher.go | 91 ++++++ .../src/golang.org/x/crypto/blowfish/const.go | 199 ++++++++++++ 8 files changed, 1286 insertions(+) create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/bcrypt/base64.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt_test.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/blowfish/block.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/blowfish/blowfish_test.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/blowfish/cipher.go create mode 100644 Godeps/_workspace/src/golang.org/x/crypto/blowfish/const.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 004b5329..9f3170d8 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -96,6 +96,14 @@ "ImportPath": "github.com/yvasiyarov/newrelic_platform_go", "Rev": "b21fdbd4370f3717f3bbd2bf41c223bc273068e6" }, + { + "ImportPath": "golang.org/x/crypto/bcrypt", + "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" + }, + { + "ImportPath": "golang.org/x/crypto/blowfish", + "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" + }, { "ImportPath": "golang.org/x/net/context", "Rev": "1dfe7915deaf3f80b962c163b918868d8a6d8974" diff --git a/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/base64.go b/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/base64.go new file mode 100644 index 00000000..fc311609 --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt.go b/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt.go new file mode 100644 index 00000000..b8e18d74 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt.go @@ -0,0 +1,294 @@ +// 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 + +// The code is a port of Provos and Mazières's C implementation. +import ( + "crypto/rand" + "crypto/subtle" + "errors" + "fmt" + "golang.org/x/crypto/blowfish" + "io" + "strconv" +) + +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. + ckey := append(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 += 1 + copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost))) + n += 2 + arr[n] = '$' + n += 1 + 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/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt_test.go b/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt_test.go new file mode 100644 index 00000000..f08a6f5b --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/bcrypt/bcrypt_test.go @@ -0,0 +1,226 @@ +// 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 ( + "bytes" + "fmt" + "testing" +) + +func TestBcryptingIsEasy(t *testing.T) { + pass := []byte("mypassword") + hp, err := GenerateFromPassword(pass, 0) + if err != nil { + t.Fatalf("GenerateFromPassword error: %s", err) + } + + if CompareHashAndPassword(hp, pass) != nil { + t.Errorf("%v should hash %s correctly", hp, pass) + } + + notPass := "notthepass" + err = CompareHashAndPassword(hp, []byte(notPass)) + if err != ErrMismatchedHashAndPassword { + t.Errorf("%v and %s should be mismatched", hp, notPass) + } +} + +func TestBcryptingIsCorrect(t *testing.T) { + pass := []byte("allmine") + salt := []byte("XajjQvNhvvRt5GSeFk1xFe") + expectedHash := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga") + + hash, err := bcrypt(pass, 10, salt) + if err != nil { + t.Fatalf("bcrypt blew up: %v", err) + } + if !bytes.HasSuffix(expectedHash, hash) { + t.Errorf("%v should be the suffix of %v", hash, expectedHash) + } + + h, err := newFromHash(expectedHash) + if err != nil { + t.Errorf("Unable to parse %s: %v", string(expectedHash), err) + } + + // This is not the safe way to compare these hashes. We do this only for + // testing clarity. Use bcrypt.CompareHashAndPassword() + if err == nil && !bytes.Equal(expectedHash, h.Hash()) { + t.Errorf("Parsed hash %v should equal %v", h.Hash(), expectedHash) + } +} + +func TestVeryShortPasswords(t *testing.T) { + key := []byte("k") + salt := []byte("XajjQvNhvvRt5GSeFk1xFe") + _, err := bcrypt(key, 10, salt) + if err != nil { + t.Errorf("One byte key resulted in error: %s", err) + } +} + +func TestTooLongPasswordsWork(t *testing.T) { + salt := []byte("XajjQvNhvvRt5GSeFk1xFe") + // One byte over the usual 56 byte limit that blowfish has + tooLongPass := []byte("012345678901234567890123456789012345678901234567890123456") + tooLongExpected := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C") + hash, err := bcrypt(tooLongPass, 10, salt) + if err != nil { + t.Fatalf("bcrypt blew up on long password: %v", err) + } + if !bytes.HasSuffix(tooLongExpected, hash) { + t.Errorf("%v should be the suffix of %v", hash, tooLongExpected) + } +} + +type InvalidHashTest struct { + err error + hash []byte +} + +var invalidTests = []InvalidHashTest{ + {ErrHashTooShort, []byte("$2a$10$fooo")}, + {ErrHashTooShort, []byte("$2a")}, + {HashVersionTooNewError('3'), []byte("$3a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")}, + {InvalidHashPrefixError('%'), []byte("%2a$10$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")}, + {InvalidCostError(32), []byte("$2a$32$sssssssssssssssssssssshhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")}, +} + +func TestInvalidHashErrors(t *testing.T) { + check := func(name string, expected, err error) { + if err == nil { + t.Errorf("%s: Should have returned an error", name) + } + if err != nil && err != expected { + t.Errorf("%s gave err %v but should have given %v", name, err, expected) + } + } + for _, iht := range invalidTests { + _, err := newFromHash(iht.hash) + check("newFromHash", iht.err, err) + err = CompareHashAndPassword(iht.hash, []byte("anything")) + check("CompareHashAndPassword", iht.err, err) + } +} + +func TestUnpaddedBase64Encoding(t *testing.T) { + original := []byte{101, 201, 101, 75, 19, 227, 199, 20, 239, 236, 133, 32, 30, 109, 243, 30} + encodedOriginal := []byte("XajjQvNhvvRt5GSeFk1xFe") + + encoded := base64Encode(original) + + if !bytes.Equal(encodedOriginal, encoded) { + t.Errorf("Encoded %v should have equaled %v", encoded, encodedOriginal) + } + + decoded, err := base64Decode(encodedOriginal) + if err != nil { + t.Fatalf("base64Decode blew up: %s", err) + } + + if !bytes.Equal(decoded, original) { + t.Errorf("Decoded %v should have equaled %v", decoded, original) + } +} + +func TestCost(t *testing.T) { + suffix := "XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C" + for _, vers := range []string{"2a", "2"} { + for _, cost := range []int{4, 10} { + s := fmt.Sprintf("$%s$%02d$%s", vers, cost, suffix) + h := []byte(s) + actual, err := Cost(h) + if err != nil { + t.Errorf("Cost, error: %s", err) + continue + } + if actual != cost { + t.Errorf("Cost, expected: %d, actual: %d", cost, actual) + } + } + } + _, err := Cost([]byte("$a$a$" + suffix)) + if err == nil { + t.Errorf("Cost, malformed but no error returned") + } +} + +func TestCostValidationInHash(t *testing.T) { + if testing.Short() { + return + } + + pass := []byte("mypassword") + + for c := 0; c < MinCost; c++ { + p, _ := newFromPassword(pass, c) + if p.cost != DefaultCost { + t.Errorf("newFromPassword should default costs below %d to %d, but was %d", MinCost, DefaultCost, p.cost) + } + } + + p, _ := newFromPassword(pass, 14) + if p.cost != 14 { + t.Errorf("newFromPassword should default cost to 14, but was %d", p.cost) + } + + hp, _ := newFromHash(p.Hash()) + if p.cost != hp.cost { + t.Errorf("newFromHash should maintain the cost at %d, but was %d", p.cost, hp.cost) + } + + _, err := newFromPassword(pass, 32) + if err == nil { + t.Fatalf("newFromPassword: should return a cost error") + } + if err != InvalidCostError(32) { + t.Errorf("newFromPassword: should return cost error, got %#v", err) + } +} + +func TestCostReturnsWithLeadingZeroes(t *testing.T) { + hp, _ := newFromPassword([]byte("abcdefgh"), 7) + cost := hp.Hash()[4:7] + expected := []byte("07$") + + if !bytes.Equal(expected, cost) { + t.Errorf("single digit costs in hash should have leading zeros: was %v instead of %v", cost, expected) + } +} + +func TestMinorNotRequired(t *testing.T) { + noMinorHash := []byte("$2$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga") + h, err := newFromHash(noMinorHash) + if err != nil { + t.Fatalf("No minor hash blew up: %s", err) + } + if h.minor != 0 { + t.Errorf("Should leave minor version at 0, but was %d", h.minor) + } + + if !bytes.Equal(noMinorHash, h.Hash()) { + t.Errorf("Should generate hash %v, but created %v", noMinorHash, h.Hash()) + } +} + +func BenchmarkEqual(b *testing.B) { + b.StopTimer() + passwd := []byte("somepasswordyoulike") + hash, _ := GenerateFromPassword(passwd, 10) + b.StartTimer() + for i := 0; i < b.N; i++ { + CompareHashAndPassword(hash, passwd) + } +} + +func BenchmarkGeneration(b *testing.B) { + b.StopTimer() + passwd := []byte("mylongpassword1234") + b.StartTimer() + for i := 0; i < b.N; i++ { + GenerateFromPassword(passwd, 10) + } +} diff --git a/Godeps/_workspace/src/golang.org/x/crypto/blowfish/block.go b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/block.go new file mode 100644 index 00000000..9d80f195 --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/block.go @@ -0,0 +1,159 @@ +// Copyright 2010 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 blowfish + +// getNextWord returns the next big-endian uint32 value from the byte slice +// at the given position in a circular manner, updating the position. +func getNextWord(b []byte, pos *int) uint32 { + var w uint32 + j := *pos + for i := 0; i < 4; i++ { + w = w<<8 | uint32(b[j]) + j++ + if j >= len(b) { + j = 0 + } + } + *pos = j + return w +} + +// ExpandKey performs a key expansion on the given *Cipher. Specifically, it +// performs the Blowfish algorithm's key schedule which sets up the *Cipher's +// pi and substitution tables for calls to Encrypt. This is used, primarily, +// by the bcrypt package to reuse the Blowfish key schedule during its +// set up. It's unlikely that you need to use this directly. +func ExpandKey(key []byte, c *Cipher) { + j := 0 + for i := 0; i < 18; i++ { + // Using inlined getNextWord for performance. + var d uint32 + for k := 0; k < 4; k++ { + d = d<<8 | uint32(key[j]) + j++ + if j >= len(key) { + j = 0 + } + } + c.p[i] ^= d + } + + var l, r uint32 + for i := 0; i < 18; i += 2 { + l, r = encryptBlock(l, r, c) + c.p[i], c.p[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s0[i], c.s0[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s1[i], c.s1[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s2[i], c.s2[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s3[i], c.s3[i+1] = l, r + } +} + +// This is similar to ExpandKey, but folds the salt during the key +// schedule. While ExpandKey is essentially expandKeyWithSalt with an all-zero +// salt passed in, reusing ExpandKey turns out to be a place of inefficiency +// and specializing it here is useful. +func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) { + j := 0 + for i := 0; i < 18; i++ { + c.p[i] ^= getNextWord(key, &j) + } + + j = 0 + var l, r uint32 + for i := 0; i < 18; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.p[i], c.p[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s0[i], c.s0[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s1[i], c.s1[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s2[i], c.s2[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s3[i], c.s3[i+1] = l, r + } +} + +func encryptBlock(l, r uint32, c *Cipher) (uint32, uint32) { + xl, xr := l, r + xl ^= c.p[0] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[1] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[2] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[3] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[4] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[5] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[6] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[7] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[8] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[9] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[10] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[11] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[12] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[13] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[14] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[15] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[16] + xr ^= c.p[17] + return xr, xl +} + +func decryptBlock(l, r uint32, c *Cipher) (uint32, uint32) { + xl, xr := l, r + xl ^= c.p[17] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[16] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[15] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[14] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[13] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[12] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[11] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[10] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[9] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[8] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[7] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[6] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[5] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[4] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[3] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[2] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[1] + xr ^= c.p[0] + return xr, xl +} diff --git a/Godeps/_workspace/src/golang.org/x/crypto/blowfish/blowfish_test.go b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/blowfish_test.go new file mode 100644 index 00000000..7afa1fdf --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/blowfish_test.go @@ -0,0 +1,274 @@ +// Copyright 2010 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 blowfish + +import "testing" + +type CryptTest struct { + key []byte + in []byte + out []byte +} + +// Test vector values are from http://www.schneier.com/code/vectors.txt. +var encryptTests = []CryptTest{ + { + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x4E, 0xF9, 0x97, 0x45, 0x61, 0x98, 0xDD, 0x78}}, + { + []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + []byte{0x51, 0x86, 0x6F, 0xD5, 0xB8, 0x5E, 0xCB, 0x8A}}, + { + []byte{0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, + []byte{0x7D, 0x85, 0x6F, 0x9A, 0x61, 0x30, 0x63, 0xF2}}, + { + []byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, + []byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, + []byte{0x24, 0x66, 0xDD, 0x87, 0x8B, 0x96, 0x3C, 0x9D}}, + + { + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, + []byte{0x61, 0xF9, 0xC3, 0x80, 0x22, 0x81, 0xB0, 0x96}}, + { + []byte{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0x7D, 0x0C, 0xC6, 0x30, 0xAF, 0xDA, 0x1E, 0xC7}}, + { + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x4E, 0xF9, 0x97, 0x45, 0x61, 0x98, 0xDD, 0x78}}, + { + []byte{0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}, + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0x0A, 0xCE, 0xAB, 0x0F, 0xC6, 0xA0, 0xA2, 0x8D}}, + { + []byte{0x7C, 0xA1, 0x10, 0x45, 0x4A, 0x1A, 0x6E, 0x57}, + []byte{0x01, 0xA1, 0xD6, 0xD0, 0x39, 0x77, 0x67, 0x42}, + []byte{0x59, 0xC6, 0x82, 0x45, 0xEB, 0x05, 0x28, 0x2B}}, + { + []byte{0x01, 0x31, 0xD9, 0x61, 0x9D, 0xC1, 0x37, 0x6E}, + []byte{0x5C, 0xD5, 0x4C, 0xA8, 0x3D, 0xEF, 0x57, 0xDA}, + []byte{0xB1, 0xB8, 0xCC, 0x0B, 0x25, 0x0F, 0x09, 0xA0}}, + { + []byte{0x07, 0xA1, 0x13, 0x3E, 0x4A, 0x0B, 0x26, 0x86}, + []byte{0x02, 0x48, 0xD4, 0x38, 0x06, 0xF6, 0x71, 0x72}, + []byte{0x17, 0x30, 0xE5, 0x77, 0x8B, 0xEA, 0x1D, 0xA4}}, + { + []byte{0x38, 0x49, 0x67, 0x4C, 0x26, 0x02, 0x31, 0x9E}, + []byte{0x51, 0x45, 0x4B, 0x58, 0x2D, 0xDF, 0x44, 0x0A}, + []byte{0xA2, 0x5E, 0x78, 0x56, 0xCF, 0x26, 0x51, 0xEB}}, + { + []byte{0x04, 0xB9, 0x15, 0xBA, 0x43, 0xFE, 0xB5, 0xB6}, + []byte{0x42, 0xFD, 0x44, 0x30, 0x59, 0x57, 0x7F, 0xA2}, + []byte{0x35, 0x38, 0x82, 0xB1, 0x09, 0xCE, 0x8F, 0x1A}}, + { + []byte{0x01, 0x13, 0xB9, 0x70, 0xFD, 0x34, 0xF2, 0xCE}, + []byte{0x05, 0x9B, 0x5E, 0x08, 0x51, 0xCF, 0x14, 0x3A}, + []byte{0x48, 0xF4, 0xD0, 0x88, 0x4C, 0x37, 0x99, 0x18}}, + { + []byte{0x01, 0x70, 0xF1, 0x75, 0x46, 0x8F, 0xB5, 0xE6}, + []byte{0x07, 0x56, 0xD8, 0xE0, 0x77, 0x47, 0x61, 0xD2}, + []byte{0x43, 0x21, 0x93, 0xB7, 0x89, 0x51, 0xFC, 0x98}}, + { + []byte{0x43, 0x29, 0x7F, 0xAD, 0x38, 0xE3, 0x73, 0xFE}, + []byte{0x76, 0x25, 0x14, 0xB8, 0x29, 0xBF, 0x48, 0x6A}, + []byte{0x13, 0xF0, 0x41, 0x54, 0xD6, 0x9D, 0x1A, 0xE5}}, + { + []byte{0x07, 0xA7, 0x13, 0x70, 0x45, 0xDA, 0x2A, 0x16}, + []byte{0x3B, 0xDD, 0x11, 0x90, 0x49, 0x37, 0x28, 0x02}, + []byte{0x2E, 0xED, 0xDA, 0x93, 0xFF, 0xD3, 0x9C, 0x79}}, + { + []byte{0x04, 0x68, 0x91, 0x04, 0xC2, 0xFD, 0x3B, 0x2F}, + []byte{0x26, 0x95, 0x5F, 0x68, 0x35, 0xAF, 0x60, 0x9A}, + []byte{0xD8, 0x87, 0xE0, 0x39, 0x3C, 0x2D, 0xA6, 0xE3}}, + { + []byte{0x37, 0xD0, 0x6B, 0xB5, 0x16, 0xCB, 0x75, 0x46}, + []byte{0x16, 0x4D, 0x5E, 0x40, 0x4F, 0x27, 0x52, 0x32}, + []byte{0x5F, 0x99, 0xD0, 0x4F, 0x5B, 0x16, 0x39, 0x69}}, + { + []byte{0x1F, 0x08, 0x26, 0x0D, 0x1A, 0xC2, 0x46, 0x5E}, + []byte{0x6B, 0x05, 0x6E, 0x18, 0x75, 0x9F, 0x5C, 0xCA}, + []byte{0x4A, 0x05, 0x7A, 0x3B, 0x24, 0xD3, 0x97, 0x7B}}, + { + []byte{0x58, 0x40, 0x23, 0x64, 0x1A, 0xBA, 0x61, 0x76}, + []byte{0x00, 0x4B, 0xD6, 0xEF, 0x09, 0x17, 0x60, 0x62}, + []byte{0x45, 0x20, 0x31, 0xC1, 0xE4, 0xFA, 0xDA, 0x8E}}, + { + []byte{0x02, 0x58, 0x16, 0x16, 0x46, 0x29, 0xB0, 0x07}, + []byte{0x48, 0x0D, 0x39, 0x00, 0x6E, 0xE7, 0x62, 0xF2}, + []byte{0x75, 0x55, 0xAE, 0x39, 0xF5, 0x9B, 0x87, 0xBD}}, + { + []byte{0x49, 0x79, 0x3E, 0xBC, 0x79, 0xB3, 0x25, 0x8F}, + []byte{0x43, 0x75, 0x40, 0xC8, 0x69, 0x8F, 0x3C, 0xFA}, + []byte{0x53, 0xC5, 0x5F, 0x9C, 0xB4, 0x9F, 0xC0, 0x19}}, + { + []byte{0x4F, 0xB0, 0x5E, 0x15, 0x15, 0xAB, 0x73, 0xA7}, + []byte{0x07, 0x2D, 0x43, 0xA0, 0x77, 0x07, 0x52, 0x92}, + []byte{0x7A, 0x8E, 0x7B, 0xFA, 0x93, 0x7E, 0x89, 0xA3}}, + { + []byte{0x49, 0xE9, 0x5D, 0x6D, 0x4C, 0xA2, 0x29, 0xBF}, + []byte{0x02, 0xFE, 0x55, 0x77, 0x81, 0x17, 0xF1, 0x2A}, + []byte{0xCF, 0x9C, 0x5D, 0x7A, 0x49, 0x86, 0xAD, 0xB5}}, + { + []byte{0x01, 0x83, 0x10, 0xDC, 0x40, 0x9B, 0x26, 0xD6}, + []byte{0x1D, 0x9D, 0x5C, 0x50, 0x18, 0xF7, 0x28, 0xC2}, + []byte{0xD1, 0xAB, 0xB2, 0x90, 0x65, 0x8B, 0xC7, 0x78}}, + { + []byte{0x1C, 0x58, 0x7F, 0x1C, 0x13, 0x92, 0x4F, 0xEF}, + []byte{0x30, 0x55, 0x32, 0x28, 0x6D, 0x6F, 0x29, 0x5A}, + []byte{0x55, 0xCB, 0x37, 0x74, 0xD1, 0x3E, 0xF2, 0x01}}, + { + []byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}, + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0xFA, 0x34, 0xEC, 0x48, 0x47, 0xB2, 0x68, 0xB2}}, + { + []byte{0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x0E, 0x0E, 0x0E}, + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0xA7, 0x90, 0x79, 0x51, 0x08, 0xEA, 0x3C, 0xAE}}, + { + []byte{0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1, 0xFE}, + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0xC3, 0x9E, 0x07, 0x2D, 0x9F, 0xAC, 0x63, 0x1D}}, + { + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + []byte{0x01, 0x49, 0x33, 0xE0, 0xCD, 0xAF, 0xF6, 0xE4}}, + { + []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0xF2, 0x1E, 0x9A, 0x77, 0xB7, 0x1C, 0x49, 0xBC}}, + { + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0x24, 0x59, 0x46, 0x88, 0x57, 0x54, 0x36, 0x9A}}, + { + []byte{0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}, + []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + []byte{0x6B, 0x5C, 0x5A, 0x9C, 0x5D, 0x9E, 0x0A, 0x5A}}, +} + +func TestCipherEncrypt(t *testing.T) { + for i, tt := range encryptTests { + c, err := NewCipher(tt.key) + if err != nil { + t.Errorf("NewCipher(%d bytes) = %s", len(tt.key), err) + continue + } + ct := make([]byte, len(tt.out)) + c.Encrypt(ct, tt.in) + for j, v := range ct { + if v != tt.out[j] { + t.Errorf("Cipher.Encrypt, test vector #%d: cipher-text[%d] = %#x, expected %#x", i, j, v, tt.out[j]) + break + } + } + } +} + +func TestCipherDecrypt(t *testing.T) { + for i, tt := range encryptTests { + c, err := NewCipher(tt.key) + if err != nil { + t.Errorf("NewCipher(%d bytes) = %s", len(tt.key), err) + continue + } + pt := make([]byte, len(tt.in)) + c.Decrypt(pt, tt.out) + for j, v := range pt { + if v != tt.in[j] { + t.Errorf("Cipher.Decrypt, test vector #%d: plain-text[%d] = %#x, expected %#x", i, j, v, tt.in[j]) + break + } + } + } +} + +func TestSaltedCipherKeyLength(t *testing.T) { + if _, err := NewSaltedCipher(nil, []byte{'a'}); err != KeySizeError(0) { + t.Errorf("NewSaltedCipher with short key, gave error %#v, expected %#v", err, KeySizeError(0)) + } + + // A 57-byte key. One over the typical blowfish restriction. + key := []byte("012345678901234567890123456789012345678901234567890123456") + if _, err := NewSaltedCipher(key, []byte{'a'}); err != nil { + t.Errorf("NewSaltedCipher with long key, gave error %#v", err) + } +} + +// Test vectors generated with Blowfish from OpenSSH. +var saltedVectors = [][8]byte{ + {0x0c, 0x82, 0x3b, 0x7b, 0x8d, 0x01, 0x4b, 0x7e}, + {0xd1, 0xe1, 0x93, 0xf0, 0x70, 0xa6, 0xdb, 0x12}, + {0xfc, 0x5e, 0xba, 0xde, 0xcb, 0xf8, 0x59, 0xad}, + {0x8a, 0x0c, 0x76, 0xe7, 0xdd, 0x2c, 0xd3, 0xa8}, + {0x2c, 0xcb, 0x7b, 0xee, 0xac, 0x7b, 0x7f, 0xf8}, + {0xbb, 0xf6, 0x30, 0x6f, 0xe1, 0x5d, 0x62, 0xbf}, + {0x97, 0x1e, 0xc1, 0x3d, 0x3d, 0xe0, 0x11, 0xe9}, + {0x06, 0xd7, 0x4d, 0xb1, 0x80, 0xa3, 0xb1, 0x38}, + {0x67, 0xa1, 0xa9, 0x75, 0x0e, 0x5b, 0xc6, 0xb4}, + {0x51, 0x0f, 0x33, 0x0e, 0x4f, 0x67, 0xd2, 0x0c}, + {0xf1, 0x73, 0x7e, 0xd8, 0x44, 0xea, 0xdb, 0xe5}, + {0x14, 0x0e, 0x16, 0xce, 0x7f, 0x4a, 0x9c, 0x7b}, + {0x4b, 0xfe, 0x43, 0xfd, 0xbf, 0x36, 0x04, 0x47}, + {0xb1, 0xeb, 0x3e, 0x15, 0x36, 0xa7, 0xbb, 0xe2}, + {0x6d, 0x0b, 0x41, 0xdd, 0x00, 0x98, 0x0b, 0x19}, + {0xd3, 0xce, 0x45, 0xce, 0x1d, 0x56, 0xb7, 0xfc}, + {0xd9, 0xf0, 0xfd, 0xda, 0xc0, 0x23, 0xb7, 0x93}, + {0x4c, 0x6f, 0xa1, 0xe4, 0x0c, 0xa8, 0xca, 0x57}, + {0xe6, 0x2f, 0x28, 0xa7, 0x0c, 0x94, 0x0d, 0x08}, + {0x8f, 0xe3, 0xf0, 0xb6, 0x29, 0xe3, 0x44, 0x03}, + {0xff, 0x98, 0xdd, 0x04, 0x45, 0xb4, 0x6d, 0x1f}, + {0x9e, 0x45, 0x4d, 0x18, 0x40, 0x53, 0xdb, 0xef}, + {0xb7, 0x3b, 0xef, 0x29, 0xbe, 0xa8, 0x13, 0x71}, + {0x02, 0x54, 0x55, 0x41, 0x8e, 0x04, 0xfc, 0xad}, + {0x6a, 0x0a, 0xee, 0x7c, 0x10, 0xd9, 0x19, 0xfe}, + {0x0a, 0x22, 0xd9, 0x41, 0xcc, 0x23, 0x87, 0x13}, + {0x6e, 0xff, 0x1f, 0xff, 0x36, 0x17, 0x9c, 0xbe}, + {0x79, 0xad, 0xb7, 0x40, 0xf4, 0x9f, 0x51, 0xa6}, + {0x97, 0x81, 0x99, 0xa4, 0xde, 0x9e, 0x9f, 0xb6}, + {0x12, 0x19, 0x7a, 0x28, 0xd0, 0xdc, 0xcc, 0x92}, + {0x81, 0xda, 0x60, 0x1e, 0x0e, 0xdd, 0x65, 0x56}, + {0x7d, 0x76, 0x20, 0xb2, 0x73, 0xc9, 0x9e, 0xee}, +} + +func TestSaltedCipher(t *testing.T) { + var key, salt [32]byte + for i := range key { + key[i] = byte(i) + salt[i] = byte(i + 32) + } + for i, v := range saltedVectors { + c, err := NewSaltedCipher(key[:], salt[:i]) + if err != nil { + t.Fatal(err) + } + var buf [8]byte + c.Encrypt(buf[:], buf[:]) + if v != buf { + t.Errorf("%d: expected %x, got %x", i, v, buf) + } + } +} + +func BenchmarkExpandKeyWithSalt(b *testing.B) { + key := make([]byte, 32) + salt := make([]byte, 16) + c, _ := NewCipher(key) + for i := 0; i < b.N; i++ { + expandKeyWithSalt(key, salt, c) + } +} + +func BenchmarkExpandKey(b *testing.B) { + key := make([]byte, 32) + c, _ := NewCipher(key) + for i := 0; i < b.N; i++ { + ExpandKey(key, c) + } +} diff --git a/Godeps/_workspace/src/golang.org/x/crypto/blowfish/cipher.go b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/cipher.go new file mode 100644 index 00000000..5019658a --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/cipher.go @@ -0,0 +1,91 @@ +// Copyright 2010 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 blowfish implements Bruce Schneier's Blowfish encryption algorithm. +package blowfish + +// The code is a port of Bruce Schneier's C implementation. +// See http://www.schneier.com/blowfish.html. + +import "strconv" + +// The Blowfish block size in bytes. +const BlockSize = 8 + +// A Cipher is an instance of Blowfish encryption using a particular key. +type Cipher struct { + p [18]uint32 + s0, s1, s2, s3 [256]uint32 +} + +type KeySizeError int + +func (k KeySizeError) Error() string { + return "crypto/blowfish: invalid key size " + strconv.Itoa(int(k)) +} + +// NewCipher creates and returns a Cipher. +// The key argument should be the Blowfish key, from 1 to 56 bytes. +func NewCipher(key []byte) (*Cipher, error) { + var result Cipher + if k := len(key); k < 1 || k > 56 { + return nil, KeySizeError(k) + } + initCipher(&result) + ExpandKey(key, &result) + return &result, nil +} + +// NewSaltedCipher creates a returns a Cipher that folds a salt into its key +// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is +// sufficient and desirable. For bcrypt compatiblity, the key can be over 56 +// bytes. +func NewSaltedCipher(key, salt []byte) (*Cipher, error) { + if len(salt) == 0 { + return NewCipher(key) + } + var result Cipher + if k := len(key); k < 1 { + return nil, KeySizeError(k) + } + initCipher(&result) + expandKeyWithSalt(key, salt, &result) + return &result, nil +} + +// BlockSize returns the Blowfish block size, 8 bytes. +// It is necessary to satisfy the Block interface in the +// package "crypto/cipher". +func (c *Cipher) BlockSize() int { return BlockSize } + +// Encrypt encrypts the 8-byte buffer src using the key k +// and stores the result in dst. +// Note that for amounts of data larger than a block, +// it is not safe to just call Encrypt on successive blocks; +// instead, use an encryption mode like CBC (see crypto/cipher/cbc.go). +func (c *Cipher) Encrypt(dst, src []byte) { + l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3]) + r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7]) + l, r = encryptBlock(l, r, c) + dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l) + dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r) +} + +// Decrypt decrypts the 8-byte buffer src using the key k +// and stores the result in dst. +func (c *Cipher) Decrypt(dst, src []byte) { + l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3]) + r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7]) + l, r = decryptBlock(l, r, c) + dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l) + dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r) +} + +func initCipher(c *Cipher) { + copy(c.p[0:], p[0:]) + copy(c.s0[0:], s0[0:]) + copy(c.s1[0:], s1[0:]) + copy(c.s2[0:], s2[0:]) + copy(c.s3[0:], s3[0:]) +} diff --git a/Godeps/_workspace/src/golang.org/x/crypto/blowfish/const.go b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/const.go new file mode 100644 index 00000000..8c5ee4cb --- /dev/null +++ b/Godeps/_workspace/src/golang.org/x/crypto/blowfish/const.go @@ -0,0 +1,199 @@ +// Copyright 2010 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. + +// The startup permutation array and substitution boxes. +// They are the hexadecimal digits of PI; see: +// http://www.schneier.com/code/constants.txt. + +package blowfish + +var s0 = [256]uint32{ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, +} + +var s1 = [256]uint32{ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, +} + +var s2 = [256]uint32{ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, +} + +var s3 = [256]uint32{ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, +} + +var p = [18]uint32{ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b, +} From e4c3ab43773fbd8bb9517065ddec8fd5b3c570e1 Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Sat, 6 Jun 2015 01:37:32 -0400 Subject: [PATCH 07/13] Removed dashes from comments, unexported htpasswd struct Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 9 +++------ registry/auth/basic/htpasswd.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 0b3e2788..52b790d2 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -2,9 +2,6 @@ // user credential hash in an htpasswd formatted file in a configuration-determined // location. // -// The use of SHA hashes (htpasswd -s) is enforced since MD5 is insecure and simple -// system crypt() may be as well. -// // This authentication method MUST be used under TLS, as simple token-replay attack is possible. package basic @@ -20,7 +17,7 @@ import ( type accessController struct { realm string - htpasswd *HTPasswd + htpasswd *htpasswd } type challenge struct { @@ -30,9 +27,9 @@ type challenge struct { var _ auth.AccessController = &accessController{} var ( - // ErrPasswordRequired - returned when no auth token is given. + // ErrPasswordRequired Returned when no auth token is given. ErrPasswordRequired = errors.New("authorization credential required") - // ErrInvalidCredential - returned when the auth token does not authenticate correctly. + // ErrInvalidCredential is returned when the auth token does not authenticate correctly. ErrInvalidCredential = errors.New("invalid authorization credential") ) diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 89e4b749..91d45e77 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -12,32 +12,32 @@ import ( "golang.org/x/crypto/bcrypt" ) -// AuthenticationFailureErr - a generic error message for authentication failure to be presented to agent. +// ErrAuthenticationFailure A generic error message for authentication failure to be presented to agent. var ErrAuthenticationFailure = errors.New("Bad username or password") -// HTPasswd - holds a path to a system .htpasswd file and the machinery to parse it. -type HTPasswd struct { +// htpasswd Holds a path to a system .htpasswd file and the machinery to parse it. +type htpasswd struct { path string reader *csv.Reader } -// AuthType represents a particular hash function used in the htpasswd file. +// AuthType Represents a particular hash function used in the htpasswd file. type AuthType int const ( - // PlainText - Plain-text password storage (htpasswd -p) + // PlainText Plain-text password storage (htpasswd -p) PlainText AuthType = iota - // SHA1 - sha hashed password storage (htpasswd -s) + // SHA1 sha hashed password storage (htpasswd -s) SHA1 - // ApacheMD5 - apr iterated md5 hashing (htpasswd -m) + // ApacheMD5 apr iterated md5 hashing (htpasswd -m) ApacheMD5 - // BCrypt - BCrypt adapative password hashing (htpasswd -B) + // BCrypt BCrypt adapative password hashing (htpasswd -B) BCrypt - // Crypt - System crypt() hashes. (htpasswd -d) + // Crypt System crypt() hashes. (htpasswd -d) Crypt ) -// String returns a text representation of the AuthType +// String Returns a text representation of the AuthType func (at AuthType) String() string { switch at { case PlainText: @@ -54,14 +54,14 @@ func (at AuthType) String() string { return "unknown" } -// NewHTPasswd - Create a new HTPasswd with the given path to .htpasswd file. -func NewHTPasswd(htpath string) *HTPasswd { - return &HTPasswd{path: htpath} +// NewHTPasswd Create a new HTPasswd with the given path to .htpasswd file. +func NewHTPasswd(htpath string) *htpasswd { + return &htpasswd{path: htpath} } var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`) -// GetAuthCredentialType - Inspect an htpasswd file credential and guess the encryption algorithm used. +// GetAuthCredentialType Inspect an htpasswd file credential and guess the encryption algorithm used. func GetAuthCredentialType(cred string) AuthType { if strings.HasPrefix(cred, "{SHA}") { return SHA1 @@ -79,8 +79,8 @@ func GetAuthCredentialType(cred string) AuthType { return PlainText } -// AuthenticateUser - Check a given user:password credential against the receiving HTPasswd's file. -func (htpasswd *HTPasswd) AuthenticateUser(user string, pwd string) (bool, error) { +// AuthenticateUser Check a given user:password credential against the receiving HTPasswd's file. +func (htpasswd *htpasswd) AuthenticateUser(user string, pwd string) (bool, error) { // Open the file. in, err := os.Open(htpasswd.path) From abd142855a89ffababeb67c774cd4e2e46789e58 Mon Sep 17 00:00:00 2001 From: Dave Trombley Date: Sat, 6 Jun 2015 01:58:45 -0400 Subject: [PATCH 08/13] Unexported function to comply with golint Signed-off-by: Dave Trombley --- registry/auth/basic/access.go | 2 +- registry/auth/basic/htpasswd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 52b790d2..24f4009f 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -44,7 +44,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController, return nil, fmt.Errorf(`"path" must be set for basic access controller`) } - return &accessController{realm: realm.(string), htpasswd: NewHTPasswd(path.(string))}, nil + return &accessController{realm: realm.(string), htpasswd: newHTPasswd(path.(string))}, nil } func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index 91d45e77..cc305ff1 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -55,7 +55,7 @@ func (at AuthType) String() string { } // NewHTPasswd Create a new HTPasswd with the given path to .htpasswd file. -func NewHTPasswd(htpath string) *htpasswd { +func newHTPasswd(htpath string) *htpasswd { return &htpasswd{path: htpath} } From ffe56ebe415684e1099450fd444ebaae6cd81be9 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 8 Jun 2015 18:56:48 -0700 Subject: [PATCH 09/13] Refactor Basic Authentication package This change refactors the basic authentication implementation to better follow Go coding standards. Many types are no longer exported. The parser is now a separate function from the authentication code. The standard functions (*http.Request).BasicAuth/SetBasicAuth are now used where appropriate. Signed-off-by: Stephen J Day --- cmd/registry/config.yml | 4 - registry/auth/basic/access.go | 54 ++++---- registry/auth/basic/access_test.go | 19 +-- registry/auth/basic/htpasswd.go | 203 +++++++++++++++-------------- 4 files changed, 142 insertions(+), 138 deletions(-) diff --git a/cmd/registry/config.yml b/cmd/registry/config.yml index abc868d9..6d41cc8f 100644 --- a/cmd/registry/config.yml +++ b/cmd/registry/config.yml @@ -26,10 +26,6 @@ storage: maintenance: uploadpurging: enabled: false -auth: - basic: - realm: test-realm - path: /tmp/registry-dev/.htpasswd http: addr: :5000 secret: asecretforlocaldevelopment diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 24f4009f..11e4ae5a 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -15,23 +15,20 @@ import ( "golang.org/x/net/context" ) +var ( + // ErrInvalidCredential is returned when the auth token does not authenticate correctly. + ErrInvalidCredential = errors.New("invalid authorization credential") + + // ErrAuthenticationFailure returned when authentication failure to be presented to agent. + ErrAuthenticationFailure = errors.New("authentication failured") +) + type accessController struct { realm string htpasswd *htpasswd } -type challenge struct { - realm string - err error -} - var _ auth.AccessController = &accessController{} -var ( - // ErrPasswordRequired Returned when no auth token is given. - ErrPasswordRequired = errors.New("authorization credential required") - // ErrInvalidCredential is returned when the auth token does not authenticate correctly. - ErrInvalidCredential = errors.New("invalid authorization credential") -) func newAccessController(options map[string]interface{}) (auth.AccessController, error) { realm, present := options["realm"] @@ -53,28 +50,29 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut return nil, err } - authHeader := req.Header.Get("Authorization") - if authHeader == "" { - challenge := challenge{ - realm: ac.realm, - } - return nil, &challenge - } - - user, pass, ok := req.BasicAuth() + username, password, ok := req.BasicAuth() if !ok { - return nil, errors.New("Invalid Authorization header") - } - - if res, _ := ac.htpasswd.AuthenticateUser(user, pass); !res { - challenge := challenge{ + return nil, &challenge{ realm: ac.realm, + err: ErrInvalidCredential, } - challenge.err = ErrInvalidCredential - return nil, &challenge } - return auth.WithUser(ctx, auth.UserInfo{Name: user}), nil + if err := ac.htpasswd.authenticateUser(ctx, username, password); err != nil { + ctxu.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err) + return nil, &challenge{ + realm: ac.realm, + err: ErrAuthenticationFailure, + } + } + + return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil +} + +// challenge implements the auth.Challenge interface. +type challenge struct { + realm string + err error } func (ch *challenge) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go index 62699a63..3bc99437 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/basic/access_test.go @@ -1,14 +1,13 @@ package basic import ( - "encoding/base64" "io/ioutil" "net/http" "net/http/httptest" "testing" + "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" - "golang.org/x/net/context" ) func TestBasicAccessController(t *testing.T) { @@ -33,6 +32,7 @@ func TestBasicAccessController(t *testing.T) { "realm": testRealm, "path": tempFile.Name(), } + ctx := context.Background() accessController, err := newAccessController(options) if err != nil { @@ -44,7 +44,7 @@ func TestBasicAccessController(t *testing.T) { var userNumber = 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(nil, "http.request", r) + ctx := context.WithRequest(ctx, r) authCtx, err := accessController.Authorized(ctx) if err != nil { switch err := err.(type) { @@ -87,13 +87,14 @@ func TestBasicAccessController(t *testing.T) { for i := 0; i < len(testUsers); i++ { userNumber = i - req, _ = http.NewRequest("GET", server.URL, nil) - sekrit := testUsers[i] + ":" + testPasswords[i] - credential := "Basic " + base64.StdEncoding.EncodeToString([]byte(sekrit)) + req, err := http.NewRequest("GET", server.URL, nil) + if err != nil { + t.Fatalf("error allocating new request: %v", err) + } + + req.SetBasicAuth(testUsers[i], testPasswords[i]) - req.Header.Set("Authorization", credential) resp, err = client.Do(req) - if err != nil { t.Fatalf("unexpected error during GET: %v", err) } @@ -101,7 +102,7 @@ func TestBasicAccessController(t *testing.T) { // Request should be authorized if resp.StatusCode != http.StatusNoContent { - t.Fatalf("unexpected non-success response status: %v != %v for %s %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i], credential) + t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i]) } } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index cc305ff1..f50805e7 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -1,54 +1,66 @@ package basic import ( + "bufio" "crypto/sha1" "encoding/base64" - "encoding/csv" - "errors" + "io" "os" "regexp" "strings" + "github.com/docker/distribution/context" "golang.org/x/crypto/bcrypt" ) -// ErrAuthenticationFailure A generic error message for authentication failure to be presented to agent. -var ErrAuthenticationFailure = errors.New("Bad username or password") - -// htpasswd Holds a path to a system .htpasswd file and the machinery to parse it. +// htpasswd holds a path to a system .htpasswd file and the machinery to parse it. type htpasswd struct { - path string - reader *csv.Reader + path string } -// AuthType Represents a particular hash function used in the htpasswd file. -type AuthType int +// authType represents a particular hash function used in the htpasswd file. +type authType int const ( - // PlainText Plain-text password storage (htpasswd -p) - PlainText AuthType = iota - // SHA1 sha hashed password storage (htpasswd -s) - SHA1 - // ApacheMD5 apr iterated md5 hashing (htpasswd -m) - ApacheMD5 - // BCrypt BCrypt adapative password hashing (htpasswd -B) - BCrypt - // Crypt System crypt() hashes. (htpasswd -d) - Crypt + authTypePlainText authType = iota // Plain-text password storage (htpasswd -p) + authTypeSHA1 // sha hashed password storage (htpasswd -s) + authTypeApacheMD5 // apr iterated md5 hashing (htpasswd -m) + authTypeBCrypt // BCrypt adapative password hashing (htpasswd -B) + authTypeCrypt // System crypt() hashes. (htpasswd -d) ) +var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`) + +// detectAuthCredentialType inspects the credential and resolves the encryption scheme. +func detectAuthCredentialType(cred string) authType { + if strings.HasPrefix(cred, "{SHA}") { + return authTypeSHA1 + } + if strings.HasPrefix(cred, "$apr1$") { + return authTypeApacheMD5 + } + if bcryptPrefixRegexp.MatchString(cred) { + return authTypeBCrypt + } + // There's just not a great way to distinguish between these next two... + if len(cred) == 13 { + return authTypeCrypt + } + return authTypePlainText +} + // String Returns a text representation of the AuthType -func (at AuthType) String() string { +func (at authType) String() string { switch at { - case PlainText: + case authTypePlainText: return "plaintext" - case SHA1: + case authTypeSHA1: return "sha1" - case ApacheMD5: + case authTypeApacheMD5: return "md5" - case BCrypt: + case authTypeBCrypt: return "bcrypt" - case Crypt: + case authTypeCrypt: return "system crypt" } return "unknown" @@ -59,83 +71,80 @@ func newHTPasswd(htpath string) *htpasswd { return &htpasswd{path: htpath} } -var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`) - -// GetAuthCredentialType Inspect an htpasswd file credential and guess the encryption algorithm used. -func GetAuthCredentialType(cred string) AuthType { - if strings.HasPrefix(cred, "{SHA}") { - return SHA1 - } - if strings.HasPrefix(cred, "$apr1$") { - return ApacheMD5 - } - if bcryptPrefixRegexp.MatchString(cred) { - return BCrypt - } - // There's just not a great way to distinguish between these next two... - if len(cred) == 13 { - return Crypt - } - return PlainText -} - -// AuthenticateUser Check a given user:password credential against the receiving HTPasswd's file. -func (htpasswd *htpasswd) AuthenticateUser(user string, pwd string) (bool, error) { - +// AuthenticateUser checks a given user:password credential against the +// receiving HTPasswd's file. If the check passes, nil is returned. Note that +// this parses the htpasswd file on each request so ensure that updates are +// available. +func (htpasswd *htpasswd) authenticateUser(ctx context.Context, username string, password string) error { // Open the file. in, err := os.Open(htpasswd.path) if err != nil { - return false, err + return err + } + defer in.Close() + + for _, entry := range parseHTPasswd(ctx, in) { + if entry.username != username { + continue // wrong entry + } + + switch t := detectAuthCredentialType(entry.password); t { + case authTypeSHA1: + sha := sha1.New() + sha.Write([]byte(password)) + hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) + + if entry.password[5:] != hash { + return ErrAuthenticationFailure + } + + return nil + case authTypeBCrypt: + err := bcrypt.CompareHashAndPassword([]byte(entry.password), []byte(password)) + if err != nil { + return ErrAuthenticationFailure + } + + return nil + case authTypePlainText: + if password != entry.password { + return ErrAuthenticationFailure + } + + return nil + default: + context.GetLogger(ctx).Errorf("unsupported basic authentication type: %v", t) + } } - // Parse the contents of the standard .htpasswd until we hit the end or find a match. - reader := csv.NewReader(in) - reader.Comma = ':' - reader.Comment = '#' - reader.TrimLeadingSpace = true - for entry, readerr := reader.Read(); entry != nil || readerr != nil; entry, readerr = reader.Read() { - if readerr != nil { - return false, readerr - } - if len(entry) == 0 { + return ErrAuthenticationFailure +} + +// htpasswdEntry represents a line in an htpasswd file. +type htpasswdEntry struct { + username string // username, plain text + password string // stores hashed passwd +} + +// parseHTPasswd parses the contents of htpasswd. Bad entries are skipped and +// logged, so this may return empty. This will read all the entries in the +// file, whether or not they are needed. +func parseHTPasswd(ctx context.Context, rd io.Reader) []htpasswdEntry { + entries := []htpasswdEntry{} + scanner := bufio.NewScanner(rd) + for scanner.Scan() { + t := strings.TrimSpace(scanner.Text()) + i := strings.Index(t, ":") + if i < 0 || i >= len(t) { + context.GetLogger(ctx).Errorf("bad entry in htpasswd: %q", t) continue } - if entry[0] == user { - credential := entry[1] - credType := GetAuthCredentialType(credential) - switch credType { - case SHA1: - { - sha := sha1.New() - sha.Write([]byte(pwd)) - hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) - return entry[1][5:] == hash, nil - } - case ApacheMD5: - { - return false, errors.New(ApacheMD5.String() + " htpasswd hash function not yet supported") - } - case BCrypt: - { - err := bcrypt.CompareHashAndPassword([]byte(credential), []byte(pwd)) - if err != nil { - return false, err - } - return true, nil - } - case Crypt: - { - return false, errors.New(Crypt.String() + " htpasswd hash function not yet supported") - } - case PlainText: - { - if pwd == credential { - return true, nil - } - return false, ErrAuthenticationFailure - } - } - } + + entries = append(entries, htpasswdEntry{ + username: t[:i], + password: t[i+1:], + }) } - return false, ErrAuthenticationFailure + + return entries } From ffd36629821be22da7a468d57958e2ca70f256e8 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 10 Jun 2015 19:29:27 -0700 Subject: [PATCH 10/13] Harden basic auth implementation After consideration, the basic authentication implementation has been simplified to only support bcrypt entries in an htpasswd file. This greatly increases the security of the implementation by reducing the possibility of timing attacks and other problems trying to detect the password hash type. Also, the htpasswd file is only parsed at startup, ensuring that the file can be edited and not effect ongoing requests. Newly added passwords take effect on restart. Subsequently, password hash entries are now stored in a map. Test cases have been modified accordingly. Signed-off-by: Stephen J Day --- registry/auth/basic/access.go | 16 ++- registry/auth/basic/access_test.go | 20 +++- registry/auth/basic/htpasswd.go | 166 ++++++++------------------- registry/auth/basic/htpasswd_test.go | 85 ++++++++++++++ registry/handlers/app.go | 1 + 5 files changed, 164 insertions(+), 124 deletions(-) create mode 100644 registry/auth/basic/htpasswd_test.go diff --git a/registry/auth/basic/access.go b/registry/auth/basic/access.go index 11e4ae5a..f7d5e79b 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/basic/access.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/http" + "os" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" @@ -41,7 +42,18 @@ func newAccessController(options map[string]interface{}) (auth.AccessController, return nil, fmt.Errorf(`"path" must be set for basic access controller`) } - return &accessController{realm: realm.(string), htpasswd: newHTPasswd(path.(string))}, nil + f, err := os.Open(path.(string)) + if err != nil { + return nil, err + } + defer f.Close() + + h, err := newHTPasswd(f) + if err != nil { + return nil, err + } + + return &accessController{realm: realm.(string), htpasswd: h}, nil } func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { @@ -58,7 +70,7 @@ func (ac *accessController) Authorized(ctx context.Context, accessRecords ...aut } } - if err := ac.htpasswd.authenticateUser(ctx, username, password); err != nil { + if err := ac.htpasswd.authenticateUser(username, password); err != nil { ctxu.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err) return nil, &challenge{ realm: ac.realm, diff --git a/registry/auth/basic/access_test.go b/registry/auth/basic/access_test.go index 3bc99437..1976b32e 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/basic/access_test.go @@ -11,7 +11,6 @@ import ( ) func TestBasicAccessController(t *testing.T) { - testRealm := "The-Shire" testUsers := []string{"bilbo", "frodo", "MiShil", "DeokMan"} testPasswords := []string{"baggins", "baggins", "새주", "공주님"} @@ -85,6 +84,11 @@ func TestBasicAccessController(t *testing.T) { t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } + nonbcrypt := map[string]struct{}{ + "bilbo": struct{}{}, + "DeokMan": struct{}{}, + } + for i := 0; i < len(testUsers); i++ { userNumber = i req, err := http.NewRequest("GET", server.URL, nil) @@ -100,9 +104,17 @@ func TestBasicAccessController(t *testing.T) { } defer resp.Body.Close() - // Request should be authorized - if resp.StatusCode != http.StatusNoContent { - t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i]) + if _, ok := nonbcrypt[testUsers[i]]; ok { + // these are not allowed. + // Request should be authorized + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusUnauthorized, testUsers[i], testPasswords[i]) + } + } else { + // Request should be authorized + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i]) + } } } diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/basic/htpasswd.go index f50805e7..dd9bb1ac 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/basic/htpasswd.go @@ -2,149 +2,79 @@ package basic import ( "bufio" - "crypto/sha1" - "encoding/base64" + "fmt" "io" - "os" - "regexp" "strings" - "github.com/docker/distribution/context" "golang.org/x/crypto/bcrypt" ) -// htpasswd holds a path to a system .htpasswd file and the machinery to parse it. +// htpasswd holds a path to a system .htpasswd file and the machinery to parse +// it. Only bcrypt hash entries are supported. type htpasswd struct { - path string + entries map[string][]byte // maps username to password byte slice. } -// authType represents a particular hash function used in the htpasswd file. -type authType int - -const ( - authTypePlainText authType = iota // Plain-text password storage (htpasswd -p) - authTypeSHA1 // sha hashed password storage (htpasswd -s) - authTypeApacheMD5 // apr iterated md5 hashing (htpasswd -m) - authTypeBCrypt // BCrypt adapative password hashing (htpasswd -B) - authTypeCrypt // System crypt() hashes. (htpasswd -d) -) - -var bcryptPrefixRegexp = regexp.MustCompile(`^\$2[ab]?y\$`) - -// detectAuthCredentialType inspects the credential and resolves the encryption scheme. -func detectAuthCredentialType(cred string) authType { - if strings.HasPrefix(cred, "{SHA}") { - return authTypeSHA1 +// newHTPasswd parses the reader and returns an htpasswd or an error. +func newHTPasswd(rd io.Reader) (*htpasswd, error) { + entries, err := parseHTPasswd(rd) + if err != nil { + return nil, err } - if strings.HasPrefix(cred, "$apr1$") { - return authTypeApacheMD5 - } - if bcryptPrefixRegexp.MatchString(cred) { - return authTypeBCrypt - } - // There's just not a great way to distinguish between these next two... - if len(cred) == 13 { - return authTypeCrypt - } - return authTypePlainText -} -// String Returns a text representation of the AuthType -func (at authType) String() string { - switch at { - case authTypePlainText: - return "plaintext" - case authTypeSHA1: - return "sha1" - case authTypeApacheMD5: - return "md5" - case authTypeBCrypt: - return "bcrypt" - case authTypeCrypt: - return "system crypt" - } - return "unknown" -} - -// NewHTPasswd Create a new HTPasswd with the given path to .htpasswd file. -func newHTPasswd(htpath string) *htpasswd { - return &htpasswd{path: htpath} + return &htpasswd{entries: entries}, nil } // AuthenticateUser checks a given user:password credential against the -// receiving HTPasswd's file. If the check passes, nil is returned. Note that -// this parses the htpasswd file on each request so ensure that updates are -// available. -func (htpasswd *htpasswd) authenticateUser(ctx context.Context, username string, password string) error { - // Open the file. - in, err := os.Open(htpasswd.path) +// receiving HTPasswd's file. If the check passes, nil is returned. +func (htpasswd *htpasswd) authenticateUser(username string, password string) error { + credentials, ok := htpasswd.entries[username] + if !ok { + // timing attack paranoia + bcrypt.CompareHashAndPassword([]byte{}, []byte(password)) + + return ErrAuthenticationFailure + } + + err := bcrypt.CompareHashAndPassword([]byte(credentials), []byte(password)) if err != nil { - return err - } - defer in.Close() - - for _, entry := range parseHTPasswd(ctx, in) { - if entry.username != username { - continue // wrong entry - } - - switch t := detectAuthCredentialType(entry.password); t { - case authTypeSHA1: - sha := sha1.New() - sha.Write([]byte(password)) - hash := base64.StdEncoding.EncodeToString(sha.Sum(nil)) - - if entry.password[5:] != hash { - return ErrAuthenticationFailure - } - - return nil - case authTypeBCrypt: - err := bcrypt.CompareHashAndPassword([]byte(entry.password), []byte(password)) - if err != nil { - return ErrAuthenticationFailure - } - - return nil - case authTypePlainText: - if password != entry.password { - return ErrAuthenticationFailure - } - - return nil - default: - context.GetLogger(ctx).Errorf("unsupported basic authentication type: %v", t) - } + return ErrAuthenticationFailure } - return ErrAuthenticationFailure + return nil } -// htpasswdEntry represents a line in an htpasswd file. -type htpasswdEntry struct { - username string // username, plain text - password string // stores hashed passwd -} - -// parseHTPasswd parses the contents of htpasswd. Bad entries are skipped and -// logged, so this may return empty. This will read all the entries in the -// file, whether or not they are needed. -func parseHTPasswd(ctx context.Context, rd io.Reader) []htpasswdEntry { - entries := []htpasswdEntry{} +// parseHTPasswd parses the contents of htpasswd. This will read all the +// entries in the file, whether or not they are needed. An error is returned +// if an syntax errors are encountered or if the reader fails. +func parseHTPasswd(rd io.Reader) (map[string][]byte, error) { + entries := map[string][]byte{} scanner := bufio.NewScanner(rd) + var line int for scanner.Scan() { + line++ // 1-based line numbering t := strings.TrimSpace(scanner.Text()) - i := strings.Index(t, ":") - if i < 0 || i >= len(t) { - context.GetLogger(ctx).Errorf("bad entry in htpasswd: %q", t) + + if len(t) < 1 { continue } - entries = append(entries, htpasswdEntry{ - username: t[:i], - password: t[i+1:], - }) + // lines that *begin* with a '#' are considered comments + if t[0] == '#' { + continue + } + + i := strings.Index(t, ":") + if i < 0 || i >= len(t) { + return nil, fmt.Errorf("htpasswd: invalid entry at line %d: %q", line, scanner.Text()) + } + + entries[t[:i]] = []byte(t[i+1:]) } - return entries + if err := scanner.Err(); err != nil { + return nil, err + } + + return entries, nil } diff --git a/registry/auth/basic/htpasswd_test.go b/registry/auth/basic/htpasswd_test.go new file mode 100644 index 00000000..5cc86126 --- /dev/null +++ b/registry/auth/basic/htpasswd_test.go @@ -0,0 +1,85 @@ +package basic + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func TestParseHTPasswd(t *testing.T) { + + for _, tc := range []struct { + desc string + input string + err error + entries map[string][]byte + }{ + { + desc: "basic example", + input: ` +# This is a comment in a basic example. +bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= +frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W +MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 +DeokMan:공주님 +`, + entries: map[string][]byte{ + "bilbo": []byte("{SHA}5siv5c0SHx681xU6GiSx9ZQryqs="), + "frodo": []byte("$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W"), + "MiShil": []byte("$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2"), + "DeokMan": []byte("공주님"), + }, + }, + { + desc: "ensures comments are filtered", + input: ` +# asdf:asdf +`, + }, + { + desc: "ensure midline hash is not comment", + input: ` +asdf:as#df +`, + entries: map[string][]byte{ + "asdf": []byte("as#df"), + }, + }, + { + desc: "ensure midline hash is not comment", + input: ` +# A valid comment +valid:entry +asdf +`, + err: fmt.Errorf(`htpasswd: invalid entry at line 4: "asdf"`), + }, + } { + + entries, err := parseHTPasswd(strings.NewReader(tc.input)) + if err != tc.err { + if tc.err == nil { + t.Fatalf("%s: unexpected error: %v", tc.desc, err) + } else { + if err.Error() != tc.err.Error() { // use string equality here. + t.Fatalf("%s: expected error not returned: %v != %v", tc.desc, err, tc.err) + } + } + } + + if tc.err != nil { + continue // don't test output + } + + // allow empty and nil to be equal + if tc.entries == nil { + tc.entries = map[string][]byte{} + } + + if !reflect.DeepEqual(entries, tc.entries) { + t.Fatalf("%s: entries not parsed correctly: %v != %v", tc.desc, entries, tc.entries) + } + } + +} diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 2f37aa53..08c1c004 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -147,6 +147,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) } app.accessController = accessController + ctxu.GetLogger(app).Debugf("configured %q access controller", authType) } return app From 0f654c25aca6b05e7170453d905823915d4f82de Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 10 Jun 2015 19:40:05 -0700 Subject: [PATCH 11/13] Rename the basic access controller to htpasswd Signed-off-by: Stephen J Day --- cmd/registry/main.go | 2 +- registry/auth/{basic => htpasswd}/access.go | 10 +++++----- registry/auth/{basic => htpasswd}/access_test.go | 2 +- registry/auth/{basic => htpasswd}/htpasswd.go | 2 +- registry/auth/{basic => htpasswd}/htpasswd_test.go | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename registry/auth/{basic => htpasswd}/access.go (88%) rename registry/auth/{basic => htpasswd}/access_test.go (99%) rename registry/auth/{basic => htpasswd}/htpasswd.go (99%) rename registry/auth/{basic => htpasswd}/htpasswd_test.go (99%) diff --git a/cmd/registry/main.go b/cmd/registry/main.go index 8c591bad..53c91a0a 100644 --- a/cmd/registry/main.go +++ b/cmd/registry/main.go @@ -18,7 +18,7 @@ import ( "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" _ "github.com/docker/distribution/health" - _ "github.com/docker/distribution/registry/auth/basic" + _ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/docker/distribution/registry/auth/silly" _ "github.com/docker/distribution/registry/auth/token" "github.com/docker/distribution/registry/handlers" diff --git a/registry/auth/basic/access.go b/registry/auth/htpasswd/access.go similarity index 88% rename from registry/auth/basic/access.go rename to registry/auth/htpasswd/access.go index f7d5e79b..5425b1da 100644 --- a/registry/auth/basic/access.go +++ b/registry/auth/htpasswd/access.go @@ -1,9 +1,9 @@ -// Package basic provides a simple authentication scheme that checks for the +// Package htpasswd provides a simple authentication scheme that checks for the // user credential hash in an htpasswd formatted file in a configuration-determined // location. // // This authentication method MUST be used under TLS, as simple token-replay attack is possible. -package basic +package htpasswd import ( "errors" @@ -34,12 +34,12 @@ var _ auth.AccessController = &accessController{} func newAccessController(options map[string]interface{}) (auth.AccessController, error) { realm, present := options["realm"] if _, ok := realm.(string); !present || !ok { - return nil, fmt.Errorf(`"realm" must be set for basic access controller`) + return nil, fmt.Errorf(`"realm" must be set for htpasswd access controller`) } path, present := options["path"] if _, ok := path.(string); !present || !ok { - return nil, fmt.Errorf(`"path" must be set for basic access controller`) + return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`) } f, err := os.Open(path.(string)) @@ -98,5 +98,5 @@ func (ch *challenge) Error() string { } func init() { - auth.Register("basic", auth.InitFunc(newAccessController)) + auth.Register("htpasswd", auth.InitFunc(newAccessController)) } diff --git a/registry/auth/basic/access_test.go b/registry/auth/htpasswd/access_test.go similarity index 99% rename from registry/auth/basic/access_test.go rename to registry/auth/htpasswd/access_test.go index 1976b32e..5cb2d7c9 100644 --- a/registry/auth/basic/access_test.go +++ b/registry/auth/htpasswd/access_test.go @@ -1,4 +1,4 @@ -package basic +package htpasswd import ( "io/ioutil" diff --git a/registry/auth/basic/htpasswd.go b/registry/auth/htpasswd/htpasswd.go similarity index 99% rename from registry/auth/basic/htpasswd.go rename to registry/auth/htpasswd/htpasswd.go index dd9bb1ac..494ad0a7 100644 --- a/registry/auth/basic/htpasswd.go +++ b/registry/auth/htpasswd/htpasswd.go @@ -1,4 +1,4 @@ -package basic +package htpasswd import ( "bufio" diff --git a/registry/auth/basic/htpasswd_test.go b/registry/auth/htpasswd/htpasswd_test.go similarity index 99% rename from registry/auth/basic/htpasswd_test.go rename to registry/auth/htpasswd/htpasswd_test.go index 5cc86126..309c359a 100644 --- a/registry/auth/basic/htpasswd_test.go +++ b/registry/auth/htpasswd/htpasswd_test.go @@ -1,4 +1,4 @@ -package basic +package htpasswd import ( "fmt" From 01f730ad71deb7c607e7a32c039b237606e9452e Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 10 Jun 2015 19:41:54 -0700 Subject: [PATCH 12/13] Document usage of htpasswd access controller Signed-off-by: Stephen J Day --- docs/configuration.md | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 68fb54c5..0771d5d2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -108,6 +108,9 @@ auth: service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle + htpasswd: + realm: basic-realm + path: /path/to/htpasswd middleware: registry: - name: ARegistryMiddleware @@ -600,6 +603,9 @@ auth: service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle + htpasswd: + realm: basic-realm + path: /path/to/htpasswd ``` The `auth` option is **optional**. There are @@ -710,6 +716,49 @@ public part of the certificates that is used to sign authentication tokens. For more information about Token based authentication configuration, see the [specification.] +### htpasswd + +The _htpasswd_ authentication backed allows one to configure basic auth using an +[Apache HTPasswd File](http://httpd.apache.org/docs/2.4/programs/htpasswd.html). +Only [`bcrypt`](http://en.wikipedia.org/wiki/Bcrypt) format passwords are +supported. Entries with other hash types will be ignored. The htpasswd file is +loaded once, at startup. If the file is invalid, the registry will display and +error and will not start. + +> __WARNING:__ This authentication scheme should only be used with TLS +> configured, since basic authentication sends passwords as part of the http +> header. + + + + + + + + + + + + + + + + + +
ParameterRequiredDescription
+ realm + + yes + + The realm in which the registry server authenticates. +
+ path + + yes + + Path to htpasswd file to load at startup. +
+ ## middleware The `middleware` option is **optional**. Use this option to inject middleware at From d9d55bcbab49ac08e0dc311d8b3fa38b4291c6e5 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 11 Jun 2015 17:06:35 -0700 Subject: [PATCH 13/13] Minor formatting fixes related to htpasswd auth Signed-off-by: Stephen J Day --- docs/configuration.md | 14 +++++++------- registry/auth/htpasswd/access_test.go | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0771d5d2..c7f20133 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -108,9 +108,9 @@ auth: service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle - htpasswd: - realm: basic-realm - path: /path/to/htpasswd + htpasswd: + realm: basic-realm + path: /path/to/htpasswd middleware: registry: - name: ARegistryMiddleware @@ -603,9 +603,9 @@ auth: service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle - htpasswd: - realm: basic-realm - path: /path/to/htpasswd + htpasswd: + realm: basic-realm + path: /path/to/htpasswd ``` The `auth` option is **optional**. There are @@ -719,7 +719,7 @@ For more information about Token based authentication configuration, see the [sp ### htpasswd The _htpasswd_ authentication backed allows one to configure basic auth using an -[Apache HTPasswd File](http://httpd.apache.org/docs/2.4/programs/htpasswd.html). +[Apache HTPasswd File](https://httpd.apache.org/docs/2.4/programs/htpasswd.html). Only [`bcrypt`](http://en.wikipedia.org/wiki/Bcrypt) format passwords are supported. Entries with other hash types will be ignored. The htpasswd file is loaded once, at startup. If the file is invalid, the registry will display and diff --git a/registry/auth/htpasswd/access_test.go b/registry/auth/htpasswd/access_test.go index 5cb2d7c9..ea0de425 100644 --- a/registry/auth/htpasswd/access_test.go +++ b/registry/auth/htpasswd/access_test.go @@ -85,8 +85,8 @@ func TestBasicAccessController(t *testing.T) { } nonbcrypt := map[string]struct{}{ - "bilbo": struct{}{}, - "DeokMan": struct{}{}, + "bilbo": {}, + "DeokMan": {}, } for i := 0; i < len(testUsers); i++ {