package utils import ( "bytes" "compress/gzip" "crypto/rand" "encoding/base64" "fmt" "math/big" "strings" "text/template" ) // ObfuscationConfig controls how the obfuscation behaves type ObfuscationConfig struct { // MinSplits is the minimum number of parts to split strings into MinSplits int // MaxSplits is the maximum number of parts to split strings into MaxSplits int // UseNumberSuffix determines if variable names should have number suffixes UseNumberSuffix bool // MinNumberSuffix is the minimum value for number suffixes MinNumberSuffix int // MaxNumberSuffix is the maximum value for number suffixes MaxNumberSuffix int // UseXOR determines if strings should be XOR encrypted UseXOR bool // MinXORKey is the minimum XOR key value (1-255) MinXORKey int // MaxXORKey is the maximum XOR key value (1-255) MaxXORKey int } // DefaultObfuscationConfig returns sensible defaults for obfuscation func DefaultObfuscationConfig() ObfuscationConfig { return ObfuscationConfig{ MinSplits: 2, MaxSplits: 4, UseNumberSuffix: true, MinNumberSuffix: 0, MaxNumberSuffix: 9, UseXOR: true, MinXORKey: 1, MaxXORKey: 255, } } // ObfuscateHTML obfuscates HTML content using compression, base64 encoding, // and random variable names to make it difficult to fingerprint func ObfuscateHTML(html string, config ObfuscationConfig, htmlTemplate string, funcMap template.FuncMap) (string, error) { // generate random variable names varNames := generateRandomVariableNames(15, config) xorFuncName := varNames[9] windowVar := varNames[10] // xor function variables use indices 11-14 to avoid conflicts with windowVar xorVars := []string{varNames[11], varNames[12], varNames[13], varNames[14]} // randomly select method to access window object windowAccessor := getRandomWindowAccessor() // compress the HTML to reduce size and add another layer compressed, err := compressGzip([]byte(html)) if err != nil { return "", fmt.Errorf("failed to compress html: %w", err) } // encode to base64 encoded := base64.StdEncoding.EncodeToString(compressed) // split the base64 payload encodedSplit := splitStringRandom(encoded, config, xorFuncName) // split critical strings t atobSplit := splitStringRandom("atob", config, xorFuncName) uint8ArraySplit := splitStringRandom("Uint8Array", config, xorFuncName) fromSplit := splitStringRandom("from", config, xorFuncName) charCodeAtSplit := splitStringRandom("charCodeAt", config, xorFuncName) responseSplit := splitStringRandom("Response", config, xorFuncName) bufferSplit := splitStringRandom("buffer", config, xorFuncName) bodySplit := splitStringRandom("body", config, xorFuncName) pipeThroughSplit := splitStringRandom("pipeThrough", config, xorFuncName) decompressionStreamSplit := splitStringRandom("DecompressionStream", config, xorFuncName) gzipSplit := splitStringRandom("gzip", config, xorFuncName) textSplit := splitStringRandom("text", config, xorFuncName) thenSplit := splitStringRandom("then", config, xorFuncName) documentSplit := splitStringRandom("document", config, xorFuncName) openSplit := splitStringRandom("open", config, xorFuncName) writeSplit := splitStringRandom("write", config, xorFuncName) closeSplit := splitStringRandom("close", config, xorFuncName) // create xor helper function if needed xorFunc := "" if config.UseXOR { // obfuscate the xor function internals (xorVars already generated above to avoid conflicts) // create a minimal config without xor to avoid recursion noXorConfig := config noXorConfig.UseXOR = false fromCharCodeSplit := splitStringRandom("fromCharCode", noXorConfig, "") parseIntSplit := splitStringRandom("parseInt", noXorConfig, "") substrSplit := splitStringRandom("substr", noXorConfig, "") lengthSplit := splitStringRandom("length", noXorConfig, "") xorFunc = fmt.Sprintf(`function %s(%s,%s){var %s='';for(var %s=0;%s<%s[%s];%s+=2)%s+=String[%s](%s[%s](%s[%s](%s,2),16)^%s);return %s;}`, xorFuncName, xorVars[0], xorVars[1], xorVars[2], xorVars[3], xorVars[3], xorVars[0], lengthSplit, xorVars[3], xorVars[2], fromCharCodeSplit, windowVar, parseIntSplit, xorVars[0], substrSplit, xorVars[3], xorVars[1], xorVars[2]) } // create the deobfuscation script with heavily obfuscated strings (minified) deobfScript := fmt.Sprintf(`%svar %s=%s;var %s=%s;var %s=%s[%s](%s);var %s=%s[%s][%s](%s,function(%s){return %s[%s](0);});var %s=new %s[%s](%s[%s])[%s][%s](new %s[%s](%s));var %s=new %s[%s](%s);%s[%s]()[%s](function(%s){%s[%s][%s]();%s[%s][%s](%s);%s[%s][%s]();});`, xorFunc, windowVar, windowAccessor, varNames[0], encodedSplit, varNames[1], windowVar, atobSplit, varNames[0], varNames[2], windowVar, uint8ArraySplit, fromSplit, varNames[1], varNames[8], varNames[8], charCodeAtSplit, varNames[3], windowVar, responseSplit, varNames[2], bufferSplit, bodySplit, pipeThroughSplit, windowVar, decompressionStreamSplit, gzipSplit, varNames[4], windowVar, responseSplit, varNames[3], varNames[4], textSplit, thenSplit, varNames[5], windowVar, documentSplit, openSplit, windowVar, documentSplit, writeSplit, varNames[5], windowVar, documentSplit, closeSplit) // render the html template with the deobfuscation script tmpl, err := template.New("obfuscation").Funcs(funcMap).Parse(htmlTemplate) if err != nil { return "", fmt.Errorf("failed to parse obfuscation template: %w", err) } var buf bytes.Buffer data := map[string]interface{}{ "Script": deobfScript, } err = tmpl.Execute(&buf, data) if err != nil { return "", fmt.Errorf("failed to execute obfuscation template: %w", err) } return buf.String(), nil } // getRandomWindowAccessor returns a random way to access the window object func getRandomWindowAccessor() string { accessors := []string{ "self", "this", "globalThis", "Function('return this')()", "(function(){return this})()", "(0,eval)('this')", } // randomly select one idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(accessors)))) return accessors[idx.Int64()] } // xorString encrypts a string with XOR using the given key and returns hex encoded string func xorString(s string, key byte) string { var result strings.Builder for i := 0; i < len(s); i++ { result.WriteString(fmt.Sprintf("%02x", s[i]^key)) } return result.String() } // splitStringRandom splits a string into random parts and returns a concatenation expression func splitStringRandom(s string, config ObfuscationConfig, xorFuncName string) string { if len(s) <= 1 { return fmt.Sprintf(`"%s"`, s) } // determine number of splits based on config minParts := config.MinSplits maxParts := config.MaxSplits if minParts < 1 { minParts = 1 } if maxParts < minParts { maxParts = minParts } rangeSize := maxParts - minParts + 1 numParts, _ := rand.Int(rand.Reader, big.NewInt(int64(rangeSize))) parts := int(numParts.Int64()) + minParts if parts > len(s) { parts = len(s) } // generate random split positions positions := make([]int, 0, parts-1) for i := 0; i < parts-1; i++ { maxPos := int64(len(s) - 1) if maxPos < 1 { break } pos, _ := rand.Int(rand.Reader, big.NewInt(maxPos)) positions = append(positions, int(pos.Int64())+1) } // sort positions to split correctly // bubble sort since we have few elements for i := 0; i < len(positions); i++ { for j := i + 1; j < len(positions); j++ { if positions[i] > positions[j] { positions[i], positions[j] = positions[j], positions[i] } } } // remove duplicates and ensure boundaries uniquePositions := make([]int, 0) lastPos := 0 for _, pos := range positions { if pos > lastPos && pos < len(s) { uniquePositions = append(uniquePositions, pos) lastPos = pos } } // build the split string parts with optional XOR encryption var result strings.Builder start := 0 for i, pos := range uniquePositions { if i > 0 { result.WriteString(" + ") } part := s[start:pos] if config.UseXOR { // generate random XOR key within configured range keyRange := config.MaxXORKey - config.MinXORKey + 1 if keyRange < 1 { keyRange = 1 } xorKey, _ := rand.Int(rand.Reader, big.NewInt(int64(keyRange))) key := byte(int(xorKey.Int64()) + config.MinXORKey) // xor encrypt the part encrypted := xorString(part, key) result.WriteString(fmt.Sprintf(`%s("%s",%d)`, xorFuncName, encrypted, key)) } else { result.WriteString(fmt.Sprintf(`"%s"`, part)) } start = pos } // add the last part if len(uniquePositions) > 0 { result.WriteString(" + ") } lastPart := s[start:] if config.UseXOR { // generate random XOR key for last part keyRange := config.MaxXORKey - config.MinXORKey + 1 if keyRange < 1 { keyRange = 1 } xorKey, _ := rand.Int(rand.Reader, big.NewInt(int64(keyRange))) key := byte(int(xorKey.Int64()) + config.MinXORKey) // xor encrypt the last part encrypted := xorString(lastPart, key) result.WriteString(fmt.Sprintf(`%s("%s",%d)`, xorFuncName, encrypted, key)) } else { result.WriteString(fmt.Sprintf(`"%s"`, lastPart)) } return result.String() } // compressGzip compresses data using gzip func compressGzip(data []byte) ([]byte, error) { var buf bytes.Buffer writer := gzip.NewWriter(&buf) _, err := writer.Write(data) if err != nil { writer.Close() return nil, err } err = writer.Close() if err != nil { return nil, err } return buf.Bytes(), nil } // generateRandomVariableNames generates random variable names to prevent fingerprinting func generateRandomVariableNames(count int, config ObfuscationConfig) []string { // common but non-suspicious variable name prefixes prefixes := []string{ "a", "b", "c", "d", "v", "x", "i", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "u", "w", "y", "z", } names := make([]string, count) used := make(map[string]bool) for i := 0; i < count; i++ { for { // select random prefix prefixIdx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(prefixes)))) prefix := prefixes[prefixIdx.Int64()] var name string if config.UseNumberSuffix { // generate random suffix within configured range suffixRange := config.MaxNumberSuffix - config.MinNumberSuffix + 1 if suffixRange < 1 { suffixRange = 1 } suffix, _ := rand.Int(rand.Reader, big.NewInt(int64(suffixRange))) name = fmt.Sprintf("%s%d", prefix, int(suffix.Int64())+config.MinNumberSuffix) } else { name = prefix } // ensure uniqueness if !used[name] && !isReservedWord(name) { names[i] = name used[name] = true break } } } return names } // isReservedWord checks if a string is a JavaScript reserved word func isReservedWord(word string) bool { reserved := []string{ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof", "let", "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with", "yield", "enum", "await", "implements", "interface", "package", "private", "protected", "public", "static", } wordLower := strings.ToLower(word) for _, r := range reserved { if r == wordLower { return true } } return false } /* // decompressGzip decompresses gzip data (for testing purposes) func decompressGzip(data []byte) ([]byte, error) { reader, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer reader.Close() return io.ReadAll(reader) } */