view auth/auth.go @ 0:c18bc7b9d1d9

Basic binaries. checkpassword doesn't yet work.
author Paul Fisher <paul@pfish.zone>
date Sat, 24 Oct 2015 21:32:03 -0400
parents
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()
}