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
131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"image/jpeg"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
)
|
|
|
|
// requireServedPhoto requires that rec answers 200 with the JPEG at photoPath
|
|
// on signedHost at the 50x50 that encPhotoURL and the generator tests ask for.
|
|
func requireServedPhoto(t *testing.T, rec *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body %q",
|
|
rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
contentType := rec.Header().Get("Content-Type")
|
|
if contentType != "image/jpeg" {
|
|
t.Errorf("Content-Type = %q, want image/jpeg", contentType)
|
|
}
|
|
|
|
img, err := jpeg.DecodeConfig(rec.Body)
|
|
if err != nil {
|
|
t.Fatalf("body is not a JPEG: %v", err)
|
|
}
|
|
|
|
if img.Width != 50 || img.Height != 50 {
|
|
t.Errorf("image is %dx%d, want 50x50", img.Width, img.Height)
|
|
}
|
|
}
|
|
|
|
// TestHandleImageEnc_ValidToken_ServesImage verifies that a token made with
|
|
// the signing key serves the image it asks for.
|
|
func TestHandleImageEnc_ValidToken_ServesImage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
|
|
|
rec := httptest.NewRecorder()
|
|
srv.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, encPhotoURL(t, h), nil))
|
|
|
|
requireServedPhoto(t, rec)
|
|
}
|
|
|
|
// TestHandleImageEnc_RejectedToken verifies that a token that has expired
|
|
// answers 410, and that a token with one character changed, a token cut
|
|
// short, and a token made with another signing key answer 400. The server
|
|
// would serve the photo for a token it accepted.
|
|
func TestHandleImageEnc_RejectedToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
|
|
|
photo := imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: signedHost,
|
|
Path: photoPath,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 50, Height: 50},
|
|
},
|
|
}
|
|
|
|
valid, err := h.encGen.Generate(&photo, time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
expired, err := h.encGen.Generate(&photo, time.Now().Add(-time.Minute))
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
keys, err := seal.DeriveKeys("another-signing-key-fedcba9876543210")
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
otherGen := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
otherKey, err := otherGen.Generate(&photo, time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
// Changing a character in the middle always changes the decoded bytes;
|
|
// the last character of unpadded base64 can carry unused bits.
|
|
middle := len(valid) / 2
|
|
|
|
replacement := "A"
|
|
if valid[middle] == 'A' {
|
|
replacement = "B"
|
|
}
|
|
|
|
changed := valid[:middle] + replacement + valid[middle+1:]
|
|
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
wantStatus int
|
|
}{
|
|
{"expired", expired, http.StatusGone},
|
|
{"one character changed", changed, http.StatusBadRequest},
|
|
{"cut short", valid[:middle], http.StatusBadRequest},
|
|
{"another signing key", otherKey, http.StatusBadRequest},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rec := getEncToken(srv, tt.token)
|
|
t.Logf("GET /v1/e/%s/img.jpg: %d %s", tt.token, rec.Code, rec.Body)
|
|
|
|
if rec.Code != tt.wantStatus {
|
|
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|