Files
LitePan/pkg/security/session.go
T
2026-07-27 19:31:43 +08:00

91 lines
2.0 KiB
Go

package security
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"errors"
"strings"
"time"
)
var (
ErrBadSessionSignature = errors.New("bad session signature")
ErrSessionExpired = errors.New("session expired")
)
type TimedSerializer struct {
secret []byte
}
func NewTimedSerializer(secret []byte) *TimedSerializer {
return &TimedSerializer{secret: secret}
}
func (s *TimedSerializer) Dumps(payload string) (string, error) {
wrapped := map[string]any{
"d": payload,
"t": time.Now().Unix(),
}
raw, err := json.Marshal(wrapped)
if err != nil {
return "", err
}
encodedPayload := b64Encode(raw)
sig := hmac.New(sha256.New, s.secret)
_, _ = sig.Write([]byte(encodedPayload))
return encodedPayload + "." + b64Encode(sig.Sum(nil)), nil
}
func (s *TimedSerializer) Loads(token string, maxAge int) (string, error) {
if token == "" {
return "", ErrBadSessionSignature
}
i := len(token) - 1
for i >= 0 && token[i] != '.' {
i--
}
if i <= 0 || i >= len(token)-1 {
return "", ErrBadSessionSignature
}
encodedPayload := token[:i]
encodedSig := token[i+1:]
expectedSig := hmac.New(sha256.New, s.secret)
_, _ = expectedSig.Write([]byte(encodedPayload))
expected := b64Encode(expectedSig.Sum(nil))
if subtle.ConstantTimeCompare([]byte(encodedSig), []byte(expected)) != 1 {
return "", ErrBadSessionSignature
}
raw, err := b64Decode(encodedPayload)
if err != nil {
return "", ErrBadSessionSignature
}
var wrapped struct {
D string `json:"d"`
T int64 `json:"t"`
}
if err := json.Unmarshal(raw, &wrapped); err != nil {
return "", ErrBadSessionSignature
}
if maxAge > 0 && time.Now().Unix()-wrapped.T > int64(maxAge) {
return "", ErrSessionExpired
}
return wrapped.D, nil
}
func b64Encode(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
func b64Decode(data string) ([]byte, error) {
pad := (4 - len(data)%4) % 4
if pad > 0 {
data += strings.Repeat("=", pad)
}
return base64.URLEncoding.DecodeString(data)
}