190 lines
4.8 KiB
Go
190 lines
4.8 KiB
Go
package stripe
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
mrand "math/rand"
|
|
"strings"
|
|
)
|
|
|
|
// randomHex generates n bytes of cryptographically random hex.
|
|
func randomHex(nBytes int) string {
|
|
b := make([]byte, nBytes)
|
|
_, _ = rand.Read(b)
|
|
return fmt.Sprintf("%x", b)
|
|
}
|
|
|
|
// generateRandomBinaryString generates a string of random 0s and 1s.
|
|
func generateRandomBinaryString(length int) string {
|
|
var sb strings.Builder
|
|
sb.Grow(length)
|
|
for i := 0; i < length; i++ {
|
|
n, _ := rand.Int(rand.Reader, big.NewInt(2))
|
|
sb.WriteByte('0' + byte(n.Int64()))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// transformFeatureValues converts extractedFeatures array to the keyed map format.
|
|
// [{v, t}, ...] -> {a: {v, t}, b: {v, t}, ...}
|
|
func transformFeatureValues(features [][]interface{}) map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
for i, f := range features {
|
|
key := string(rune('a' + i))
|
|
entry := map[string]interface{}{
|
|
"v": f[0],
|
|
"t": f[1],
|
|
}
|
|
if len(f) > 2 {
|
|
entry["at"] = f[2]
|
|
}
|
|
result[key] = entry
|
|
}
|
|
return result
|
|
}
|
|
|
|
// featuresSameLine joins all feature values with spaces (for MD5 id).
|
|
func featuresSameLine(features [][]interface{}) string {
|
|
parts := make([]string, len(features))
|
|
for i, f := range features {
|
|
parts[i] = fmt.Sprintf("%v", f[0])
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// domainToTabTitle returns a suitable tab title for the given domain.
|
|
func domainToTabTitle(domain string) string {
|
|
switch domain {
|
|
case "chatgpt.com":
|
|
return "ChatGPT"
|
|
case "discord.com":
|
|
return "Discord | Billing | User Settings"
|
|
default:
|
|
return domain
|
|
}
|
|
}
|
|
|
|
// CreateInitPayload builds the m.stripe.com/6 fingerprint payload.
|
|
// domain should be "chatgpt.com" (the merchant site).
|
|
// If browserFP is provided, real fingerprint data overrides hardcoded defaults.
|
|
func CreateInitPayload(userAgent, domain, tagVersion string, browserFP ...*BrowserFingerprint) map[string]interface{} {
|
|
if tagVersion == "" {
|
|
tagVersion = "4.5.43"
|
|
}
|
|
|
|
// Defaults (hardcoded)
|
|
language := "en-US"
|
|
platform := "Win32"
|
|
plugins := "Browser PDF plug-in,HqVxgvf2j4FKFpUSJjZUxg368mTJr8Hq,application/pdf,pdf, aNlxBIr0,ECozZzCJECozZrdO,,ZYz, OToct9e,Ar89HqVpzhQQvAn,,tiZ, JavaScript portable-document-format plug in,7CgQIMl5k5kxBAAIjRnb05FKNGqdWTw3,application/x-google-chrome-pdf,pdf"
|
|
screenSize := "1920w_1032h_24d_1r"
|
|
canvasHash := "b723b5fba9cb9289de8b7e1e6de668fd"
|
|
fontsBits := generateRandomBinaryString(55)
|
|
cookieSupport := "true"
|
|
doNotTrack := "false"
|
|
|
|
// Override with real fingerprint if available
|
|
if len(browserFP) > 0 && browserFP[0] != nil {
|
|
fp := browserFP[0]
|
|
if fp.Language != "" {
|
|
language = fp.Language
|
|
}
|
|
if fp.Platform != "" {
|
|
platform = fp.Platform
|
|
}
|
|
if fp.Plugins != "" {
|
|
plugins = fp.Plugins
|
|
}
|
|
if fp.ScreenSize != "" {
|
|
screenSize = fp.ScreenSize
|
|
}
|
|
if fp.CanvasHash != "" {
|
|
canvasHash = fp.CanvasHash
|
|
}
|
|
if fp.FontsBits != "" {
|
|
fontsBits = fp.FontsBits
|
|
}
|
|
if fp.CookieSupport {
|
|
cookieSupport = "true"
|
|
}
|
|
if fp.DoNotTrack {
|
|
doNotTrack = "true"
|
|
}
|
|
if fp.UserAgent != "" {
|
|
userAgent = fp.UserAgent
|
|
}
|
|
}
|
|
|
|
extractedFeatures := [][]interface{}{
|
|
{cookieSupport, 0},
|
|
{doNotTrack, 0},
|
|
{language, 0},
|
|
{platform, 0},
|
|
{plugins, 19},
|
|
{screenSize, 0},
|
|
{"1", 0},
|
|
{"false", 0},
|
|
{"sessionStorage-enabled, localStorage-enabled", 3},
|
|
{fontsBits, 85},
|
|
{"", 0},
|
|
{userAgent, 0},
|
|
{"", 0},
|
|
{"false", 85, 1},
|
|
{canvasHash, 83},
|
|
}
|
|
|
|
randomVal := randomHex(10) // 20 hex chars from 10 bytes
|
|
|
|
urlToHash := fmt.Sprintf("https://%s/", domain)
|
|
hashedURL := HashURL(urlToHash)
|
|
|
|
joined := featuresSameLine(extractedFeatures)
|
|
featureID := fmt.Sprintf("%x", md5.Sum([]byte(joined)))
|
|
|
|
tabTitle := domainToTabTitle(domain)
|
|
|
|
// Random timing values matching JS: Math.floor(Math.random() * (350 - 200 + 1) + 200)
|
|
t := mrand.Intn(151) + 200 // 200-350
|
|
n := mrand.Intn(251) + 100 // 100-350
|
|
|
|
return map[string]interface{}{
|
|
"v2": 1,
|
|
"id": featureID,
|
|
"t": t,
|
|
"tag": tagVersion,
|
|
"src": "js",
|
|
"a": transformFeatureValues(extractedFeatures),
|
|
"b": map[string]interface{}{
|
|
"a": hashedURL,
|
|
"b": hashedURL,
|
|
"c": SHA256WithSalt(tabTitle),
|
|
"d": "NA",
|
|
"e": "NA",
|
|
"f": false,
|
|
"g": true,
|
|
"h": true,
|
|
"i": []string{"location"},
|
|
"j": []interface{}{},
|
|
"n": n,
|
|
"u": domain,
|
|
"v": domain,
|
|
"w": GetHashTimestampWithSalt(randomVal),
|
|
},
|
|
"h": randomVal,
|
|
}
|
|
}
|
|
|
|
// EncodePayload JSON-encodes, encodeURIComponent-encodes, then base64-encodes the payload.
|
|
// Matches JS: Buffer.from(encodeURIComponent(JSON.stringify(payload))).toString('base64')
|
|
func EncodePayload(payload map[string]interface{}) (string, error) {
|
|
jsonBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal payload: %w", err)
|
|
}
|
|
encoded := EncodeURIComponent(string(jsonBytes))
|
|
return base64.StdEncoding.EncodeToString([]byte(encoded)), nil
|
|
}
|