distribution/docs/auth/basic/htpasswd.go

151 lines
3.9 KiB
Go
Raw Normal View History

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
}