comparison 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
comparison
equal deleted inserted replaced
17:342f63116bfd 18:00d30c67b56d
1 package file
2
3 import (
4 "crypto/rand"
5 "encoding/base64"
6 "errors"
7 "math/big"
8 "strconv"
9 "strings"
10
11 "golang.org/x/crypto/bcrypt"
12 )
13
14 const (
15 // default cost stolen from python bcrypt
16 bcryptCost = 12
17 // we only generate passwords from lowercases for non-ambiguity
18 lowercases = "abcdefghijklmnopqrstuvwxyz"
19 template = "????-????-????-????"
20 )
21
22 var (
23 lowercaseLen *big.Int = big.NewInt(int64(len(lowercases)))
24 maxID = big.NewInt(0)
25 )
26
27 var (
28 WrongLengthError error = errors.New("multipass/auth: password entry must have 3 fields")
29 BadIDError = errors.New("multipass/auth: ID field invalid")
30 Base64Error = errors.New("multipass/auth: can't decode base64 data")
31 LongDescriptionError = errors.New("multipass/auth: description must be less than 255 bytes")
32 )
33
34 func init() {
35 one := big.NewInt(1)
36 maxID.Lsh(one, 64)
37 }
38
39 // Entry represents a single entry in the a multipass file.
40 type Entry struct {
41 id uint64
42 hash string
43 description string
44 }
45
46 // EntryFromShadow creates a new entry from a line in a multipass shadow file.
47 // The line should not end in a newline.
48 func EntryFromShadow(shadow string) (*Entry, error) {
49 segments := strings.Split(shadow, ":")
50 if len(segments) != 3 {
51 return nil, WrongLengthError
52 }
53 entry := new(Entry)
54 id, err := strconv.ParseUint(segments[0], 10, 64)
55 if err != nil {
56 return nil, BadIDError
57 }
58 entry.id = id
59 entry.hash = segments[1]
60 description, err := base64.StdEncoding.DecodeString(segments[2])
61 if err != nil {
62 return nil, Base64Error
63 }
64 entry.description = string(description)
65 return entry, nil
66 }
67
68 // NewEntry creates an Entry for the given description.
69 // It returns the Entry itself and a generated password.
70 func NewEntry(description string) (entry *Entry, password string, err error) {
71 if len(description) > 255 {
72 return nil, "", LongDescriptionError
73 }
74 passBytes := genPassword()
75 password = string(passBytes)
76 hashBytes, err := bcrypt.GenerateFromPassword(passBytes, bcryptCost)
77 if err != nil {
78 // This is very unexpected.
79 return nil, "", err
80 }
81 e := new(Entry)
82 e.id = newID()
83 e.hash = string(hashBytes)
84 e.description = description
85 return e, password, nil
86 }
87
88 // ID is a unique 64-bit integer which identifies the entry.
89 func (e *Entry) ID() uint64 {
90 return e.id
91 }
92
93 // Description is the user's description of their password.
94 func (e *Entry) Description() string {
95 return e.description
96 }
97
98 // Authenticate tests whether the password is correct.
99 func (e *Entry) Authenticate(password string) bool {
100 err := bcrypt.CompareHashAndPassword([]byte(e.hash), []byte(password))
101 return err == nil
102 }
103
104 // Encode encodes this Entry to a bytestring for writing to a multipass shadow file.
105 func (e *Entry) Encode() string {
106 segments := []string{
107 strconv.FormatUint(e.id, 10),
108 e.hash,
109 base64.StdEncoding.EncodeToString([]byte(e.description)),
110 }
111 return strings.Join(segments, ":")
112 }
113
114 func genPassword() []byte {
115 password := []byte(template)
116 for i, chr := range password {
117 if chr == '?' {
118 password[i] = randChr()
119 }
120 }
121 return password
122 }
123
124 func randChr() byte {
125 bigIdx, err := rand.Int(rand.Reader, lowercaseLen)
126 if err != nil {
127 panic("multipass/auth: can't get a random number")
128 }
129 idx := bigIdx.Int64()
130 return byte(lowercases[idx])
131 }
132
133 func newID() uint64 {
134 bigID, err := rand.Int(rand.Reader, maxID)
135 if err != nil {
136 panic("multipass/auth: can't get a random number")
137 }
138 return bigID.Uint64()
139 }