Files
2026-05-31 19:48:56 +01:00

221 lines
4.3 KiB
Go

// Long string splitting transformation.
//
// Splits string literals longer than a threshold into concatenated chunks
// of randomized length (5-9 characters). This ensures that even if string
// encryption is bypassed, long sensitive values like GPG keys, API keys,
// and connection strings cannot be found via `strings` on the binary.
package transform
import (
"fmt"
"math/rand"
"strings"
)
const (
splitMinChunk = 5
splitMaxChunk = 9
splitMinLen = 20
)
// SplitLongStrings breaks string literals longer than splitMinLen into
// concatenated chunks of 5-9 characters each.
func SplitLongStrings(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
return splitStringLiterals(src, rng)
}
func splitStringLiterals(src string, rng *rand.Rand) string {
var buf strings.Builder
buf.Grow(len(src) * 2)
i := 0
inImport := false
parenImport := false
for i < len(src) {
// Track import block context
rest := src[i:]
if !inImport && strings.HasPrefix(rest, "import") {
afterImport := strings.TrimSpace(rest[6:])
if strings.HasPrefix(afterImport, "(") {
inImport = true
parenImport = true
} else if strings.HasPrefix(afterImport, "\"") {
inImport = true
parenImport = false
}
}
if inImport {
if parenImport && src[i] == ')' {
inImport = false
parenImport = false
} else if !parenImport && src[i] == '\n' {
inImport = false
}
buf.WriteByte(src[i])
i++
continue
}
if src[i] == '"' {
end := findStringEnd(src, i)
if end == -1 {
buf.WriteByte(src[i])
i++
continue
}
literal := src[i : end+1]
inner := literal[1 : len(literal)-1]
if len(inner) >= splitMinLen && !containsEscapes(inner) {
split := splitString(inner, rng)
buf.WriteString(split)
} else {
buf.WriteString(literal)
}
i = end + 1
} else if src[i] == '`' {
end := findBacktickEnd(src, i)
if end == -1 {
buf.WriteByte(src[i])
i++
continue
}
literal := src[i : end+1]
inner := literal[1 : len(literal)-1]
if len(inner) >= splitMinLen {
split := splitRawString(inner, rng)
buf.WriteString(split)
} else {
buf.WriteString(literal)
}
i = end + 1
} else {
buf.WriteByte(src[i])
i++
}
}
return buf.String()
}
func findStringEnd(src string, start int) int {
i := start + 1
for i < len(src) {
if src[i] == '\\' {
i += 2
continue
}
if src[i] == '"' {
return i
}
i++
}
return -1
}
func findBacktickEnd(src string, start int) int {
for i := start + 1; i < len(src); i++ {
if src[i] == '`' {
return i
}
}
return -1
}
func containsEscapes(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] == '\\' {
return true
}
}
return false
}
func splitString(s string, rng *rand.Rand) string {
chunks := splitIntoChunks(s, rng)
if len(chunks) <= 1 {
return `"` + s + `"`
}
var parts []string
for _, c := range chunks {
parts = append(parts, `"`+escapeString(c)+`"`)
}
return "(" + strings.Join(parts, " + ") + ")"
}
func splitRawString(s string, rng *rand.Rand) string {
chunks := splitIntoChunks(s, rng)
if len(chunks) <= 1 {
return "`" + s + "`"
}
var parts []string
for _, c := range chunks {
parts = append(parts, `"`+escapeString(c)+`"`)
}
return "(" + strings.Join(parts, " + ") + ")"
}
func splitIntoChunks(s string, rng *rand.Rand) []string {
runes := []rune(s)
if len(runes) < splitMinLen {
return []string{s}
}
var chunks []string
pos := 0
for pos < len(runes) {
remaining := len(runes) - pos
if remaining <= splitMaxChunk {
chunks = append(chunks, string(runes[pos:]))
break
}
chunkLen := splitMinChunk + rng.Intn(splitMaxChunk-splitMinChunk+1)
if remaining-chunkLen < splitMinChunk && remaining-chunkLen > 0 {
chunkLen = remaining / 2
if chunkLen < splitMinChunk {
chunkLen = splitMinChunk
}
}
chunks = append(chunks, string(runes[pos:pos+chunkLen]))
pos += chunkLen
}
return chunks
}
func escapeString(s string) string {
var buf strings.Builder
buf.Grow(len(s))
for _, r := range s {
switch r {
case '\\', '"':
buf.WriteRune('\\')
buf.WriteRune(r)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
case '\r':
buf.WriteString(`\r`)
default:
if r < 32 {
buf.WriteString(fmt.Sprintf(`\x%02x`, r))
} else {
buf.WriteRune(r)
}
}
}
return buf.String()
}