Mercurial > go > multipass
view auth/auth.go @ 7:406d3cd76739
Report errors to the user.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Sat, 24 Oct 2015 23:40:13 -0400 |
parents | c18bc7b9d1d9 |
children | 1c194fa9bbf4 |
line wrap: on
line source
// Package auth contains data structures for authenticating users. package auth import ( "crypto/rand" "encoding/base64" "errors" "math/big" "strconv" "strings" "golang.org/x/crypto/bcrypt" ) const ( // default cost stolen from python bcrypt bcryptCost = 12 // we only generate passwords from lowercases for non-ambiguity lowercases = "abcdefghijklmnopqrstuvwxyz" template = "____-____-____-____" ) var ( lowercaseLen *big.Int = big.NewInt(int64(len(lowercases))) maxID = big.NewInt(0) ) var ( ShortEntryError error = errors.New("multipass/auth: password entry must have 3 or more fields") BadIDError = errors.New("multipass/auth: ID field invalid") Base64Error = errors.New("multipass/auth: can't decode base64 data") LongDescriptionError = errors.New("multipass/auth: description must be less than 255 bytes") ) func init() { one := big.NewInt(1) maxID.Lsh(one, 64) } // Entry represents a single entry in the a multipass file. type Entry struct { id uint64 hash string description string rest []string } // EntryFromShadow creates a new entry from a line in a multipass shadow file. // The line should not end in a newline. func EntryFromShadow(shadow string) (*Entry, error) { segments := strings.Split(shadow, ":") if len(segments) < 2 { return nil, ShortEntryError } entry := new(Entry) id, err := strconv.ParseUint(segments[0], 10, 64) if err != nil { return nil, BadIDError } entry.id = id entry.hash = segments[1] if len(segments) > 2 { description, err := base64.StdEncoding.DecodeString(segments[2]) if err != nil { return nil, Base64Error } entry.description = string(description) entry.rest = segments[3:] } return entry, nil } // NewEntry creates an Entry for the given description. // It returns the Entry itself and a generated password. func NewEntry(description string) (entry *Entry, password string, err error) { if len(description) > 255 { return nil, "", LongDescriptionError } passBytes := genPassword() password = string(passBytes) hashBytes, err := bcrypt.GenerateFromPassword(passBytes, bcryptCost) if err != nil { // This is very unexpected. return nil, "", err } e := new(Entry) e.id = newID() e.hash = string(hashBytes) e.description = description return e, password, nil } // ID is a unique 64-bit integer which identifies the entry. func (e *Entry) ID() uint64 { return e.id } // Description is the user's description of their password. func (e *Entry) Description() string { return e.description } // Authenticate tests whether the password is correct. func (e *Entry) Authenticate(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(e.hash), []byte(password)) return err == nil } // Encode encodes this Entry to a bytestring for writing to a multipass shadow file. func (e *Entry) Encode() string { segments := []string{ strconv.FormatUint(e.id, 10), e.hash, base64.StdEncoding.EncodeToString([]byte(e.description)), } segments = append(segments, e.rest...) return strings.Join(segments, ":") } func genPassword() []byte { password := []byte(template) for group := 0; group < 4; group++ { base := group * 5 for chr := 0; chr < 4; chr++ { password[base+chr] = randChr() } } return password } func randChr() byte { bigIdx, err := rand.Int(rand.Reader, lowercaseLen) if err != nil { panic("multipass/auth: can't get a random number") } idx := bigIdx.Int64() return byte(lowercases[idx]) } func newID() uint64 { bigID, err := rand.Int(rand.Reader, maxID) if err != nil { panic("multipass/auth: can't get a random number") } return bigID.Uint64() }