feat: store layer with indexes, optimistic concurrency, GridFS, credential crypto (phase 2)
- 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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user