diff --git a/internal/crypto/session.go b/internal/crypto/session.go new file mode 100644 index 0000000..2c938d0 --- /dev/null +++ b/internal/crypto/session.go @@ -0,0 +1,44 @@ +package crypto + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" +) + +// token = base64(user) "." expiryUnix "." base64(hmac) +func SignSession(secret []byte, user string, expiry time.Time) string { + payload := base64.RawURLEncoding.EncodeToString([]byte(user)) + "." + + strconv.FormatInt(expiry.Unix(), 10) + return payload + "." + sign(secret, payload) +} + +func VerifySession(secret []byte, token string, now time.Time) (string, bool) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return "", false + } + payload := parts[0] + "." + parts[1] + if !hmac.Equal([]byte(parts[2]), []byte(sign(secret, payload))) { + return "", false + } + exp, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil || now.Unix() > exp { + return "", false + } + user, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return "", false + } + return string(user), true +} + +func sign(secret []byte, payload string) string { + m := hmac.New(sha256.New, secret) + fmt.Fprint(m, payload) + return base64.RawURLEncoding.EncodeToString(m.Sum(nil)) +} diff --git a/internal/crypto/session_test.go b/internal/crypto/session_test.go new file mode 100644 index 0000000..8d844f9 --- /dev/null +++ b/internal/crypto/session_test.go @@ -0,0 +1,34 @@ +package crypto + +import ( + "testing" + "time" +) + +func TestSessionRoundTrip(t *testing.T) { + secret := []byte("s3cr3t") + now := time.Unix(1_700_000_000, 0) + tok := SignSession(secret, "admin", now.Add(time.Hour)) + user, ok := VerifySession(secret, tok, now) + if !ok || user != "admin" { + t.Fatalf("verify = %q,%v want admin,true", user, ok) + } +} + +func TestSessionRejectsExpired(t *testing.T) { + secret := []byte("s3cr3t") + now := time.Unix(1_700_000_000, 0) + tok := SignSession(secret, "admin", now.Add(-time.Second)) + if _, ok := VerifySession(secret, tok, now); ok { + t.Fatal("expired token must be rejected") + } +} + +func TestSessionRejectsTampered(t *testing.T) { + secret := []byte("s3cr3t") + now := time.Unix(1_700_000_000, 0) + tok := SignSession(secret, "admin", now.Add(time.Hour)) + if _, ok := VerifySession([]byte("other"), tok, now); ok { + t.Fatal("wrong secret must be rejected") + } +}