view file/auth.go @ 20:ef2ef22ca4b1 default tip

Add pfish.zone/go/multipass/file import directive to 'file' package.
author Paul Fisher <paul@pfish.zone>
date Sun, 01 Nov 2015 13:00:54 -0500
parents 00d30c67b56d
children
line wrap: on
line source

package file

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 (
	WrongLengthError     error = errors.New("multipass/auth: password entry must have 3 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
}

// 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) != 3 {
		return nil, WrongLengthError
	}
	entry := new(Entry)
	id, err := strconv.ParseUint(segments[0], 10, 64)
	if err != nil {
		return nil, BadIDError
	}
	entry.id = id
	entry.hash = segments[1]
	description, err := base64.StdEncoding.DecodeString(segments[2])
	if err != nil {
		return nil, Base64Error
	}
	entry.description = string(description)
	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)),
	}
	return strings.Join(segments, ":")
}

func genPassword() []byte {
	password := []byte(template)
	for i, chr := range password {
		if chr == '?' {
			password[i] = 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()
}