f2c534f636
- mongo-driver v2 connection with bounded startup retry + /readyz gate - idempotent creation of all §4.9 indexes - UpdateVersioned: version-filtered updates, conflict vs not-found errors - AES-256-GCM Seal/Open for ticketing credentials, base64(nonce|ct) - GridFS file store: MIME sniffing, MAX_UPLOAD_MB cap, sha256 metadata - HMAC-signed short-lived file URL tokens (§5.1, 1h TTL) - integration tests against compose Mongo via docker-compose.test.yml overlay Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.0 KiB
Go
62 lines
2.0 KiB
Go
// Package files implements upload storage (GridFS) and the short-lived
|
|
// HMAC-signed file URLs from §5.1: /files/{id}?st=<token> lets external
|
|
// services (atomizer, work performer) fetch attachments without a session.
|
|
package files
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// DefaultTokenTTL is the §5.1 signed-URL lifetime.
|
|
const DefaultTokenTTL = time.Hour
|
|
|
|
// signingKey derives a dedicated key from the session secret so file tokens
|
|
// can never be confused with anything else signed by the same secret.
|
|
func signingKey(secret []byte) []byte {
|
|
h := sha256.New()
|
|
h.Write([]byte("bountyboard/file-url/v1"))
|
|
h.Write(secret)
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
func mac(key []byte, fileID string, exp int64) []byte {
|
|
m := hmac.New(sha256.New, key)
|
|
fmt.Fprintf(m, "%s|%d", fileID, exp)
|
|
return m.Sum(nil)
|
|
}
|
|
|
|
// SignToken returns the st token granting access to fileID until expiry.
|
|
// Format: base64url(bigendian-int64-exp || hmac-sha256(fileID|exp)).
|
|
func SignToken(secret []byte, fileID string, expiresAt time.Time) string {
|
|
exp := expiresAt.Unix()
|
|
key := signingKey(secret)
|
|
buf := make([]byte, 8, 8+sha256.Size)
|
|
binary.BigEndian.PutUint64(buf, uint64(exp))
|
|
buf = append(buf, mac(key, fileID, exp)...)
|
|
return base64.RawURLEncoding.EncodeToString(buf)
|
|
}
|
|
|
|
// VerifyToken reports whether token grants access to fileID at time now.
|
|
func VerifyToken(secret []byte, fileID, token string, now time.Time) bool {
|
|
raw, err := base64.RawURLEncoding.DecodeString(token)
|
|
if err != nil || len(raw) != 8+sha256.Size {
|
|
return false
|
|
}
|
|
exp := int64(binary.BigEndian.Uint64(raw[:8]))
|
|
if now.Unix() > exp {
|
|
return false
|
|
}
|
|
expected := mac(signingKey(secret), fileID, exp)
|
|
return hmac.Equal(raw[8:], expected)
|
|
}
|
|
|
|
// SignedURL builds the absolute fetch URL for a stored file.
|
|
func SignedURL(appBaseURL string, secret []byte, fileID string, expiresAt time.Time) string {
|
|
return fmt.Sprintf("%s/files/%s?st=%s", appBaseURL, fileID, SignToken(secret, fileID, expiresAt))
|
|
}
|