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 }