Raw
 1package web
 2
 3import (
 4	"crypto/hmac"
 5	"crypto/rand"
 6	"encoding/base64"
 7	"encoding/binary"
 8	"net/http"
 9	"strings"
10	"time"
11)
12
13const (
14	onceField      = "once"
15	onceTTL        = 5 * time.Minute
16	onceLength     = 16
17	onceSeedLength = 16
18)
19
20type Once int
21
22const (
23	OnceRefused Once = iota
24	OnceFresh
25	OnceSpent
26)
27
28func (k *Kit) onceMAC(payload []byte, r *http.Request) []byte {
29	return mac(k.keys.once, payload, *k.keys.seed, []byte(sessionID(r)))
30}
31
32func (k *Kit) formNonce(r *http.Request) string {
33	nonce := make([]byte, onceLength)
34	rand.Read(nonce)
35	payload := binary.BigEndian.AppendUint64(nonce, uint64(k.now().Add(onceTTL).Unix()))
36	enc := base64.RawURLEncoding
37	return enc.EncodeToString(payload) + "." + enc.EncodeToString(k.onceMAC(payload, r))
38}
39
40func (k *Kit) SpendOnce(r *http.Request) Once {
41	p64, t64, ok := strings.Cut(r.PostFormValue(onceField), ".")
42	enc := base64.RawURLEncoding.Strict()
43	payload, err1 := enc.DecodeString(p64)
44	tag, err2 := enc.DecodeString(t64)
45	if !ok || err1 != nil || err2 != nil || len(payload) != onceLength+8 || !hmac.Equal(tag, k.onceMAC(payload, r)) {
46		return OnceRefused
47	}
48	now := k.now()
49	expiry := time.Unix(int64(binary.BigEndian.Uint64(payload[onceLength:])), 0)
50	if now.After(expiry) {
51		return OnceRefused
52	}
53	if !k.spent.Spend(onceField+":"+string(payload[:onceLength]), expiry, now) {
54		return OnceSpent
55	}
56	return OnceFresh
57}