package web import ( "crypto/hmac" "crypto/rand" "encoding/base64" "encoding/binary" "net/http" "strings" "time" ) const ( onceField = "once" onceTTL = 5 * time.Minute onceLength = 16 onceSeedLength = 16 ) type Once int const ( OnceRefused Once = iota OnceFresh OnceSpent ) func (k *Kit) onceMAC(payload []byte, r *http.Request) []byte { return mac(k.keys.once, payload, *k.keys.seed, []byte(sessionID(r))) } func (k *Kit) formNonce(r *http.Request) string { nonce := make([]byte, onceLength) rand.Read(nonce) payload := binary.BigEndian.AppendUint64(nonce, uint64(k.now().Add(onceTTL).Unix())) enc := base64.RawURLEncoding return enc.EncodeToString(payload) + "." + enc.EncodeToString(k.onceMAC(payload, r)) } func (k *Kit) SpendOnce(r *http.Request) Once { p64, t64, ok := strings.Cut(r.PostFormValue(onceField), ".") enc := base64.RawURLEncoding.Strict() payload, err1 := enc.DecodeString(p64) tag, err2 := enc.DecodeString(t64) if !ok || err1 != nil || err2 != nil || len(payload) != onceLength+8 || !hmac.Equal(tag, k.onceMAC(payload, r)) { return OnceRefused } now := k.now() expiry := time.Unix(int64(binary.BigEndian.Uint64(payload[onceLength:])), 0) if now.After(expiry) { return OnceRefused } if !k.spent.Spend(onceField+":"+string(payload[:onceLength]), expiry, now) { return OnceSpent } return OnceFresh }