ffe56ebe41
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 <stephen.day@docker.com>
150 lines
3.9 KiB
Go
150 lines
3.9 KiB
Go
package basic
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"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.
|
|
type htpasswd struct {
|
|
path string
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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}
|
|
}
|
|
|
|
// 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 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
entries = append(entries, htpasswdEntry{
|
|
username: t[:i],
|
|
password: t[i+1:],
|
|
})
|
|
}
|
|
|
|
return entries
|
|
}
|