Files
gpt-plus-gpt/pkg/stripe/sha256.go
2026-03-15 20:48:19 +08:00

286 lines
6.9 KiB
Go

package stripe
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"regexp"
"strings"
"time"
)
// SHA256URLSafe computes SHA-256 and returns URL-safe base64 (no padding).
// Matches JS: btoa(sha256_raw(input)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")
func SHA256URLSafe(input string) string {
h := sha256.Sum256([]byte(input))
return base64.RawURLEncoding.EncodeToString(h[:])
}
const urlSalt = "7766e861-8279-424d-87a1-07a6022fd8cd"
// SHA256WithSalt hashes input with the Stripe URL salt.
// Matches JS: sha256WithSalt which does sha256(unescape(encodeURIComponent(e)) + URL_SALT)
func SHA256WithSalt(input string) string {
if input == "" {
return ""
}
// JS unescape(encodeURIComponent(s)) converts to UTF-8 bytes, which Go strings already are.
return SHA256URLSafe(input + urlSalt)
}
// EncodeURIComponent mimics JavaScript's encodeURIComponent.
func EncodeURIComponent(s string) string {
result := url.QueryEscape(s)
result = strings.ReplaceAll(result, "+", "%20")
// JS encodeURIComponent does NOT encode: - _ . ! ~ * ' ( )
for _, c := range []string{"!", "'", "(", ")", "*", "~"} {
result = strings.ReplaceAll(result, url.QueryEscape(c), c)
}
return result
}
// ---------- URL Hashing ----------
const (
defaultFullHashLimit = 10
totalPartsLimit = 40
partialHashLen = 6
pathPartsLimit = 30
)
func isStripeAuthority(s string) bool {
if s == "//stripe.com" || s == "//stripe.com." {
return true
}
return strings.HasSuffix(s, ".stripe.com") || strings.HasSuffix(s, ".stripe.com.")
}
func isStripeCheckoutAuthority(s string) bool {
candidates := []string{
"//checkout.stripe.com",
"//qa-checkout.stripe.com",
"//edge-checkout.stripe.com",
}
for _, c := range candidates {
if s == c {
return true
}
}
return false
}
// removeUserInfo strips user-info from an authority component.
func removeUserInfo(e string) string {
if e == "" {
return e
}
t := strings.LastIndex(e, "@")
if t == -1 {
return e
}
return e[:2] + e[t+1:] // keep "//" prefix
}
// PartitionedUrl parses a URL into RFC-3986 components.
type PartitionedUrl struct {
Scheme string
Authority string
Path string
Query string
Fragment string
}
var urlParseRe = regexp.MustCompile(`^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?`)
// NewPartitionedUrl parses a URL string.
func NewPartitionedUrl(rawURL string) *PartitionedUrl {
p := &PartitionedUrl{}
if rawURL == "" {
return p
}
m := urlParseRe.FindStringSubmatch(rawURL)
if m == nil {
return p
}
p.Scheme = m[1] // e.g. "https:"
if m[3] != "" {
p.Authority = removeUserInfo(m[3]) // e.g. "//chatgpt.com"
}
p.Path = m[5] // e.g. "/"
p.Query = m[6] // e.g. "?foo=bar"
p.Fragment = m[8] // e.g. "#section"
return p
}
func (p *PartitionedUrl) String() string {
var parts []string
for _, s := range []string{p.Scheme, p.Authority, p.Path, p.Query, p.Fragment} {
if s != "" {
parts = append(parts, s)
}
}
return strings.Join(parts, "")
}
// SequentialHashWithLimit manages individual segment hashing with full/partial limits.
type SequentialHashWithLimit struct {
s string
cur int
hashedCount int
fullHashLimit int
totalHashLimit int
}
func newSequentialHashWithLimit(s string, fullHashLimit, totalHashLimit int) *SequentialHashWithLimit {
return &SequentialHashWithLimit{
s: s,
cur: 0,
hashedCount: 0,
fullHashLimit: fullHashLimit,
totalHashLimit: totalHashLimit,
}
}
func (h *SequentialHashWithLimit) shouldHash() bool {
return h.hashedCount < h.totalHashLimit
}
func (h *SequentialHashWithLimit) isLastHash() bool {
return h.hashedCount == h.totalHashLimit-1
}
func (h *SequentialHashWithLimit) shouldPartialHash() bool {
return !h.isLastHash() && h.hashedCount >= h.fullHashLimit
}
func (h *SequentialHashWithLimit) replace(e string) {
t := e
n := strings.Index(h.s[h.cur:], e)
if n == -1 {
return
}
n += h.cur
if h.isLastHash() {
t = h.s[n:]
}
r := SHA256WithSalt(t)
if h.shouldPartialHash() {
if len(r) > partialHashLen {
r = r[:partialHashLen]
}
}
h.s = h.s[:n] + r + h.s[n+len(t):]
h.cur = n + len(r)
h.hashedCount++
}
// SequentialSplitterAndHasher tracks remaining hash budget across URL parts.
type SequentialSplitterAndHasher struct {
remainingHashes int
fullHashLimit int
}
func newSequentialSplitterAndHasher(fullHashLimit int) *SequentialSplitterAndHasher {
return &SequentialSplitterAndHasher{
remainingHashes: totalPartsLimit,
fullHashLimit: fullHashLimit,
}
}
func (s *SequentialSplitterAndHasher) getFullHashLimit(part string) int {
if part == "authority" {
return totalPartsLimit
}
return s.fullHashLimit
}
func (s *SequentialSplitterAndHasher) totalHashLimitForPart(part string) int {
switch part {
case "authority":
return totalPartsLimit
case "path":
v := s.remainingHashes
if v > pathPartsLimit {
v = pathPartsLimit
}
if v < 1 {
v = 1
}
return v
case "query", "fragment":
if s.remainingHashes < 1 {
return 1
}
return s.remainingHashes
default:
return 0
}
}
func (s *SequentialSplitterAndHasher) splitAndHash(input, partType string, delim *regexp.Regexp) string {
if partType == "authority" && input != "" && isStripeCheckoutAuthority(input) {
return input
}
if input == "" {
return input
}
h := newSequentialHashWithLimit(input, s.getFullHashLimit(partType), s.totalHashLimitForPart(partType))
// Split by delimiter, filter empty strings
segments := delim.Split(input, -1)
for _, seg := range segments {
if seg == "" {
continue
}
if h.shouldHash() {
h.replace(seg)
}
}
s.remainingHashes -= h.hashedCount
return h.s
}
// hashURL hashes URL components using SequentialSplitterAndHasher.
func hashURL(p *PartitionedUrl, fullHashLimit int) *PartitionedUrl {
r := newSequentialSplitterAndHasher(fullHashLimit)
authorityRe := regexp.MustCompile(`[/.:]`)
otherRe := regexp.MustCompile(`[/#?!&+,=]`)
p.Authority = r.splitAndHash(p.Authority, "authority", authorityRe)
p.Path = r.splitAndHash(p.Path, "path", otherRe)
p.Query = r.splitAndHash(p.Query, "query", otherRe)
p.Fragment = r.splitAndHash(p.Fragment, "fragment", otherRe)
return p
}
// hashURLWithAuthorityCheck applies Stripe-specific authority logic before hashing.
func hashURLWithAuthorityCheck(p *PartitionedUrl, fullHashLimit int) *PartitionedUrl {
n := p.Authority
if n != "" && isStripeCheckoutAuthority(n) {
return hashURL(p, defaultFullHashLimit)
}
if n != "" && isStripeAuthority(n) {
return p // don't hash stripe.com URLs
}
return hashURL(p, fullHashLimit)
}
// HashURL is the public entry point for URL hashing.
func HashURL(urlStr string) string {
p := NewPartitionedUrl(urlStr)
return hashURLWithAuthorityCheck(p, defaultFullHashLimit).String()
}
// GetHashTimestampWithSalt returns "timestamp:hash" string.
func GetHashTimestampWithSalt(randomValue string) string {
now := time.Now().UnixMilli()
hash := SHA256URLSafe(randomValue + fmt.Sprintf("%d", now+1))
return fmt.Sprintf("%d:%s", now, hash)
}