forked from TrueCloudLab/distribution
14f3b07db0
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 <stephen.day@docker.com>
80 lines
1.9 KiB
Go
80 lines
1.9 KiB
Go
package basic
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// htpasswd holds a path to a system .htpasswd file and the machinery to parse
|
|
// it. Only bcrypt hash entries are supported.
|
|
type htpasswd struct {
|
|
entries map[string][]byte // maps username to password byte slice.
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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.
|
|
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 ErrAuthenticationFailure
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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())
|
|
|
|
if len(t) < 1 {
|
|
continue
|
|
}
|
|
|
|
// 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:])
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entries, nil
|
|
}
|