Initial sanitized code sync
This commit is contained in:
285
pkg/stripe/sha256.go
Normal file
285
pkg/stripe/sha256.go
Normal file
@@ -0,0 +1,285 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user