// Package files implements upload storage (GridFS) and the short-lived // HMAC-signed file URLs from §5.1: /files/{id}?st= 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)) }