github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/auth/htpasswd/htpasswd.go (about)

     1  package htpasswd
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  
     9  	"golang.org/x/crypto/bcrypt"
    10  )
    11  
    12  // htpasswd holds a path to a system .htpasswd file and the machinery to parse
    13  // it. Only bcrypt hash entries are supported.
    14  type htpasswd struct {
    15  	entries map[string][]byte // maps username to password byte slice.
    16  }
    17  
    18  // newHTPasswd parses the reader and returns an htpasswd or an error.
    19  func newHTPasswd(rd io.Reader) (*htpasswd, error) {
    20  	entries, err := parseHTPasswd(rd)
    21  	if err != nil {
    22  		return nil, err
    23  	}
    24  
    25  	return &htpasswd{entries: entries}, nil
    26  }
    27  
    28  // AuthenticateUser checks a given user:password credential against the
    29  // receiving HTPasswd's file. If the check passes, nil is returned.
    30  func (htpasswd *htpasswd) authenticateUser(username string, password string) error {
    31  	credentials, ok := htpasswd.entries[username]
    32  	if !ok {
    33  		// timing attack paranoia
    34  		bcrypt.CompareHashAndPassword([]byte{}, []byte(password))
    35  
    36  		return ErrAuthenticationFailure
    37  	}
    38  
    39  	err := bcrypt.CompareHashAndPassword([]byte(credentials), []byte(password))
    40  	if err != nil {
    41  		return ErrAuthenticationFailure
    42  	}
    43  
    44  	return nil
    45  }
    46  
    47  // parseHTPasswd parses the contents of htpasswd. This will read all the
    48  // entries in the file, whether or not they are needed. An error is returned
    49  // if an syntax errors are encountered or if the reader fails.
    50  func parseHTPasswd(rd io.Reader) (map[string][]byte, error) {
    51  	entries := map[string][]byte{}
    52  	scanner := bufio.NewScanner(rd)
    53  	var line int
    54  	for scanner.Scan() {
    55  		line++ // 1-based line numbering
    56  		t := strings.TrimSpace(scanner.Text())
    57  
    58  		if len(t) < 1 {
    59  			continue
    60  		}
    61  
    62  		// lines that *begin* with a '#' are considered comments
    63  		if t[0] == '#' {
    64  			continue
    65  		}
    66  
    67  		i := strings.Index(t, ":")
    68  		if i < 0 || i >= len(t) {
    69  			return nil, fmt.Errorf("htpasswd: invalid entry at line %d: %q", line, scanner.Text())
    70  		}
    71  
    72  		entries[t[:i]] = []byte(t[i+1:])
    73  	}
    74  
    75  	if err := scanner.Err(); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return entries, nil
    80  }