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