diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/auth/auth.go	Sat Oct 24 21:32:03 2015 -0400
@@ -0,0 +1,147 @@
+// 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()
+}