check / check (push) Canceled after 0s
seal.DeriveKeys derives the session hash and block keys, the encrypted URL key and the CSRF key from the signing key, with their salts declared beside it, and returns them as one seal.Keys. internal/providers calls it once at startup and provides the value to the fx graph. The session manager, the encrypted URL generator and the CSRF protection each take their own key. session.NewManager, encurl.NewGenerator, newCSRFProtect and handlers.New return no error, as nothing in them can fail now. Each key is derived as before, with seal.DeriveKey and the same salt. The signing key itself stays the HMAC key of signed URLs and the login password. README.md says under signing_key what it protects. Model: opus-5-5
256 lines
6.1 KiB
Go
256 lines
6.1 KiB
Go
package encurl_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
)
|
|
|
|
// Shared test fixture strings.
|
|
const (
|
|
testSourceHost = "cdn.example.com"
|
|
testSourcePath = "/images/photo.jpg"
|
|
testSourceQuery = "v=2"
|
|
)
|
|
|
|
func TestGenerator_GenerateAndParse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, err := seal.DeriveKeys("test-signing-key-12345")
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
Query: testSourceQuery,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 800, Height: 600},
|
|
Format: imagerequest.FormatWebP,
|
|
Quality: 90,
|
|
FitMode: imagerequest.FitContain,
|
|
},
|
|
}
|
|
expires := time.Now().Add(time.Hour)
|
|
|
|
token, err := gen.Generate(req, expires)
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
if token == "" {
|
|
t.Fatal("Generate() returned empty token")
|
|
}
|
|
|
|
parsed, parsedExpires, err := gen.Parse(token)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
// Verify all fields
|
|
if parsed.Source.Host != req.Source.Host {
|
|
t.Errorf("SourceHost = %q, want %q", parsed.Source.Host, req.Source.Host)
|
|
}
|
|
|
|
if parsed.Source.Path != req.Source.Path {
|
|
t.Errorf("SourcePath = %q, want %q", parsed.Source.Path, req.Source.Path)
|
|
}
|
|
|
|
if parsed.Source.Query != req.Source.Query {
|
|
t.Errorf("SourceQuery = %q, want %q", parsed.Source.Query, req.Source.Query)
|
|
}
|
|
|
|
if parsed.Transform.Size.Width != req.Transform.Size.Width {
|
|
t.Errorf("Width = %d, want %d",
|
|
parsed.Transform.Size.Width, req.Transform.Size.Width)
|
|
}
|
|
|
|
if parsed.Transform.Size.Height != req.Transform.Size.Height {
|
|
t.Errorf("Height = %d, want %d",
|
|
parsed.Transform.Size.Height, req.Transform.Size.Height)
|
|
}
|
|
|
|
if parsed.Transform.Format != req.Transform.Format {
|
|
t.Errorf("Format = %q, want %q",
|
|
parsed.Transform.Format, req.Transform.Format)
|
|
}
|
|
|
|
if parsed.Transform.Quality != req.Transform.Quality {
|
|
t.Errorf("Quality = %d, want %d",
|
|
parsed.Transform.Quality, req.Transform.Quality)
|
|
}
|
|
|
|
if parsed.Transform.FitMode != req.Transform.FitMode {
|
|
t.Errorf("FitMode = %q, want %q",
|
|
parsed.Transform.FitMode, req.Transform.FitMode)
|
|
}
|
|
|
|
if parsedExpires.Unix() != expires.Unix() {
|
|
t.Errorf("ExpiresAt = %d, want %d", parsedExpires.Unix(), expires.Unix())
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_Expired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, err := gen.Generate(req, time.Now().Add(-time.Hour)) // Already expired
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
_, _, err = gen.Parse(token)
|
|
if err == nil {
|
|
t.Error("Parse() should fail for expired token")
|
|
}
|
|
|
|
if !errors.Is(err, encurl.ErrExpired) {
|
|
t.Errorf("Parse() error = %v, want %v", err, encurl.ErrExpired)
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_InvalidToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
_, _, err := gen.Parse("not-a-valid-token")
|
|
if err == nil {
|
|
t.Error("Parse() should fail for invalid token")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_TamperedToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
// Tamper with the token
|
|
tampered := []byte(token)
|
|
if len(tampered) > 10 {
|
|
tampered[10] ^= 0x01
|
|
}
|
|
|
|
_, _, err := gen.Parse(string(tampered))
|
|
if err == nil {
|
|
t.Error("Parse() should fail for tampered token")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_WrongKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys1, _ := seal.DeriveKeys("signing-key-1")
|
|
keys2, _ := seal.DeriveKeys("signing-key-2")
|
|
gen1 := encurl.NewGenerator(keys1.EncryptedURLKey)
|
|
gen2 := encurl.NewGenerator(keys2.EncryptedURLKey)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen1.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
_, _, err := gen2.Parse(token)
|
|
if err == nil {
|
|
t.Error("Parse() should fail with different signing key")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_Defaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
// Request with only required fields - should get defaults
|
|
token, err := gen.Generate(&imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}, time.Now().Add(time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
req, _, err := gen.Parse(token)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if req.Transform.Format != encurl.DefaultFormat {
|
|
t.Errorf("Format = %q, want default %q",
|
|
req.Transform.Format, encurl.DefaultFormat)
|
|
}
|
|
|
|
if req.Transform.Quality != encurl.DefaultQuality {
|
|
t.Errorf("Quality = %d, want default %d",
|
|
req.Transform.Quality, encurl.DefaultQuality)
|
|
}
|
|
|
|
if req.Transform.FitMode != encurl.DefaultFitMode {
|
|
t.Errorf("FitMode = %q, want default %q",
|
|
req.Transform.FitMode, encurl.DefaultFitMode)
|
|
}
|
|
}
|
|
|
|
func TestGenerator_TokenIsURLSafe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
gen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
// Check that token only contains URL-safe characters
|
|
for _, c := range token {
|
|
isURLSafe := (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
c == '-' || c == '_'
|
|
if !isURLSafe {
|
|
t.Errorf("Token contains non-URL-safe character: %q", c)
|
|
}
|
|
}
|
|
}
|