-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoken.go
More file actions
72 lines (66 loc) · 2.33 KB
/
Copy pathtoken.go
File metadata and controls
72 lines (66 loc) · 2.33 KB
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
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"log"
"time"
)
// A token is hex(expiry ‖ nonce ‖ mac), where expiry is a big-endian uint32 of
// unix seconds and mac is HMAC-SHA256(secret, expiry ‖ nonce) truncated. The
// MAC makes tokens self-validating: forged or expired labels are rejected in
// memory, so random-subdomain floods never reach Redis, and minting needs no
// store write at all.
const (
tokenExpiryBytes = 4
tokenNonceBytes = 8
tokenMACBytes = 8
tokenBytes = tokenExpiryBytes + tokenNonceBytes + tokenMACBytes
// tokenLen is the number of hex characters in a minted token.
tokenLen = 2 * tokenBytes
)
// mintToken returns a fresh token that expires cfg.TTL after now.
func mintToken(cfg Config, now time.Time) string {
var b [tokenBytes]byte
binary.BigEndian.PutUint32(b[:tokenExpiryBytes], uint32(now.Add(cfg.TTL).Unix())) // #nosec G115 -- fits until 2106
if _, err := rand.Read(b[tokenExpiryBytes : tokenExpiryBytes+tokenNonceBytes]); err != nil {
// crypto/rand failing is fatal; we never want predictable tokens
log.Fatalf("rand: %v", err)
}
copy(b[tokenExpiryBytes+tokenNonceBytes:], tokenMAC(cfg.TokenSecret, b[:tokenExpiryBytes+tokenNonceBytes]))
return hex.EncodeToString(b[:])
}
// verifyToken reports whether label is a token we minted that has not yet
// expired at now, and returns its expiry.
func verifyToken(cfg Config, label string, now time.Time) (time.Time, bool) {
if len(label) != tokenLen {
return time.Time{}, false
}
var b [tokenBytes]byte
for i := range tokenLen {
// Lower-case only: callers lower-case DNS names and Host headers, and
// this keeps one canonical spelling (and Redis key) per token.
if c := label[i]; (c < '0' || c > '9') && (c < 'a' || c > 'f') {
return time.Time{}, false
}
}
if _, err := hex.Decode(b[:], []byte(label)); err != nil {
return time.Time{}, false
}
signed := b[:tokenExpiryBytes+tokenNonceBytes]
if !hmac.Equal(b[len(signed):], tokenMAC(cfg.TokenSecret, signed)) {
return time.Time{}, false
}
expires := time.Unix(int64(binary.BigEndian.Uint32(b[:tokenExpiryBytes])), 0)
if !now.Before(expires) {
return time.Time{}, false
}
return expires, true
}
func tokenMAC(secret, msg []byte) []byte {
m := hmac.New(sha256.New, secret)
m.Write(msg)
return m.Sum(nil)[:tokenMACBytes]
}