Files
warmbly/internal/formserver/token.go
T

51 lines
1.5 KiB
Go

package formserver
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"strconv"
"time"
)
// The render token is the anti-scraping gate: the JSON API and submit
// endpoints only answer requests carrying a token minted when the page shell
// was served, so a script cannot harvest form definitions or spray submits
// without loading the page first. The key derives from INTERNAL_API_TOKEN,
// so replicas agree without any new configuration.
const renderTokenTTL = 12 * time.Hour
func renderKey(internalToken string) []byte {
sum := sha256.Sum256([]byte(internalToken + ":forms-render"))
return sum[:]
}
func renderMAC(key []byte, publicID string, expiry int64) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(publicID + "." + strconv.FormatInt(expiry, 10)))
return mac.Sum(nil)
}
// mintRenderToken returns base64url(expiryUnix_be8 || HMAC(publicID.expiry)).
func mintRenderToken(key []byte, publicID string, now time.Time) string {
expiry := now.Add(renderTokenTTL).Unix()
buf := make([]byte, 8, 8+sha256.Size)
binary.BigEndian.PutUint64(buf, uint64(expiry))
buf = append(buf, renderMAC(key, publicID, expiry)...)
return base64.RawURLEncoding.EncodeToString(buf)
}
func verifyRenderToken(key []byte, publicID, token string, now time.Time) bool {
raw, err := base64.RawURLEncoding.DecodeString(token)
if err != nil || len(raw) != 8+sha256.Size {
return false
}
expiry := int64(binary.BigEndian.Uint64(raw[:8]))
if now.Unix() > expiry {
return false
}
return hmac.Equal(raw[8:], renderMAC(key, publicID, expiry))
}