core/captcha/tokens.go

97 lines
2.3 KiB
Go

package captcha
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
// nowMs is a seam so tests can control time. Millisecond epoch to match the
// reference (JS Date.now()).
var nowMs = func() int64 { return time.Now().UnixMilli() }
type challengeClaims struct {
Nonce string
ExpiresMs int64
C, S, D int
}
func sign(secret []byte, payload string) string {
m := hmac.New(sha256.New, secret)
m.Write([]byte(payload))
return hex.EncodeToString(m.Sum(nil))
}
// makeChallengeToken encodes challenge params as "nonce:expires:c:s:d:hmac".
func makeChallengeToken(secret []byte, nonce string, expiresMs int64, c, s, d int) string {
payload := nonce + ":" + strconv.FormatInt(expiresMs, 10) + ":" +
strconv.Itoa(c) + ":" + strconv.Itoa(s) + ":" + strconv.Itoa(d)
return payload + ":" + sign(secret, payload)
}
func verifyChallengeToken(secret []byte, token string) *challengeClaims {
if token == "" {
return nil
}
i := strings.LastIndex(token, ":")
if i < 0 {
return nil
}
payload, sig := token[:i], token[i+1:]
if !hmac.Equal([]byte(sig), []byte(sign(secret, payload))) {
return nil
}
f := strings.Split(payload, ":")
if len(f) != 5 {
return nil
}
expires, err1 := strconv.ParseInt(f[1], 10, 64)
c, err2 := strconv.Atoi(f[2])
s, err3 := strconv.Atoi(f[3])
d, err4 := strconv.Atoi(f[4])
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
return nil
}
if expires <= nowMs() {
return nil
}
return &challengeClaims{Nonce: f[0], ExpiresMs: expires, C: c, S: s, D: d}
}
// makeVerificationToken creates "random:expires:hmac".
func makeVerificationToken(secret []byte, expiresMs int64) (string, error) {
buf := make([]byte, 15)
if _, err := rand.Read(buf); err != nil {
return "", err
}
payload := hex.EncodeToString(buf) + ":" + strconv.FormatInt(expiresMs, 10)
return payload + ":" + sign(secret, payload), nil
}
func verifyVerificationToken(secret []byte, token string) bool {
if token == "" {
return false
}
i := strings.LastIndex(token, ":")
if i < 0 {
return false
}
payload, sig := token[:i], token[i+1:]
if !hmac.Equal([]byte(sig), []byte(sign(secret, payload))) {
return false
}
f := strings.Split(payload, ":")
if len(f) != 2 {
return false
}
expires, err := strconv.ParseInt(f[1], 10, 64)
if err != nil {
return false
}
return expires > nowMs()
}