-
Notifications
You must be signed in to change notification settings - Fork 579
/
userpassword.go
150 lines (128 loc) · 4.27 KB
/
userpassword.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package userpassword
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/util/lazy"
)
var (
// The base64 encoder used when producing the string representation of
// hashes.
base64Encoding = base64.RawStdEncoding
// The number of iterations to use when generating the hash. This was chosen
// to make it about as fast as bcrypt hashes. Increasing this causes hashes
// to take longer to compute.
defaultHashIter = 65535
// This is the length of our output hash. bcrypt has a hash size of up to
// 60, so we rounded up to a power of 8.
hashLength = 64
// The scheme to include in our hashed password.
hashScheme = "pbkdf2-sha256"
// A salt size of 16 is the default in passlib. A minimum of 8 can be safely
// used.
defaultSaltSize = 16
// The simulated hash is used when trying to simulate password checks for
// users that don't exist. It's meant to preserve the timing of the hash
// comparison.
simulatedHash = lazy.New(func() string {
h, err := Hash("hunter2")
if err != nil {
panic(err)
}
return h
})
)
// Make password hashing much faster in tests.
func init() {
args := os.Args[1:]
// Ensure this can never be enabled if running in server mode.
if slices.Contains(args, "server") {
return
}
for _, flag := range args {
if strings.HasPrefix(flag, "-test.") {
defaultHashIter = 1
return
}
}
}
// Compare checks the equality of passwords from a hashed pbkdf2 string. This
// uses pbkdf2 to ensure FIPS 140-2 compliance. See:
// https://csrc.nist.gov/csrc/media/templates/cryptographic-module-validation-program/documents/security-policies/140sp2261.pdf
func Compare(hashed string, password string) (bool, error) {
// If the hased password provided is empty, simulate comparing a real hash.
if hashed == "" {
// TODO: this seems ripe for creating a vulnerability where
// hunter2 can log into any account.
hashed = simulatedHash.Load()
}
if len(hashed) < hashLength {
return false, xerrors.Errorf("hash too short: %d", len(hashed))
}
parts := strings.SplitN(hashed, "$", 5)
if len(parts) != 5 {
return false, xerrors.Errorf("hash has too many parts: %d", len(parts))
}
if len(parts[0]) != 0 {
return false, xerrors.Errorf("hash prefix is invalid")
}
if parts[1] != hashScheme {
return false, xerrors.Errorf("hash isn't %q scheme: %q", hashScheme, parts[1])
}
iter, err := strconv.Atoi(parts[2])
if err != nil {
return false, xerrors.Errorf("parse iter from hash: %w", err)
}
salt, err := base64Encoding.DecodeString(parts[3])
if err != nil {
return false, xerrors.Errorf("decode salt: %w", err)
}
if subtle.ConstantTimeCompare([]byte(hashWithSaltAndIter(password, salt, iter)), []byte(hashed)) != 1 {
return false, nil
}
return true, nil
}
// Hash generates a hash using pbkdf2.
// See the Compare() comment for rationale.
func Hash(password string) (string, error) {
salt := make([]byte, defaultSaltSize)
_, err := rand.Read(salt)
if err != nil {
return "", xerrors.Errorf("read random bytes for salt: %w", err)
}
return hashWithSaltAndIter(password, salt, defaultHashIter), nil
}
// Produces a string representation of the hash.
func hashWithSaltAndIter(password string, salt []byte, iter int) string {
var (
hash = pbkdf2.Key([]byte(password), salt, iter, hashLength, sha256.New)
encHash = make([]byte, base64Encoding.EncodedLen(len(hash)))
encSalt = make([]byte, base64Encoding.EncodedLen(len(salt)))
)
base64Encoding.Encode(encHash, hash)
base64Encoding.Encode(encSalt, salt)
return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, encSalt, encHash)
}
// Validate checks that the plain text password meets the minimum password requirements.
// It returns properly formatted errors for detailed form validation on the client.
func Validate(password string) error {
// Ensure passwords are secure enough!
// See: https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use
err := passwordvalidator.Validate(password, 52)
if err != nil {
return err
}
if len(password) > 64 {
return xerrors.Errorf("password must be no more than %d characters", 64)
}
return nil
}