diff file/auth.go @ 18:00d30c67b56d

Put all the library stuff into multipass/file.
author Paul Fisher <paul@pfish.zone>
date Sun, 01 Nov 2015 12:16:51 -0500
parents auth/auth.go@4368a377ff64
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/file/auth.go	Sun Nov 01 12:16:51 2015 -0500
@@ -0,0 +1,139 @@
+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()
+}