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}