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
150 lines
4.2 KiB
Go
150 lines
4.2 KiB
Go
// Package encurl provides encrypted URL generation and parsing for pixa.
|
|
package encurl
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
)
|
|
|
|
// Default values for optional fields.
|
|
const (
|
|
DefaultQuality = 85
|
|
DefaultFormat = imagerequest.FormatJXL
|
|
DefaultFitMode = imagerequest.FitCover
|
|
)
|
|
|
|
// Errors returned by encurl operations.
|
|
var (
|
|
ErrExpired = errors.New("encrypted URL has expired")
|
|
ErrInvalidFormat = errors.New("invalid encrypted URL format")
|
|
ErrDecryptFailed = errors.New("failed to decrypt URL")
|
|
)
|
|
|
|
// payload contains all image request parameters in a compact format.
|
|
// Field names are short to minimize encoded size.
|
|
// Fields with omitempty use sane defaults when absent.
|
|
type payload struct {
|
|
SourceHost string `cbor:"h"` // required
|
|
SourcePath string `cbor:"p"` // required
|
|
SourceQuery string `cbor:"q,omitempty"` // optional
|
|
Width int `cbor:"w,omitempty"` // 0 = original
|
|
Height int `cbor:"ht,omitempty"` // 0 = original
|
|
Format imagerequest.Format `cbor:"f,omitempty"` // default: jxl
|
|
Quality int `cbor:"ql,omitempty"` // default: 85
|
|
FitMode imagerequest.Fit `cbor:"fm,omitempty"` // default: cover
|
|
ExpiresAt int64 `cbor:"e,omitempty"` // 0 = never expires
|
|
}
|
|
|
|
// Generator creates and parses encrypted URL tokens.
|
|
type Generator struct {
|
|
key [seal.KeySize]byte
|
|
}
|
|
|
|
// NewGenerator creates an encrypted URL generator that encrypts its tokens
|
|
// with key.
|
|
func NewGenerator(key [seal.KeySize]byte) *Generator {
|
|
return &Generator{key: key}
|
|
}
|
|
|
|
// Generate creates an encrypted URL token from the request's source and
|
|
// transform, each as the request holds it, and from expires; an empty format,
|
|
// a quality of 0 or an empty fit mode is left out of the token, and a zero
|
|
// expires makes a token that never expires. The token is CBOR-encoded,
|
|
// encrypted with NaCl secretbox, and base64url-encoded.
|
|
func (g *Generator) Generate(
|
|
req *imagerequest.Request, expires time.Time,
|
|
) (string, error) {
|
|
p := &payload{
|
|
SourceHost: req.Source.Host,
|
|
SourcePath: req.Source.Path,
|
|
SourceQuery: req.Source.Query,
|
|
Width: req.Transform.Size.Width,
|
|
Height: req.Transform.Size.Height,
|
|
Format: req.Transform.Format,
|
|
Quality: req.Transform.Quality,
|
|
FitMode: req.Transform.FitMode,
|
|
}
|
|
|
|
if !expires.IsZero() {
|
|
p.ExpiresAt = expires.Unix()
|
|
}
|
|
|
|
// CBOR encode the payload
|
|
data, err := cbor.Marshal(p)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Encrypt and base64url encode
|
|
return seal.Encrypt(g.key, data)
|
|
}
|
|
|
|
// Parse decrypts and validates an encrypted URL token and returns the
|
|
// request it holds, with default values for omitted optional fields, and its
|
|
// expiry. A token that never expires gives the zero time.
|
|
// Returns ErrExpired if the token has expired, or other errors for invalid tokens.
|
|
func (g *Generator) Parse(token string) (*imagerequest.Request, time.Time, error) {
|
|
// Decrypt
|
|
data, err := seal.Decrypt(g.key, token)
|
|
if err != nil {
|
|
if errors.Is(err, seal.ErrDecryptionFailed) ||
|
|
errors.Is(err, seal.ErrInvalidPayload) {
|
|
return nil, time.Time{}, ErrDecryptFailed
|
|
}
|
|
|
|
return nil, time.Time{}, err
|
|
}
|
|
|
|
// CBOR decode
|
|
var p payload
|
|
|
|
err = cbor.Unmarshal(data, &p)
|
|
if err != nil {
|
|
return nil, time.Time{}, ErrInvalidFormat
|
|
}
|
|
|
|
// Check expiration (0 = never expires)
|
|
if p.ExpiresAt != 0 && time.Now().Unix() > p.ExpiresAt {
|
|
return nil, time.Time{}, ErrExpired
|
|
}
|
|
|
|
format := p.Format
|
|
if format == "" {
|
|
format = DefaultFormat
|
|
}
|
|
|
|
quality := p.Quality
|
|
if quality == 0 {
|
|
quality = DefaultQuality
|
|
}
|
|
|
|
fitMode := p.FitMode
|
|
if fitMode == "" {
|
|
fitMode = DefaultFitMode
|
|
}
|
|
|
|
var expires time.Time
|
|
if p.ExpiresAt != 0 {
|
|
expires = time.Unix(p.ExpiresAt, 0)
|
|
}
|
|
|
|
return &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: p.SourceHost,
|
|
Path: p.SourcePath,
|
|
Query: p.SourceQuery,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: p.Width, Height: p.Height},
|
|
Format: format,
|
|
Quality: quality,
|
|
FitMode: fitMode,
|
|
},
|
|
}, expires, nil
|
|
}
|