Skip to content

Commit 76ef5ea

Browse files
hashed token generation facility
1 parent ec02350 commit 76ef5ea

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

auth/token.go

+64
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ package auth
22

33
import (
44
"crypto/rand"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"errors"
8+
"fmt"
9+
"io"
510
"math/big"
11+
"strings"
612
)
713

814
var (
@@ -24,6 +30,64 @@ func randIntn(n int) int {
2430
return int(res.Int64())
2531
}
2632

33+
// Convert a Token to its hashed representation.
34+
func HashToken(s string, salt []byte) (string, error) {
35+
saltHex := fmt.Sprintf("%x", salt)
36+
sha := sha256.New()
37+
_, err := sha.Write(salt)
38+
if err != nil {
39+
return "", err
40+
}
41+
_, err = sha.Write([]byte(s))
42+
if err != nil {
43+
return "", err
44+
}
45+
hashed := fmt.Sprintf("%x", sha.Sum(nil))
46+
return fmt.Sprintf("%s$%s", saltHex, hashed), nil
47+
}
48+
49+
// CompareToken compares a token with a hashed representation, optionally upgrading the hash if necessary.
50+
func CompareToken(s string, hashed string) (bool, *string, error) {
51+
if len(s) != randomTokenLength+1 /* prefix */ {
52+
return false, nil, errors.New("invalid token length")
53+
}
54+
55+
split := strings.SplitN(hashed, "$", 2)
56+
57+
// determine if we need to upgrade the hash
58+
if len(split) == 1 {
59+
match := s == hashed
60+
if match {
61+
var salt [16]byte
62+
_, err := io.ReadFull(randReader, salt[:])
63+
if err != nil {
64+
return false, nil, err
65+
}
66+
hashed, err := HashToken(s, salt[:])
67+
if err != nil {
68+
return false, nil, err
69+
}
70+
return true, &hashed, nil
71+
} else {
72+
return false, nil, nil
73+
}
74+
}
75+
76+
if len(split) == 2 {
77+
salt, err := hex.DecodeString(split[0])
78+
if err != nil {
79+
return false, nil, err
80+
}
81+
inputHashed, err := HashToken(s, salt)
82+
if err != nil {
83+
return false, nil, err
84+
}
85+
return inputHashed == hashed, nil, nil
86+
}
87+
88+
return false, nil, errors.New("invalid hash format")
89+
}
90+
2791
// GenerateNotExistingToken receives a token generation func and a func to check whether the token exists, returns a unique token.
2892
func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
2993
for {

auth/token_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,46 @@ func TestTokenHavePrefix(t *testing.T) {
1919
}
2020
}
2121

22+
func TestHashTokenStable(t *testing.T) {
23+
salt1 := []byte("salt")
24+
salt2 := []byte("pepper")
25+
seen := make(map[string]bool)
26+
for _, plain := range []string{"", "a", "b", "c", "a\x000", "a\n"} {
27+
hash1, err := HashToken(plain, salt1)
28+
assert.NoError(t, err)
29+
hash1Again, err := HashToken(plain, salt1)
30+
assert.NoError(t, err)
31+
assert.Equal(t, hash1, hash1Again)
32+
hash2, err := HashToken(plain, salt2)
33+
assert.NoError(t, err)
34+
hash2Again, err := HashToken(plain, salt2)
35+
assert.NoError(t, err)
36+
assert.Equal(t, hash2, hash2Again)
37+
38+
assert.NotEqual(t, hash1, hash2)
39+
assert.False(t, seen[hash1])
40+
assert.False(t, seen[hash2])
41+
seen[hash1] = true
42+
seen[hash2] = true
43+
}
44+
}
45+
46+
func TestCompareToken(t *testing.T) {
47+
salt := []byte("salt")
48+
tokenPlain := GenerateApplicationToken()
49+
hashed, err := HashToken(tokenPlain, salt)
50+
assert.NoError(t, err)
51+
cmpPlain, upgPlain, err := CompareToken(tokenPlain, tokenPlain)
52+
assert.NoError(t, err)
53+
assert.True(t, cmpPlain)
54+
assert.NotEmpty(t, *upgPlain)
55+
56+
cmpHashed, upgHashed, err := CompareToken(tokenPlain, hashed)
57+
assert.NoError(t, err)
58+
assert.True(t, cmpHashed)
59+
assert.Nil(t, upgHashed)
60+
}
61+
2262
func TestGenerateNotExistingToken(t *testing.T) {
2363
count := 5
2464
token := GenerateNotExistingToken(func() string {

0 commit comments

Comments
 (0)