diff --git a/hash.go b/hash.go index 2e29c22..0c17ffd 100644 --- a/hash.go +++ b/hash.go @@ -98,17 +98,27 @@ func addGarbleToHash(inputHash []byte) []byte { if cache.GOGARBLE != "" { fmt.Fprintf(h, " GOGARBLE=%s", cache.GOGARBLE) } - if opts.ObfuscateLiterals { - fmt.Fprintf(h, " -literals") + appendFlags(h) + return h.Sum(nil)[:buildIDComponentLength] +} + +// appendFlags writes garble's own flags to w in string form. +// Errors are ignored, as w is always a buffer or hasher. +func appendFlags(w io.Writer) { + if flagLiterals { + io.WriteString(w, " -literals") } - if opts.Tiny { - fmt.Fprintf(h, " -tiny") + if flagTiny { + io.WriteString(w, " -tiny") } - if len(opts.Seed) > 0 { - fmt.Fprintf(h, " -seed=%x", opts.Seed) + if flagDebugDir != "" { + io.WriteString(w, " -debugdir=") + io.WriteString(w, flagDebugDir) + } + if len(flagSeed.bytes) > 0 { + io.WriteString(w, " -seed=") + io.WriteString(w, flagSeed.String()) } - - return h.Sum(nil)[:buildIDComponentLength] } // buildIDComponentLength is the number of bytes each build ID component takes, @@ -196,7 +206,7 @@ func hashWith(salt []byte, name string) string { d := sha256.New() d.Write(salt) - d.Write(opts.Seed) + d.Write(flagSeed.bytes) io.WriteString(d, name) sum := make([]byte, nameBase64.EncodedLen(d.Size())) nameBase64.Encode(sum, d.Sum(nil)) diff --git a/main.go b/main.go index 8874aa7..a32d715 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "bytes" + "crypto/rand" "encoding/base64" "encoding/binary" "encoding/gob" @@ -19,7 +20,6 @@ import ( "go/types" "io" "io/fs" - "io/ioutil" "log" mathrand "math/rand" "os" @@ -48,18 +48,50 @@ var ( ) var ( - flagObfuscateLiterals bool - flagGarbleTiny bool - flagDebugDir string - flagSeed string + flagLiterals bool + flagTiny bool + flagDebugDir string + flagSeed seedFlag ) func init() { flagSet.Usage = usage - flagSet.BoolVar(&flagObfuscateLiterals, "literals", false, "Obfuscate literals such as strings") - flagSet.BoolVar(&flagGarbleTiny, "tiny", false, "Optimize for binary size, losing some ability to reverse the process") + flagSet.BoolVar(&flagLiterals, "literals", false, "Obfuscate literals such as strings") + flagSet.BoolVar(&flagTiny, "tiny", false, "Optimize for binary size, losing some ability to reverse the process") flagSet.StringVar(&flagDebugDir, "debugdir", "", "Write the obfuscated source to a directory, e.g. -debugdir=out") - flagSet.StringVar(&flagSeed, "seed", "", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random") + flagSet.Var(&flagSeed, "seed", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random") +} + +type seedFlag struct { + random bool + bytes []byte +} + +func (f seedFlag) String() string { + return base64.RawStdEncoding.EncodeToString(f.bytes) +} + +func (f *seedFlag) Set(s string) error { + if s == "random" { + f.bytes = make([]byte, 16) // random 128 bit seed + if _, err := rand.Read(f.bytes); err != nil { + return fmt.Errorf("error generating random seed: %v", err) + } + } else { + // We expect unpadded base64, but to be nice, accept padded + // strings too. + s = strings.TrimRight(s, "=") + seed, err := base64.RawStdEncoding.DecodeString(s) + if err != nil { + return fmt.Errorf("error decoding seed: %v", err) + } + + if len(seed) < 8 { + return fmt.Errorf("-seed needs at least 8 bytes, have %d", len(seed)) + } + f.bytes = seed + } + return nil } func usage() { @@ -100,6 +132,7 @@ func main() { os.Exit(main1()) } var ( fset = token.NewFileSet() sharedTempDir = os.Getenv("GARBLE_SHARED") + parentWorkDir = os.Getenv("GARBLE_PARENT_WORK") // origImporter is a go/types importer which uses the original versions // of packages, without any obfuscation. This is helpful to make @@ -122,8 +155,6 @@ var ( } return os.Open(pkgfile) }).(types.ImporterFrom) - - opts *flagOptions ) type importerWithMap func(path, dir string, mode types.ImportMode) (*types.Package, error) @@ -146,7 +177,7 @@ func obfuscatedTypesPackage(path string) *types.Package { if pkg := cachedTypesPackages[path]; pkg != nil { return pkg } - pkg, err := garbledImporter.ImportFrom(path, opts.GarbleDir, 0) + pkg, err := garbledImporter.ImportFrom(path, parentWorkDir, 0) if err != nil { panic(err) } @@ -175,8 +206,8 @@ func main1() int { // If the build failed and a random seed was used, // the failure might not reproduce with a different seed. // Print it before we exit. - if flagSeed == "random" { - fmt.Fprintf(os.Stderr, "random seed: %s\n", base64.RawStdEncoding.EncodeToString(opts.Seed)) + if flagSeed.random { + fmt.Fprintf(os.Stderr, "random seed: %s\n", base64.RawStdEncoding.EncodeToString(flagSeed.bytes)) } return 1 } @@ -264,7 +295,6 @@ func mainErr(args []string) error { if err := loadSharedCache(); err != nil { return err } - opts = &cache.Options _, tool := filepath.Split(args[0]) if runtime.GOOS == "windows" { @@ -330,13 +360,9 @@ This command wraps "go %s". Below is its help: return nil, errJustExit(2) } - if err := setFlagOptions(); err != nil { - return nil, err - } - // Here is the only place we initialize the cache. // The sub-processes will parse it from a shared gob file. - cache = &sharedCache{Options: *opts} + cache = &sharedCache{} // Note that we also need to pass build flags to 'go list', such // as -tags. @@ -369,11 +395,38 @@ This command wraps "go %s". Below is its help: } os.Setenv("GARBLE_SHARED", sharedTempDir) defer os.Remove(sharedTempDir) + wd, err := os.Getwd() + if err != nil { + return nil, err + } + os.Setenv("GARBLE_PARENT_WORK", wd) + + if flagDebugDir != "" { + if !filepath.IsAbs(flagDebugDir) { + flagDebugDir = filepath.Join(wd, flagDebugDir) + } + + if err := os.RemoveAll(flagDebugDir); err == nil || errors.Is(err, fs.ErrExist) { + err := os.MkdirAll(flagDebugDir, 0o755) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("debugdir error: %v", err) + } + } + + // Pass the garble flags down to each toolexec invocation. + // This way, all garble processes see the same flag values. + var toolexecFlag strings.Builder + toolexecFlag.WriteString("-toolexec=") + toolexecFlag.WriteString(cache.ExecPath) + appendFlags(&toolexecFlag) goArgs := []string{ command, "-trimpath", - "-toolexec=" + cache.ExecPath, + toolexecFlag.String(), } if flagDebugDir != "" { // In case the user deletes the debug directory, @@ -595,11 +648,11 @@ func transformCompile(args []string) ([]string, error) { // because obfuscated literals sometimes escape to heap, // and that's not allowed in the runtime itself. if runtimeAndDeps[curPkg.ImportPath] { - opts.ObfuscateLiterals = false + flagLiterals = false } // Literal obfuscation uses math/rand, so seed it deterministically. - randSeed := opts.Seed + randSeed := flagSeed.bytes if len(randSeed) == 0 { randSeed = curPkg.GarbleActionID } @@ -621,7 +674,7 @@ func transformCompile(args []string) ([]string, error) { for i, file := range files { name := filepath.Base(paths[i]) - if curPkg.ImportPath == "runtime" && opts.Tiny { + if curPkg.ImportPath == "runtime" && flagTiny { // strip unneeded runtime code stripRuntime(name, file) } @@ -650,9 +703,9 @@ func transformCompile(args []string) ([]string, error) { } else { newPaths = append(newPaths, path) } - if opts.DebugDir != "" { + if flagDebugDir != "" { osPkgPath := filepath.FromSlash(curPkg.ImportPath) - pkgDebugDir := filepath.Join(opts.DebugDir, osPkgPath) + pkgDebugDir := filepath.Join(flagDebugDir, osPkgPath) if err := os.MkdirAll(pkgDebugDir, 0o755); err != nil { return nil, err } @@ -1229,7 +1282,7 @@ func (tf *transformer) recordType(t types.Type) { func (tf *transformer) transformGo(file *ast.File) *ast.File { // Only obfuscate the literals here if the flag is on // and if the package in question is to be obfuscated. - if opts.ObfuscateLiterals && curPkg.ToObfuscate { + if flagLiterals && curPkg.ToObfuscate { file = literals.Obfuscate(file, tf.info, fset, tf.ignoreObjects) } @@ -1473,7 +1526,7 @@ func locateForeignAlias(dependentImportPath, aliasName string) *types.TypeName { panic(err) // shouldn't happen } for _, importedPath := range lpkg.Imports { - pkg2, err := origImporter.ImportFrom(importedPath, opts.GarbleDir, 0) + pkg2, err := origImporter.ImportFrom(importedPath, parentWorkDir, 0) if err != nil { panic(err) } @@ -1842,7 +1895,7 @@ How to install Go: https://golang.org/doc/install // path as a GOPRIVATE default. Include a _test variant too. // TODO(mvdan): we shouldn't need the _test variant here, // as the import path should not include it; only the package name. - if mod, err := ioutil.ReadFile(cache.GoEnv.GOMOD); err == nil { + if mod, err := os.ReadFile(cache.GoEnv.GOMOD); err == nil { modpath := modfile.ModulePath(mod) if modpath != "" { cache.GOGARBLE = modpath + "," + modpath + "_test" diff --git a/position.go b/position.go index 06da04c..5c13856 100644 --- a/position.go +++ b/position.go @@ -103,7 +103,7 @@ func printFile(file1 *ast.File) ([]byte, error) { origNode := origCallExprs[i] i++ newName := "" - if !opts.Tiny { + if !flagTiny { origPos := fmt.Sprintf("%s:%d", filename, fset.Position(origNode.Pos()).Offset) newName = hashWith(curPkg.GarbleActionID, origPos) + ".go" // log.Printf("%q hashed with %x to %q", origPos, curPkg.GarbleActionID, newName) diff --git a/shared.go b/shared.go index e21f1ce..d4d5359 100644 --- a/shared.go +++ b/shared.go @@ -2,13 +2,9 @@ package main import ( "bytes" - "crypto/rand" - "encoding/base64" "encoding/gob" "encoding/json" - "errors" "fmt" - "io/fs" "os" "os/exec" "path/filepath" @@ -25,8 +21,6 @@ type sharedCache struct { ExecPath string // absolute path to the garble binary being used ForwardBuildFlags []string // build flags fed to the original "garble ..." command - Options flagOptions // garble options being used, i.e. our own flags - // ListedPackages contains data obtained via 'go list -json -export -deps'. // This allows us to obtain the non-obfuscated export data of all dependencies, // useful for type checking of the packages as we obfuscate them. @@ -120,73 +114,6 @@ func writeGobExclusive(name string, val interface{}) error { return err } -// flagOptions are derived from the flags -type flagOptions struct { - ObfuscateLiterals bool - Tiny bool - GarbleDir string - DebugDir string - Seed []byte -} - -// setFlagOptions sets flagOptions from the user supplied flags. -func setFlagOptions() error { - wd, err := os.Getwd() - if err != nil { - return err - } - - if cache != nil { - panic("opts set twice?") - } - opts = &flagOptions{ - GarbleDir: wd, - ObfuscateLiterals: flagObfuscateLiterals, - Tiny: flagGarbleTiny, - } - - if flagSeed == "random" { - opts.Seed = make([]byte, 16) // random 128 bit seed - if _, err := rand.Read(opts.Seed); err != nil { - return fmt.Errorf("error generating random seed: %v", err) - } - - } else if len(flagSeed) > 0 { - // We expect unpadded base64, but to be nice, accept padded - // strings too. - flagSeed = strings.TrimRight(flagSeed, "=") - seed, err := base64.RawStdEncoding.DecodeString(flagSeed) - if err != nil { - return fmt.Errorf("error decoding seed: %v", err) - } - - if len(seed) < 8 { - return fmt.Errorf("-seed needs at least 8 bytes, have %d", len(seed)) - } - - opts.Seed = seed - } - - if flagDebugDir != "" { - if !filepath.IsAbs(flagDebugDir) { - flagDebugDir = filepath.Join(wd, flagDebugDir) - } - - if err := os.RemoveAll(flagDebugDir); err == nil || errors.Is(err, fs.ErrExist) { - err := os.MkdirAll(flagDebugDir, 0o755) - if err != nil { - return err - } - } else { - return fmt.Errorf("debugdir error: %v", err) - } - - opts.DebugDir = flagDebugDir - } - - return nil -} - // listedPackage contains the 'go list -json -export' fields obtained by the // root process, shared with all garble sub-processes via a file. type listedPackage struct {