forgejo-runner-optimiser/internal/receiver/token.go
2026-02-13 12:48:57 +01:00

77 lines
2.6 KiB
Go

// ABOUTME: HMAC-SHA256 token generation and validation for scoped push authentication.
// ABOUTME: Tokens are derived from a key + scope + timestamp, enabling stateless validation with expiration.
package receiver
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
)
// DefaultTokenTTL is the default time-to-live for push tokens.
const DefaultTokenTTL = 2 * time.Hour
// GenerateToken creates a token with embedded timestamp for expiration support.
// Format: "<unix_timestamp>:<hmac_hex>"
func GenerateToken(key, org, repo, workflow, job string) string {
return GenerateTokenAt(key, org, repo, workflow, job, time.Now())
}
// GenerateTokenAt creates a token with the specified timestamp.
// The HMAC input is "v1\x00<org>\x00<repo>\x00<workflow>\x00<job>\x00<timestamp>".
func GenerateTokenAt(key, org, repo, workflow, job string, timestamp time.Time) string {
ts := strconv.FormatInt(timestamp.Unix(), 10)
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte("v1\x00" + org + "\x00" + repo + "\x00" + workflow + "\x00" + job + "\x00" + ts))
return ts + ":" + hex.EncodeToString(mac.Sum(nil))
}
// ValidateToken validates a token and checks expiration.
// Returns true if the token is valid and not expired.
func ValidateToken(key, token, org, repo, workflow, job string, ttl time.Duration) bool {
return ValidateTokenAt(key, token, org, repo, workflow, job, ttl, time.Now())
}
// ValidateTokenAt validates a token against a specific reference time.
func ValidateTokenAt(key, token, org, repo, workflow, job string, ttl time.Duration, now time.Time) bool {
parts := strings.SplitN(token, ":", 2)
if len(parts) != 2 {
return false
}
tsStr, hmacHex := parts[0], parts[1]
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil {
return false
}
tokenTime := time.Unix(ts, 0)
if now.Sub(tokenTime) > ttl {
return false
}
// Recompute expected HMAC
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte("v1\x00" + org + "\x00" + repo + "\x00" + workflow + "\x00" + job + "\x00" + tsStr))
expected := hex.EncodeToString(mac.Sum(nil))
return subtle.ConstantTimeCompare([]byte(hmacHex), []byte(expected)) == 1
}
// ParseTokenTimestamp extracts the timestamp from a timestamped token without validating it.
func ParseTokenTimestamp(token string) (time.Time, error) {
parts := strings.SplitN(token, ":", 2)
if len(parts) != 2 {
return time.Time{}, fmt.Errorf("invalid token format")
}
ts, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("invalid timestamp: %w", err)
}
return time.Unix(ts, 0), nil
}