You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/shared.go

232 lines
5.7 KiB
Go

package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/gob"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
)
// shared this data is shared between the different garble processes
type shared struct {
ExecPath string // absolute path to the garble binary being used
BuildFlags []string // build flags fed to the original "garble ..." command
Options options // garble options being used, i.e. our own flags
ListedPackages listedPackages // non-garbled view of all packages to build
}
var cache *shared
// loadShared the shared data passed from the entry garble process
func loadShared() error {
if cache == nil {
f, err := os.Open(filepath.Join(sharedTempDir, "main-cache.gob"))
if err != nil {
return fmt.Errorf(`cannot open shared file, this is most likely due to not running "garble [command]"`)
}
defer f.Close()
if err := gob.NewDecoder(f).Decode(&cache); err != nil {
return err
}
}
return nil
}
// saveShared creates a temporary directory to share between garble processes.
// This directory also includes the gob-encoded cache global.
func saveShared() (string, error) {
dir, err := ioutil.TempDir("", "garble-shared")
if err != nil {
return "", err
}
sharedCache := filepath.Join(dir, "main-cache.gob")
f, err := os.Create(sharedCache)
if err != nil {
return "", err
}
defer f.Close()
if err := gob.NewEncoder(f).Encode(&cache); err != nil {
return "", err
}
return dir, nil
}
// options are derived from the flags
type options struct {
GarbleLiterals bool
Tiny bool
GarbleDir string
DebugDir string
Seed []byte
Random bool
}
// setOptions sets all options from the user supplied flags.
func setOptions() error {
wd, err := os.Getwd()
if err != nil {
return err
}
opts = &options{
GarbleDir: wd,
GarbleLiterals: flagGarbleLiterals,
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)
}
opts.Random = true
} else {
flagSeed = strings.TrimRight(flagSeed, "=")
seed, err := base64.RawStdEncoding.DecodeString(flagSeed)
if err != nil {
return fmt.Errorf("error decoding seed: %v", err)
}
if len(seed) != 0 && len(seed) < 8 {
return fmt.Errorf("the seed needs to be at least 8 bytes, but is only %v bytes", len(seed))
}
opts.Seed = seed
}
if flagDebugDir != "" {
if !filepath.IsAbs(flagDebugDir) {
flagDebugDir = filepath.Join(wd, flagDebugDir)
}
if err := os.RemoveAll(flagDebugDir); err == nil || os.IsNotExist(err) {
err := os.MkdirAll(flagDebugDir, 0o755)
if err != nil {
return err
}
} else {
return fmt.Errorf("debugdir error: %v", err)
}
opts.DebugDir = flagDebugDir
}
cache = &shared{Options: *opts}
return nil
}
// listedPackages contains data obtained via 'go list -json -export -deps'. This
// allows us to obtain the non-garbled export data of all dependencies, useful
// for type checking of the packages as we obfuscate them.
//
// Note that we obtain this data once in saveListedPackages, store it into a
// temporary file via gob encoding, and then reuse that file in each of the
// garble processes that wrap a package compilation.
type listedPackages map[string]*listedPackage
// listedPackage contains information useful for obfuscating a package
type listedPackage struct {
Name string
ImportPath string
Export string
Deps []string
ImportMap map[string]string
reverse: support unexported names and package paths (#233) Unexported names are a bit tricky, since they are not listed in the export data file. Perhaps unsurprisingly, it's only meant to expose exported objects. One option would be to go back to adding an extra header to the export data file, containing the unexported methods in a map[string]T or []string. However, we have an easier route: just parse the Go files and look up the names directly. This does mean that we parse the Go files every time "reverse" runs, even if the build cache is warm, but that should not be an issue. Parsing Go files without any typechecking is very cheap compared to everything else we do. Plus, we save having to load go/types information from the build cache, or having to load extra headers from export files. It should be noted that the obfuscation process does need type information, mainly to be careful about which names can be obfuscated and how they should be obfuscated. Neither is a worry here; all names belong to a single package, and it doesn't matter if some aren't actually obfuscated, since the string replacements would simply never trigger in practice. The test includes an unexported func, to test the new feature. We also start reversing the obfuscation of import paths. Now, the test's reverse output is as follows: goroutine 1 [running]: runtime/debug.Stack(0x??, 0x??, 0x??) runtime/debug/stack.go:24 +0x?? test/main/lib.ExportedLibFunc(0x??, 0x??, 0x??, 0x??) p.go:6 +0x?? main.unexportedMainFunc(...) C.go:2 main.main() z.go:3 +0x?? The only major missing feature is positions and filenames. A follow-up PR will take care of those. Updates #5.
3 years ago
Dir string
GoFiles []string
// TODO(mvdan): reuse this field once TOOLEXEC_IMPORTPATH is used
private bool
}
// setListedPackages gets information about the current package
// and all of its dependencies
func setListedPackages(patterns []string) error {
args := []string{"list", "-json", "-deps", "-export"}
args = append(args, cache.BuildFlags...)
args = append(args, patterns...)
cmd := exec.Command("go", args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("go list error: %v", err)
}
dec := json.NewDecoder(stdout)
cache.ListedPackages = make(listedPackages)
for dec.More() {
var pkg listedPackage
if err := dec.Decode(&pkg); err != nil {
return err
}
cache.ListedPackages[pkg.ImportPath] = &pkg
}
if err := cmd.Wait(); err != nil {
return fmt.Errorf("go list error: %v: %s", err, stderr.Bytes())
}
anyPrivate := false
for path, pkg := range cache.ListedPackages {
if isPrivate(path) {
pkg.private = true
anyPrivate = true
}
}
if !anyPrivate {
return fmt.Errorf("GOPRIVATE=%q does not match any packages to be built", os.Getenv("GOPRIVATE"))
}
for path, pkg := range cache.ListedPackages {
if pkg.private {
continue
}
for _, depPath := range pkg.Deps {
if cache.ListedPackages[depPath].private {
return fmt.Errorf("public package %q can't depend on obfuscated package %q (matched via GOPRIVATE=%q)",
path, depPath, os.Getenv("GOPRIVATE"))
}
}
}
return nil
}
// listPackage gets the listedPackage information for a certain package
func listPackage(path string) (*listedPackage, error) {
// If the path is listed in the top-level ImportMap, use its mapping instead.
// This is a common scenario when dealing with vendored packages in GOROOT.
// The map is flat, so we don't need to recurse.
if fromPkg, ok := cache.ListedPackages[curPkgPath]; ok {
if path2 := fromPkg.ImportMap[path]; path2 != "" {
path = path2
}
}
pkg, ok := cache.ListedPackages[path]
if !ok {
return nil, fmt.Errorf("path not found in listed packages: %s", path)
}
return pkg, nil
}