@@ -6,25 +6,69 @@ import (
6
6
"crypto/subtle"
7
7
"encoding/base64"
8
8
"fmt"
9
+ "os"
9
10
"strconv"
10
11
"strings"
11
12
12
13
"golang.org/x/crypto/pbkdf2"
14
+ "golang.org/x/exp/slices"
13
15
"golang.org/x/xerrors"
16
+
17
+ "github.com/coder/coder/cryptorand"
14
18
)
15
19
16
- const (
17
- // This is the length of our output hash.
18
- // bcrypt has a hash size of 59, so we rounded up to a power of 8.
20
+ var (
21
+ // The base64 encoder used when producing the string representation of
22
+ // hashes.
23
+ base64Encoder = base64 .RawStdEncoding
24
+
25
+ // The number of iterations to use when generating the hash. This was chosen
26
+ // to make it about as fast as bcrypt hashes. Increasing this causes hashes
27
+ // to take longer to compute.
28
+ defaultHashIter = 65535
29
+
30
+ // This is the length of our output hash. bcrypt has a hash size of up to
31
+ // 60, so we rounded up to a power of 8.
19
32
hashLength = 64
33
+
20
34
// The scheme to include in our hashed password.
21
35
hashScheme = "pbkdf2-sha256"
36
+
37
+ // A salt size of 16 is the default in passlib. A minimum of 8 can be safely
38
+ // used.
39
+ saltSize = 16
40
+
41
+ // The simulated hash is used when trying to simulate password checks for
42
+ // users that don't exist.
43
+ simulatedHash , _ = Hash (cryptorand .MustString (10 ))
22
44
)
23
45
24
- // Compare checks the equality of passwords from a hashed pbkdf2 string.
25
- // This uses pbkdf2 to ensure FIPS 140-2 compliance. See:
46
+ // Make password hashing much faster in tests.
47
+ func init () {
48
+ args := os .Args [1 :]
49
+
50
+ // Ensure this can never be enabled if running in server mode.
51
+ if slices .Contains (args , "server" ) {
52
+ return
53
+ }
54
+
55
+ for _ , flag := range args {
56
+ if strings .HasPrefix (flag , "-test." ) {
57
+ defaultHashIter = 1
58
+ return
59
+ }
60
+ }
61
+ }
62
+
63
+ // Compare checks the equality of passwords from a hashed pbkdf2 string. This
64
+ // uses pbkdf2 to ensure FIPS 140-2 compliance. See:
26
65
// https://csrc.nist.gov/csrc/media/templates/cryptographic-module-validation-program/documents/security-policies/140sp2261.pdf
27
66
func Compare (hashed string , password string ) (bool , error ) {
67
+ // If the hased password provided is empty, simulate comparing a real hash.
68
+ if hashed == "" {
69
+ hashed = simulatedHash
70
+ }
71
+
28
72
if len (hashed ) < hashLength {
29
73
return false , xerrors .Errorf ("hash too short: %d" , len (hashed ))
30
74
}
@@ -42,37 +86,40 @@ func Compare(hashed string, password string) (bool, error) {
42
86
if err != nil {
43
87
return false , xerrors .Errorf ("parse iter from hash: %w" , err )
44
88
}
45
- salt , err := base64 . RawStdEncoding .DecodeString (parts [3 ])
89
+ salt , err := base64Encoder .DecodeString (parts [3 ])
46
90
if err != nil {
47
91
return false , xerrors .Errorf ("decode salt: %w" , err )
48
92
}
49
93
50
94
if subtle .ConstantTimeCompare ([]byte (hashWithSaltAndIter (password , salt , iter )), []byte (hashed )) != 1 {
51
95
return false , nil
52
96
}
97
+
53
98
return true , nil
54
99
}
55
100
56
101
// Hash generates a hash using pbkdf2.
57
102
// See the Compare() comment for rationale.
58
103
func Hash (password string ) (string , error ) {
59
- // bcrypt uses a salt size of 16 bytes.
60
- salt := make ([]byte , 16 )
104
+ salt := make ([]byte , saltSize )
61
105
_ , err := rand .Read (salt )
62
106
if err != nil {
63
107
return "" , xerrors .Errorf ("read random bytes for salt: %w" , err )
64
108
}
65
- // The default hash iteration is 1024 for speed.
66
- // As this is increased, the password is hashed more.
67
- return hashWithSaltAndIter (password , salt , 1024 ), nil
109
+
110
+ return hashWithSaltAndIter (password , salt , defaultHashIter ), nil
68
111
}
69
112
70
113
// Produces a string representation of the hash.
71
114
func hashWithSaltAndIter (password string , salt []byte , iter int ) string {
72
- hash := pbkdf2 .Key ([]byte (password ), salt , iter , hashLength , sha256 .New )
73
- hash = []byte (base64 .RawStdEncoding .EncodeToString (hash ))
74
- salt = []byte (base64 .RawStdEncoding .EncodeToString (salt ))
75
- // This format is similar to bcrypt. See:
76
- // https://en.wikipedia.org/wiki/Bcrypt#Description
77
- return fmt .Sprintf ("$%s$%d$%s$%s" , hashScheme , iter , salt , hash )
115
+ var (
116
+ hash = pbkdf2 .Key ([]byte (password ), salt , iter , hashLength , sha256 .New )
117
+ encHash = make ([]byte , base64Encoder .EncodedLen (len (hash )))
118
+ encSalt = make ([]byte , base64Encoder .EncodedLen (len (salt )))
119
+ )
120
+
121
+ base64Encoder .Encode (encHash , hash )
122
+ base64Encoder .Encode (encSalt , salt )
123
+
124
+ return fmt .Sprintf ("$%s$%d$%s$%s" , hashScheme , iter , encSalt , encHash )
78
125
}
0 commit comments