diff --git a/hash.go b/hash.go index 96380fb..1c6b50e 100644 --- a/hash.go +++ b/hash.go @@ -14,6 +14,8 @@ import ( "io" "os/exec" "strings" + + "mvdan.cc/garble/internal/literals" ) const buildIDSeparator = "/" @@ -144,6 +146,9 @@ func appendFlags(w io.Writer, forBuildHash bool) { io.WriteString(w, " -seed=") io.WriteString(w, flagSeed.String()) } + if literals.TestObfuscator != "" && forBuildHash { + io.WriteString(w, literals.TestObfuscator) + } } // buildIDComponentLength is the number of bytes each build ID component takes, diff --git a/internal/literals/literals.go b/internal/literals/literals.go index 534faa3..0549bad 100644 --- a/internal/literals/literals.go +++ b/internal/literals/literals.go @@ -31,7 +31,8 @@ const minSize = 8 const maxSize = 2 << 10 // KiB // Obfuscate replaces literals with obfuscated anonymous functions. -func Obfuscate(obfRand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File { +func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File { + obfRand := newObfRand(rand, file) pre := func(cursor *astutil.Cursor) bool { switch node := cursor.Node().(type) { case *ast.GenDecl: @@ -122,7 +123,7 @@ func Obfuscate(obfRand *mathrand.Rand, file *ast.File, info *types.Info, linkStr // be used to replace it. // // If the input node cannot be obfuscated nil is returned. -func handleCompositeLiteral(obfRand *mathrand.Rand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { +func handleCompositeLiteral(obfRand *obfRand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { if len(node.Elts) < minSize || len(node.Elts) > maxSize { return nil } @@ -216,18 +217,18 @@ func withPos(node ast.Node, pos token.Pos) ast.Node { return node } -func obfuscateString(obfRand *mathrand.Rand, data string) *ast.CallExpr { - obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] - block := obfuscator.obfuscate(obfRand, []byte(data)) +func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr { + obfuscator := obfRand.nextObfuscator() + block := obfuscator.obfuscate(obfRand.Rand, []byte(data)) block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data")))) return ah.LambdaCall(ast.NewIdent("string"), block) } -func obfuscateByteSlice(obfRand *mathrand.Rand, isPointer bool, data []byte) *ast.CallExpr { - obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] - block := obfuscator.obfuscate(obfRand, data) +func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.CallExpr { + obfuscator := obfRand.nextObfuscator() + block := obfuscator.obfuscate(obfRand.Rand, data) if isPointer { block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{ @@ -243,9 +244,9 @@ func obfuscateByteSlice(obfRand *mathrand.Rand, isPointer bool, data []byte) *as return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block) } -func obfuscateByteArray(obfRand *mathrand.Rand, isPointer bool, data []byte, length int64) *ast.CallExpr { - obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] - block := obfuscator.obfuscate(obfRand, data) +func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length int64) *ast.CallExpr { + obfuscator := obfRand.nextObfuscator() + block := obfuscator.obfuscate(obfRand.Rand, data) arrayType := &ast.ArrayType{ Len: ah.IntLit(int(length)), diff --git a/internal/literals/obfuscators.go b/internal/literals/obfuscators.go index 24e66a6..dc026ac 100644 --- a/internal/literals/obfuscators.go +++ b/internal/literals/obfuscators.go @@ -15,14 +15,19 @@ type obfuscator interface { obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt } -// obfuscators contains all types which implement the obfuscator Interface -var obfuscators = []obfuscator{ - simple{}, - swap{}, - split{}, - shuffle{}, - // seed{}, TODO: re-enable once https://go.dev/issue/47631 is fixed in Go 1.20 -} +var ( + // Obfuscators contains all types which implement the obfuscator Interface + Obfuscators = []obfuscator{ + simple{}, + swap{}, + split{}, + shuffle{}, + // seed{}, TODO: re-enable once https://go.dev/issue/47631 is fixed in Go 1.20 + } + + TestObfuscator string + testPkgToObfuscatorMap map[string]obfuscator +) func genRandIntSlice(obfRand *mathrand.Rand, max, count int) []int { indexes := make([]int, count) @@ -66,3 +71,20 @@ func operatorToReversedBinaryExpr(t token.Token, x, y ast.Expr) *ast.BinaryExpr return expr } + +type obfRand struct { + *mathrand.Rand + testObfuscator obfuscator +} + +func (r *obfRand) nextObfuscator() obfuscator { + if r.testObfuscator != nil { + return r.testObfuscator + } + return Obfuscators[r.Intn(len(Obfuscators))] +} + +func newObfRand(rand *mathrand.Rand, file *ast.File) *obfRand { + testObf := testPkgToObfuscatorMap[file.Name.Name] + return &obfRand{rand, testObf} +} diff --git a/internal/literals/random_testing.go b/internal/literals/random_testing.go new file mode 100644 index 0000000..179b137 --- /dev/null +++ b/internal/literals/random_testing.go @@ -0,0 +1,31 @@ +//go:build garble_testing + +package literals + +import ( + "os" + "strconv" + "strings" +) + +func init() { + obfMapEnv := os.Getenv("GARBLE_TEST_LITERALS_OBFUSCATOR_MAP") + if obfMapEnv == "" { + panic("literals obfuscator map required for testing build") + } + testPkgToObfuscatorMap = make(map[string]obfuscator) + + // Parse obfuscator mapping: pkgName1=obfIndex1,pkgName2=obfIndex2 + pairs := strings.Split(obfMapEnv, ",") + for _, pair := range pairs { + keyValue := strings.SplitN(pair, "=", 2) + + pkgName := keyValue[0] + obfIndex, err := strconv.Atoi(keyValue[1]) + if err != nil { + panic(err) + } + testPkgToObfuscatorMap[pkgName] = Obfuscators[obfIndex] + } + TestObfuscator = obfMapEnv +} diff --git a/scripts/bench_literals.go b/scripts/bench_literals.go new file mode 100644 index 0000000..5e54c2b --- /dev/null +++ b/scripts/bench_literals.go @@ -0,0 +1,158 @@ +// This script generate benchmarks for performance analysis of individual obfuscator literals. +// Note that only the speed of obfuscated methods is measured, initialization cost or build speed are not measured. + +package main + +import ( + _ "embed" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + + "mvdan.cc/garble/internal/literals" +) + +const ( + minDataLen = 8 + maxDataLen = 64 + stepDataLen = 4 + dataCountPerLen = 10 + moduleName = "test/literals" + garbleSeed = "o9WDTZ4CN4w" + + // For benchmarking individual obfuscators, we use package=obfuscator mapping + // and add a prefix to package name to make sure there are no collisions with system packages. + packagePrefix = "literals_bench_" +) + +func generateRunSrc() string { + var sb strings.Builder + + sb.WriteString(` +var alwaysFalseFlag = false + +func noop(i interface{}) { + if alwaysFalseFlag { + println(i) + } +} + +func Run() { +`) + + dataStr := strings.Repeat("X", maxDataLen) + dataBytes := make([]string, maxDataLen) + for i := 0; i < len(dataBytes); i++ { + dataBytes[i] = strconv.Itoa(i) + } + + for dataLen := minDataLen; dataLen <= maxDataLen; dataLen += stepDataLen { + for y := 0; y < dataCountPerLen; y++ { + fmt.Fprintf(&sb, "\tnoop(%q)\n", dataStr[:dataLen]) + fmt.Fprintf(&sb, "\tnoop([]byte{%s})\n", strings.Join(dataBytes[:dataLen], ", ")) + } + } + + sb.WriteString("}\n") + return sb.String() +} + +func buildTestGarble(tdir string) string { + garbleBin := filepath.Join(tdir, "garble") + if runtime.GOOS == "windows" { + garbleBin += ".exe" + } + + output, err := exec.Command("go", "build", "-tags", "garble_testing", "-o="+garbleBin).CombinedOutput() + if err != nil { + log.Fatalf("garble build failed: %v\n%s", err, string(output)) + } + + return garbleBin +} + +func handle(err error) { + if err != nil { + panic(err) + } +} + +func writeDateFile(tdir, obfName, src string) { + pkgName := packagePrefix + obfName + + var sb strings.Builder + fmt.Fprintf(&sb, "package %s\n\n", pkgName) + sb.WriteString(src) + + dir := filepath.Join(tdir, pkgName) + handle(os.MkdirAll(dir, 0o777)) + handle(os.WriteFile(filepath.Join(dir, "data.go"), []byte(sb.String()), 0o777)) +} + +func writeTestFile(dir, obfName string) { + var sb strings.Builder + sb.WriteString(`package main +import "testing" +`) + pkgName := packagePrefix + obfName + fmt.Fprintf(&sb, "import %q\n", moduleName+"/"+pkgName) + fmt.Fprintf(&sb, `func Benchmark%s(b *testing.B) { + for i := 0; i < b.N; i++ { + %s.Run() + } +} +`, strings.ToUpper(obfName[:1])+obfName[1:], pkgName) + + handle(os.WriteFile(filepath.Join(dir, obfName+"_test.go"), []byte(sb.String()), 0o777)) +} + +func main() { + tdir, err := os.MkdirTemp("", "literals-bench*") + if err != nil { + log.Fatalf("create temp directory failed: %v", err) + } + defer os.RemoveAll(tdir) + + if err := os.WriteFile(filepath.Join(tdir, "go.mod"), []byte("module "+moduleName), 0o777); err != nil { + log.Fatalf("write go.mod failed: %v", err) + } + + runSrc := generateRunSrc() + writeTest := func(name string) { + writeDateFile(tdir, name, runSrc) + writeTestFile(tdir, name) + } + + var packageToObfuscatorIndex []string + for i, obf := range literals.Obfuscators { + obfName := reflect.TypeOf(obf).Name() + writeTest(obfName) + packageToObfuscatorIndex = append(packageToObfuscatorIndex, fmt.Sprintf(packagePrefix+"%s=%d", obfName, i)) + } + writeTest("all") + + garbleBin := buildTestGarble(tdir) + args := append([]string{"-seed", garbleSeed, "-literals", "test", "-bench"}, os.Args[1:]...) + cmd := exec.Command(garbleBin, args...) + cmd.Env = append(os.Environ(), + // Explicitly specify package for obfuscation to avoid affecting testing package. + "GOGARBLE="+moduleName, + "GARBLE_TEST_LITERALS_OBFUSCATOR_MAP="+strings.Join(packageToObfuscatorIndex, ","), + ) + cmd.Dir = tdir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + log.Fatalf("run garble test failed: %v", err) + } +}