├── .gitignore ├── LICENSE ├── README.md ├── const_to_var.go ├── go.mod ├── go.sum ├── gopath_copy.go ├── hash.go ├── main.go ├── pkg_names.go ├── strings.go ├── symbols.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore binary 2 | /gobfuscate 3 | *.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017, Alexander Nichol. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gobfuscate 2 | 3 | When you compile a Go binary, it contains a lot of information about your source code: field names, strings, package paths, etc. If you want to ship a binary without leaking this kind of information, what are you to do? 4 | 5 | With gobfuscate, you can compile a Go binary from obfuscated source code. This makes a lot of information difficult or impossible to decipher from the binary. 6 | 7 | # How to use 8 | ``` 9 | go get -u github.com/unixpickle/gobfuscate 10 | gobfuscate [flags] pkg_name out_path 11 | ``` 12 | `pkg_name` is the path relative from your $GOPATH/src to the package to obfuscate (typically something like domain.tld/user/repo) 13 | 14 | `out_path` is the path where the binary will be written to 15 | 16 | ### Flags 17 | ``` 18 | Usage: gobfuscate [flags] pkg_name out_path 19 | -keeptests 20 | keep _test.go files 21 | -noencrypt 22 | no encrypted package name for go build command (works when main package has CGO code) 23 | -nostatic 24 | do not statically link 25 | -outdir 26 | output a full GOPATH 27 | -padding string 28 | use a custom padding for hashing sensitive information (otherwise a random padding will be used) 29 | -tags string 30 | tags are passed to the go compiler 31 | -verbose 32 | verbose mode 33 | -winhide 34 | hide windows GUI 35 | ``` 36 | 37 | 38 | # What it does 39 | 40 | Currently, gobfuscate manipulates package names, global variable and function names, type names, method names, and strings. 41 | 42 | ### Package name obfuscation 43 | 44 | When gobfuscate builds your program, it constructs a copy of a subset of your GOPATH. It then refactors this GOPATH by hashing package names and paths. As a result, a package like "github.com/unixpickle/deleteme" becomes something like "jiikegpkifenppiphdhi/igijfdokiaecdkihheha/jhiofoppieegdaif". This helps get rid of things like Github usernames from the executable. 45 | 46 | **Limitation:** currently, packages which use CGO cannot be renamed. I suspect this is due to a bug in Go's refactoring API. 47 | 48 | ### Global names 49 | 50 | Gobfuscate hashes the names of global vars, consts, and funcs. It also hashes the names of any newly-defined types. 51 | 52 | Due to restrictions in the refactoring API, this does not work for packages which contain assembly files or use CGO. It also does not work for names which appear multiple times because of build constraints. 53 | 54 | ### Struct methods 55 | 56 | Gobfuscate hashes the names of most struct methods. However, it does not rename methods whose names match methods of any imported interfaces. This is mostly due to internal constraints from the refactoring engine. Theoretically, most interfaces could be obfuscated as well (except for those in the standard library). 57 | 58 | Due to restrictions in the refactoring API, this does not work for packages which contain assembly files or use CGO. It also does not work for names which appear multiple times because of build constraints. 59 | 60 | ### Strings 61 | 62 | Strings are obfuscated by replacing them with functions. A string will be turned into an expression like the following: 63 | 64 | ```go 65 | (func() string { 66 | mask := []byte{33, 15, 199} 67 | maskedStr := []byte{73, 106, 190} 68 | res := make([]byte, 3) 69 | for i, m := range mask { 70 | res[i] = m ^ maskedStr[i] 71 | } 72 | return string(res) 73 | }()) 74 | ``` 75 | 76 | Since `const` declarations cannot include function calls, gobfuscate tries to change any `const` strings into `var`s. It works for declarations like any of the following: 77 | 78 | ``` 79 | const MyStr = "hello" 80 | const MyStr1 = MyStr + "yoyo" 81 | const MyStr2 = MyStr + (MyStr1 + "hello1") 82 | 83 | const ( 84 | MyStr3 = "hey there" 85 | MyStr4 = MyStr1 + "yo" 86 | ) 87 | ``` 88 | 89 | However, it does not work for mixed const/int blocks: 90 | 91 | ``` 92 | const ( 93 | MyStr = "hey there" 94 | MyNum = 3 95 | ) 96 | ``` 97 | 98 | # License 99 | 100 | This is under a BSD 2-clause license. See [LICENSE](LICENSE). 101 | -------------------------------------------------------------------------------- /const_to_var.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "io/ioutil" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | func stringConstsToVar(path string) error { 15 | contents, err := ioutil.ReadFile(path) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | set := token.NewFileSet() 21 | file, err := parser.ParseFile(set, path, contents, 0) 22 | if err != nil { 23 | // If the file is invalid, we do nothing. 24 | return nil 25 | } 26 | 27 | ctv := &constToVar{} 28 | for _, decl := range file.Decls { 29 | ast.Walk(ctv, decl) 30 | } 31 | sort.Sort(ctv) 32 | 33 | var resBuf bytes.Buffer 34 | var lastIdx int 35 | for _, decl := range ctv.Decls { 36 | start := int(decl.Pos() - 1) 37 | end := int(decl.End() - 1) 38 | resBuf.Write(contents[lastIdx:start]) 39 | declData := contents[start:end] 40 | varData := strings.Replace(string(declData), "const", "var", 1) 41 | resBuf.WriteString(varData) 42 | lastIdx = end 43 | } 44 | resBuf.Write(contents[lastIdx:]) 45 | 46 | return ioutil.WriteFile(path, resBuf.Bytes(), 0755) 47 | } 48 | 49 | type constToVar struct { 50 | Decls []*ast.GenDecl 51 | } 52 | 53 | func (c *constToVar) Visit(n ast.Node) ast.Visitor { 54 | if decl, ok := n.(*ast.GenDecl); ok { 55 | if decl.Tok == token.CONST { 56 | if constOnlyHasStrings(decl) { 57 | c.Decls = append(c.Decls, decl) 58 | } 59 | } 60 | } 61 | return c 62 | } 63 | 64 | func (c *constToVar) Len() int { 65 | return len(c.Decls) 66 | } 67 | 68 | func (c *constToVar) Swap(i, j int) { 69 | c.Decls[i], c.Decls[j] = c.Decls[j], c.Decls[i] 70 | } 71 | 72 | func (c *constToVar) Less(i, j int) bool { 73 | return c.Decls[i].Pos() < c.Decls[j].Pos() 74 | } 75 | 76 | func constOnlyHasStrings(decl *ast.GenDecl) bool { 77 | for _, spec := range decl.Specs { 78 | if cs, ok := spec.(*ast.ValueSpec); ok { 79 | if !specIsString(cs) { 80 | return false 81 | } 82 | } 83 | } 84 | return true 85 | } 86 | 87 | func specIsString(v *ast.ValueSpec) bool { 88 | if v.Type != nil { 89 | s, ok := v.Type.(fmt.Stringer) 90 | if ok && s.String() == "string" { 91 | return true 92 | } 93 | } 94 | if len(v.Values) != 1 { 95 | return false 96 | } 97 | return exprIsString(v.Values[0]) 98 | } 99 | 100 | func exprIsString(e ast.Expr) bool { 101 | switch e := e.(type) { 102 | case *ast.BasicLit: 103 | if e.Kind == token.STRING { 104 | return true 105 | } 106 | case *ast.BinaryExpr: 107 | if e.Op == token.ADD { 108 | return exprIsString(e.X) || exprIsString(e.Y) 109 | } 110 | return false 111 | case *ast.ParenExpr: 112 | return exprIsString(e.X) 113 | } 114 | return false 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/unixpickle/gobfuscate 2 | 3 | go 1.15 4 | 5 | require golang.org/x/tools v0.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 5 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 8 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 9 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= 15 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 19 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 20 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= 21 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 22 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | -------------------------------------------------------------------------------- /gopath_copy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "golang.org/x/tools/refactor/importgraph" 12 | ) 13 | 14 | // CopyGopath creates a new Gopath with a copy of a package 15 | // and all of its dependencies. 16 | func CopyGopath(packageName, newGopath string, keepTests bool) error { 17 | ctx := build.Default 18 | 19 | rootPkg, err := ctx.Import(packageName, "", 0) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | allDeps, err := findDeps(packageName, &ctx) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | for dep := range allDeps { 30 | pkg, err := build.Default.Import(dep, rootPkg.Dir, 0) 31 | if err != nil { 32 | return err 33 | } 34 | if pkg.Goroot { 35 | continue 36 | } 37 | if err := copyDep(pkg, newGopath, keepTests); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | if !keepTests { 43 | ctx.GOPATH = newGopath 44 | allDeps, err = findDeps(packageName, &ctx) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | if err := removeUnusedPkgs(newGopath, allDeps); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | func findDeps(packageName string, ctx *build.Context) (map[string]bool, error) { 57 | forward, _, errs := importgraph.Build(ctx) 58 | if _, ok := forward[packageName]; !ok { 59 | if err, ok := errs[packageName]; ok { 60 | return nil, err 61 | } 62 | return nil, fmt.Errorf("package %s not found", packageName) 63 | } 64 | return forward.Search(packageName), nil 65 | } 66 | 67 | func copyDep(pkg *build.Package, newGopath string, keepTests bool) error { 68 | newPath := filepath.Join(newGopath, "src", pkg.ImportPath) 69 | err := os.MkdirAll(newPath, 0755) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | srcFiles := [][]string{ 75 | pkg.GoFiles, 76 | pkg.CgoFiles, 77 | pkg.CFiles, 78 | pkg.CXXFiles, 79 | pkg.MFiles, 80 | pkg.HFiles, 81 | pkg.FFiles, 82 | pkg.SFiles, 83 | pkg.SwigFiles, 84 | pkg.SwigCXXFiles, 85 | pkg.SysoFiles, 86 | } 87 | if keepTests { 88 | srcFiles = append(srcFiles, pkg.TestGoFiles, pkg.XTestGoFiles) 89 | } 90 | 91 | for _, list := range srcFiles { 92 | for _, file := range list { 93 | src := filepath.Join(pkg.Dir, file) 94 | dst := filepath.Join(newPath, file) 95 | if err := copyFile(src, dst); err != nil { 96 | return err 97 | } 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func removeUnusedPkgs(gopath string, deps map[string]bool) error { 105 | srcDir := filepath.Join(gopath, "src") 106 | return filepath.Walk(srcDir, func(sub string, info os.FileInfo, err error) error { 107 | if err != nil { 108 | return err 109 | } 110 | if !info.IsDir() { 111 | return nil 112 | } 113 | if !containsDep(gopath, sub, deps) { 114 | os.RemoveAll(sub) 115 | return filepath.SkipDir 116 | } 117 | return nil 118 | }) 119 | } 120 | 121 | func containsDep(gopath, dir string, deps map[string]bool) bool { 122 | for dep := range deps { 123 | depDir := filepath.Clean(filepath.Join(gopath, "src", dep)) 124 | if strings.HasPrefix(depDir, filepath.Clean(dir)) { 125 | return true 126 | } 127 | } 128 | return false 129 | } 130 | 131 | func copyFile(src, dest string) error { 132 | newFile, err := os.Create(dest) 133 | if err != nil { 134 | return err 135 | } 136 | defer newFile.Close() 137 | oldFile, err := os.Open(src) 138 | if err != nil { 139 | return err 140 | } 141 | defer oldFile.Close() 142 | _, err = io.Copy(newFile, oldFile) 143 | return err 144 | } 145 | -------------------------------------------------------------------------------- /hash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | const hashedSymbolSize = 10 10 | 11 | // A NameHasher is added to the input of a hash function 12 | // to make it 'impossible' to find the input value 13 | type NameHasher []byte 14 | 15 | // Hash hashes the padding + token. 16 | // The case of the first letter of the token is preserved. 17 | func (n NameHasher) Hash(token string) string { 18 | hashArray := sha256.Sum256(append(n, []byte(token)...)) 19 | 20 | hexStr := strings.ToLower(hex.EncodeToString(hashArray[:hashedSymbolSize])) 21 | for i, x := range hexStr { 22 | if x >= '0' && x <= '9' { 23 | x = 'g' + (x - '0') 24 | hexStr = hexStr[:i] + string(x) + hexStr[i+1:] 25 | } 26 | } 27 | if strings.ToUpper(token[:1]) == token[:1] { 28 | hexStr = strings.ToUpper(hexStr[:1]) + hexStr[1:] 29 | } 30 | return hexStr 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "flag" 6 | "fmt" 7 | "go/build" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | ) 14 | 15 | // Command line arguments. 16 | var ( 17 | customPadding string 18 | tags string 19 | outputGopath bool 20 | keepTests bool 21 | winHide bool 22 | noStaticLink bool 23 | preservePackageName bool 24 | verbose bool 25 | ) 26 | 27 | func main() { 28 | flag.StringVar(&customPadding, "padding", "", "use a custom padding for hashing sensitive information (otherwise a random padding will be used)") 29 | flag.BoolVar(&outputGopath, "outdir", false, "output a full GOPATH") 30 | flag.BoolVar(&keepTests, "keeptests", false, "keep _test.go files") 31 | flag.BoolVar(&winHide, "winhide", false, "hide windows GUI") 32 | flag.BoolVar(&noStaticLink, "nostatic", false, "do not statically link") 33 | flag.BoolVar(&preservePackageName, "noencrypt", false, 34 | "no encrypted package name for go build command (works when main package has CGO code)") 35 | flag.BoolVar(&verbose, "verbose", false, "verbose mode") 36 | flag.StringVar(&tags, "tags", "", "tags are passed to the go compiler") 37 | 38 | flag.Parse() 39 | 40 | if len(flag.Args()) != 2 { 41 | fmt.Fprintln(os.Stderr, "Usage: gobfuscate [flags] pkg_name out_path") 42 | flag.PrintDefaults() 43 | os.Exit(1) 44 | } 45 | 46 | pkgName := flag.Args()[0] 47 | outPath := flag.Args()[1] 48 | 49 | if !obfuscate(pkgName, outPath) { 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func obfuscate(pkgName, outPath string) bool { 55 | var newGopath string 56 | if outputGopath { 57 | newGopath = outPath 58 | if err := os.Mkdir(newGopath, 0755); err != nil { 59 | fmt.Fprintln(os.Stderr, "Failed to create destination:", err) 60 | return false 61 | } 62 | } else { 63 | var err error 64 | newGopath, err = ioutil.TempDir("", "") 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, "Failed to create temp dir:", err) 67 | return false 68 | } 69 | defer os.RemoveAll(newGopath) 70 | } 71 | 72 | log.Println("Copying GOPATH...") 73 | 74 | if err := CopyGopath(pkgName, newGopath, keepTests); err != nil { 75 | moreInfo := "\nNote: Setting GO111MODULE env variable to `off` may resolve the above error." 76 | fmt.Fprintln(os.Stderr, "Failed to copy into a new GOPATH:", err, moreInfo) 77 | return false 78 | } 79 | var n NameHasher 80 | if customPadding == "" { 81 | buf := make([]byte, 32) 82 | rand.Read(buf) 83 | n = buf 84 | } else { 85 | n = []byte(customPadding) 86 | } 87 | 88 | log.Println("Obfuscating package names...") 89 | if err := ObfuscatePackageNames(newGopath, n); err != nil { 90 | fmt.Fprintln(os.Stderr, "Failed to obfuscate package names:", err) 91 | return false 92 | } 93 | log.Println("Obfuscating strings...") 94 | if err := ObfuscateStrings(newGopath); err != nil { 95 | fmt.Fprintln(os.Stderr, "Failed to obfuscate strings:", err) 96 | return false 97 | } 98 | log.Println("Obfuscating symbols...") 99 | if err := ObfuscateSymbols(newGopath, n); err != nil { 100 | fmt.Fprintln(os.Stderr, "Failed to obfuscate symbols:", err) 101 | return false 102 | } 103 | 104 | if outputGopath { 105 | return true 106 | } 107 | 108 | ctx := build.Default 109 | 110 | newPkg := pkgName 111 | if !preservePackageName { 112 | newPkg = encryptComponents(pkgName, n) 113 | } 114 | 115 | ldflags := `-s -w` 116 | if winHide { 117 | ldflags += " -H=windowsgui" 118 | } 119 | if !noStaticLink { 120 | ldflags += ` -extldflags '-static'` 121 | } 122 | 123 | goCache := newGopath + "/cache" 124 | os.Mkdir(goCache, 0755) 125 | 126 | arguments := []string{"build", "-trimpath", "-ldflags", ldflags, "-tags", tags, "-o", outPath, newPkg} 127 | environment := []string{ 128 | "GO111MODULE=off", // needs to be off to make Go search GOPATH 129 | "GOROOT=" + ctx.GOROOT, 130 | "GOARCH=" + ctx.GOARCH, 131 | "GOOS=" + ctx.GOOS, 132 | "GOPATH=" + newGopath, 133 | "PATH=" + os.Getenv("PATH"), 134 | "GOCACHE=" + goCache, 135 | } 136 | 137 | cmd := exec.Command("go", arguments...) 138 | cmd.Env = environment 139 | cmd.Stdout = os.Stdout 140 | cmd.Stderr = os.Stderr 141 | 142 | if verbose { 143 | fmt.Println() 144 | fmt.Println("[Verbose] Temporary path:", newGopath) 145 | fmt.Println("[Verbose] Go build command: go", strings.Join(arguments, " ")) 146 | fmt.Println("[Verbose] Environment variables:") 147 | for _, envLine := range environment { 148 | fmt.Println(envLine) 149 | } 150 | fmt.Println() 151 | } 152 | 153 | if err := cmd.Run(); err != nil { 154 | fmt.Fprintln(os.Stderr, "Failed to compile:", err) 155 | return false 156 | } 157 | 158 | return true 159 | } 160 | 161 | func encryptComponents(pkgName string, n NameHasher) string { 162 | comps := strings.Split(pkgName, "/") 163 | for i, comp := range comps { 164 | comps[i] = n.Hash(comp) 165 | } 166 | return strings.Join(comps, "/") 167 | } 168 | -------------------------------------------------------------------------------- /pkg_names.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/build" 8 | "go/parser" 9 | "go/token" 10 | "io/ioutil" 11 | "path/filepath" 12 | "strings" 13 | 14 | "golang.org/x/tools/refactor/rename" 15 | ) 16 | 17 | func ObfuscatePackageNames(gopath string, n NameHasher) error { 18 | ctx := build.Default 19 | ctx.GOPATH = gopath 20 | 21 | level := 1 22 | srcDir := filepath.Join(gopath, "src") 23 | 24 | doneChan := make(chan struct{}) 25 | defer close(doneChan) 26 | 27 | for { 28 | resChan := make(chan string) 29 | go func() { 30 | scanLevel(srcDir, level, resChan, doneChan) 31 | close(resChan) 32 | }() 33 | var gotAny bool 34 | for dirPath := range resChan { 35 | gotAny = true 36 | if containsCGO(dirPath) { 37 | continue 38 | } 39 | isMain := isMainPackage(dirPath) 40 | encPath := encryptPackageName(dirPath, n) 41 | srcPkg, err := filepath.Rel(srcDir, dirPath) 42 | if err != nil { 43 | return err 44 | } 45 | dstPkg, err := filepath.Rel(srcDir, encPath) 46 | if err != nil { 47 | return err 48 | } 49 | if err := rename.Move(&ctx, srcPkg, dstPkg, ""); err != nil { 50 | return fmt.Errorf("package move: %s", err) 51 | } 52 | if isMain { 53 | if err := makeMainPackage(encPath); err != nil { 54 | return fmt.Errorf("make main package %s: %s", encPath, err) 55 | } 56 | } 57 | } 58 | if !gotAny { 59 | break 60 | } 61 | level++ 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func scanLevel(dir string, depth int, res chan<- string, done <-chan struct{}) { 68 | if depth == 0 { 69 | select { 70 | case res <- dir: 71 | case <-done: 72 | return 73 | } 74 | return 75 | } 76 | listing, _ := ioutil.ReadDir(dir) 77 | for _, item := range listing { 78 | if item.IsDir() { 79 | scanLevel(filepath.Join(dir, item.Name()), depth-1, res, done) 80 | } 81 | select { 82 | case <-done: 83 | return 84 | default: 85 | } 86 | } 87 | } 88 | 89 | func encryptPackageName(dir string, p NameHasher) string { 90 | subDir, base := filepath.Split(dir) 91 | return filepath.Join(subDir, p.Hash(base)) 92 | } 93 | 94 | func isMainPackage(dir string) bool { 95 | listing, err := ioutil.ReadDir(dir) 96 | if err != nil { 97 | return false 98 | } 99 | for _, item := range listing { 100 | if isGoFile(item.Name()) { 101 | path := filepath.Join(dir, item.Name()) 102 | set := token.NewFileSet() 103 | contents, err := ioutil.ReadFile(path) 104 | if err != nil { 105 | return false 106 | } 107 | file, err := parser.ParseFile(set, path, contents, 0) 108 | if err != nil { 109 | return false 110 | } 111 | fields := strings.Fields(string(contents[int(file.Package)-1:])) 112 | if len(fields) < 2 { 113 | return false 114 | } 115 | return fields[1] == "main" 116 | } 117 | } 118 | return false 119 | } 120 | 121 | func makeMainPackage(dir string) error { 122 | listing, err := ioutil.ReadDir(dir) 123 | if err != nil { 124 | return err 125 | } 126 | for _, item := range listing { 127 | if !isGoFile(item.Name()) { 128 | continue 129 | } 130 | path := filepath.Join(dir, item.Name()) 131 | contents, err := ioutil.ReadFile(path) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | set := token.NewFileSet() 137 | file, err := parser.ParseFile(set, path, contents, 0) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | pkgNameIdx := int(file.Package) + len("package") - 1 143 | prePkg := contents[:pkgNameIdx] 144 | postPkg := string(contents[pkgNameIdx:]) 145 | 146 | fields := strings.Fields(postPkg) 147 | if len(fields) < 1 { 148 | return errors.New("no fields after package keyword") 149 | } 150 | packageName := fields[0] 151 | 152 | var newData bytes.Buffer 153 | newData.Write(prePkg) 154 | newData.WriteString(strings.Replace(postPkg, packageName, "main", 1)) 155 | 156 | if err := ioutil.WriteFile(path, newData.Bytes(), item.Mode()); err != nil { 157 | return err 158 | } 159 | } 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "io/ioutil" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "strconv" 15 | ) 16 | 17 | func ObfuscateStrings(gopath string) error { 18 | return filepath.Walk(gopath, func(path string, info os.FileInfo, err error) error { 19 | if err != nil { 20 | return err 21 | } 22 | if info.IsDir() || !isGoFile(path) { 23 | return nil 24 | } 25 | if err := stringConstsToVar(path); err != nil { 26 | return err 27 | } 28 | 29 | contents, err := ioutil.ReadFile(path) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | set := token.NewFileSet() 35 | file, err := parser.ParseFile(set, path, contents, 0) 36 | if err != nil { 37 | return nil 38 | } 39 | 40 | obfuscator := &stringObfuscator{Contents: contents} 41 | for _, decl := range file.Decls { 42 | ast.Walk(obfuscator, decl) 43 | } 44 | newCode, err := obfuscator.Obfuscate() 45 | if err != nil { 46 | return err 47 | } 48 | return ioutil.WriteFile(path, newCode, 0755) 49 | }) 50 | } 51 | 52 | type stringObfuscator struct { 53 | Contents []byte 54 | Nodes []*ast.BasicLit 55 | } 56 | 57 | func (s *stringObfuscator) Visit(n ast.Node) ast.Visitor { 58 | if lit, ok := n.(*ast.BasicLit); ok { 59 | if lit.Kind == token.STRING { 60 | s.Nodes = append(s.Nodes, lit) 61 | } 62 | return nil 63 | } else if decl, ok := n.(*ast.GenDecl); ok { 64 | if decl.Tok == token.CONST || decl.Tok == token.IMPORT { 65 | return nil 66 | } 67 | } else if _, ok := n.(*ast.StructType); ok { 68 | // Avoid messing with annotation strings. 69 | return nil 70 | } 71 | return s 72 | } 73 | 74 | func (s *stringObfuscator) Obfuscate() ([]byte, error) { 75 | sort.Sort(s) 76 | 77 | parsed := make([]string, s.Len()) 78 | for i, n := range s.Nodes { 79 | var err error 80 | parsed[i], err = strconv.Unquote(n.Value) 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | var lastIndex int 87 | var result bytes.Buffer 88 | data := s.Contents 89 | for i, node := range s.Nodes { 90 | strVal := parsed[i] 91 | startIdx := node.Pos() - 1 92 | endIdx := node.End() - 1 93 | result.Write(data[lastIndex:startIdx]) 94 | result.Write(obfuscatedStringCode(strVal)) 95 | lastIndex = int(endIdx) 96 | } 97 | result.Write(data[lastIndex:]) 98 | return result.Bytes(), nil 99 | } 100 | 101 | func (s *stringObfuscator) Len() int { 102 | return len(s.Nodes) 103 | } 104 | 105 | func (s *stringObfuscator) Swap(i, j int) { 106 | s.Nodes[i], s.Nodes[j] = s.Nodes[j], s.Nodes[i] 107 | } 108 | 109 | func (s *stringObfuscator) Less(i, j int) bool { 110 | return s.Nodes[i].Pos() < s.Nodes[j].Pos() 111 | } 112 | 113 | func obfuscatedStringCode(str string) []byte { 114 | var res bytes.Buffer 115 | res.WriteString("(func() string {\n") 116 | res.WriteString("mask := []byte(\"") 117 | mask := make([]byte, len(str)) 118 | for i := range mask { 119 | mask[i] = byte(rand.Intn(256)) 120 | res.WriteString(fmt.Sprintf("\\x%02x", mask[i])) 121 | } 122 | res.WriteString("\")\nmaskedStr := []byte(\"") 123 | for i, x := range []byte(str) { 124 | res.WriteString(fmt.Sprintf("\\x%02x", x^mask[i])) 125 | } 126 | res.WriteString("\")\nres := make([]byte, ") 127 | res.WriteString(strconv.Itoa(len(mask))) 128 | res.WriteString(`) 129 | for i, m := range mask { 130 | res[i] = m ^ maskedStr[i] 131 | } 132 | return string(res) 133 | }())`) 134 | return res.Bytes() 135 | } 136 | -------------------------------------------------------------------------------- /symbols.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/build" 7 | "go/parser" 8 | "go/token" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "golang.org/x/tools/refactor/importgraph" 16 | "golang.org/x/tools/refactor/rename" 17 | ) 18 | 19 | var IgnoreMethods = map[string]bool{"main": true, "init": true} 20 | 21 | type symbolRenameReq struct { 22 | OldName string 23 | NewName string 24 | } 25 | 26 | func ObfuscateSymbols(gopath string, n NameHasher) error { 27 | removeDoNotEdit(gopath) 28 | renames, err := topLevelRenames(gopath, n) 29 | if err != nil { 30 | return fmt.Errorf("top-level renames: %s", err) 31 | } 32 | if err := runRenames(gopath, renames); err != nil { 33 | return fmt.Errorf("top-level renaming: %s", err) 34 | } 35 | renames, err = methodRenames(gopath, n) 36 | if err != nil { 37 | return fmt.Errorf("method renames: %s", err) 38 | } 39 | if err := runRenames(gopath, renames); err != nil { 40 | return fmt.Errorf("method renaming: %s", err) 41 | } 42 | return nil 43 | } 44 | 45 | func runRenames(gopath string, renames []symbolRenameReq) error { 46 | ctx := build.Default 47 | ctx.GOPATH = gopath 48 | for _, r := range renames { 49 | if err := rename.Main(&ctx, "", r.OldName, r.NewName); err != nil { 50 | log.Println("Error running renames proceding...", err) 51 | continue 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | func topLevelRenames(gopath string, n NameHasher) ([]symbolRenameReq, error) { 58 | srcDir := filepath.Join(gopath, "src") 59 | res := map[symbolRenameReq]int{} 60 | addRes := func(pkgPath, name string) { 61 | prefix := "\"" + pkgPath + "\"." 62 | oldName := prefix + name 63 | newName := n.Hash(name) 64 | res[symbolRenameReq{oldName, newName}]++ 65 | } 66 | err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { 67 | if err != nil { 68 | return err 69 | } 70 | if info.IsDir() && containsUnsupportedCode(path) { 71 | return filepath.SkipDir 72 | } 73 | if !isGoFile(path) { 74 | return nil 75 | } 76 | pkgPath, err := filepath.Rel(srcDir, filepath.Dir(path)) 77 | if err != nil { 78 | return err 79 | } 80 | set := token.NewFileSet() 81 | file, err := parser.ParseFile(set, path, nil, 0) 82 | if err != nil { 83 | return err 84 | } 85 | for _, decl := range file.Decls { 86 | switch d := decl.(type) { 87 | case *ast.FuncDecl: 88 | if !IgnoreMethods[d.Name.Name] && d.Recv == nil { 89 | addRes(pkgPath, d.Name.Name) 90 | } 91 | case *ast.GenDecl: 92 | for _, spec := range d.Specs { 93 | switch spec := spec.(type) { 94 | case *ast.TypeSpec: 95 | addRes(pkgPath, spec.Name.Name) 96 | case *ast.ValueSpec: 97 | for _, name := range spec.Names { 98 | addRes(pkgPath, name.Name) 99 | } 100 | } 101 | } 102 | } 103 | } 104 | return nil 105 | }) 106 | return singleRenames(res), err 107 | } 108 | 109 | func methodRenames(gopath string, n NameHasher) ([]symbolRenameReq, error) { 110 | exclude, err := interfaceMethods(gopath) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | srcDir := filepath.Join(gopath, "src") 116 | res := map[symbolRenameReq]int{} 117 | err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { 118 | if err != nil { 119 | return err 120 | } 121 | if info.IsDir() && containsUnsupportedCode(path) { 122 | return filepath.SkipDir 123 | } 124 | if !isGoFile(path) { 125 | return nil 126 | } 127 | pkgPath, err := filepath.Rel(srcDir, filepath.Dir(path)) 128 | if err != nil { 129 | return err 130 | } 131 | set := token.NewFileSet() 132 | file, err := parser.ParseFile(set, path, nil, 0) 133 | if err != nil { 134 | return err 135 | } 136 | for _, decl := range file.Decls { 137 | d, ok := decl.(*ast.FuncDecl) 138 | if !ok || exclude[d.Name.Name] || d.Recv == nil { 139 | continue 140 | } 141 | prefix := "\"" + pkgPath + "\"." 142 | for _, rec := range d.Recv.List { 143 | receiver := receiverString(prefix, rec) 144 | if receiver == "" { 145 | continue 146 | } 147 | oldName := receiver + "." + d.Name.Name 148 | newName := n.Hash(d.Name.Name) 149 | res[symbolRenameReq{oldName, newName}]++ 150 | } 151 | } 152 | return nil 153 | }) 154 | return singleRenames(res), err 155 | } 156 | 157 | func interfaceMethods(gopath string) (map[string]bool, error) { 158 | ctx := build.Default 159 | ctx.GOPATH = gopath 160 | forward, backward, _ := importgraph.Build(&ctx) 161 | pkgs := map[string]bool{} 162 | for _, m := range []importgraph.Graph{forward, backward} { 163 | for x := range m { 164 | pkgs[x] = true 165 | } 166 | } 167 | res := map[string]bool{} 168 | for pkgName := range pkgs { 169 | pkg, err := ctx.Import(pkgName, gopath, 0) 170 | if err != nil { 171 | return nil, fmt.Errorf("import %s: %s", pkgName, err) 172 | } 173 | for _, fileName := range pkg.GoFiles { 174 | sourcePath := filepath.Join(pkg.Dir, fileName) 175 | set := token.NewFileSet() 176 | file, err := parser.ParseFile(set, sourcePath, nil, 0) 177 | if err != nil { 178 | return nil, err 179 | } 180 | for _, decl := range file.Decls { 181 | d, ok := decl.(*ast.GenDecl) 182 | if !ok { 183 | continue 184 | } 185 | for _, spec := range d.Specs { 186 | spec, ok := spec.(*ast.TypeSpec) 187 | if !ok { 188 | continue 189 | } 190 | t, ok := spec.Type.(*ast.InterfaceType) 191 | if !ok { 192 | continue 193 | } 194 | for _, field := range t.Methods.List { 195 | for _, name := range field.Names { 196 | res[name.Name] = true 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | return res, nil 204 | } 205 | 206 | // singleRenames removes any rename requests which appear 207 | // more than one time. 208 | // This is necessary because of build constraints, which 209 | // the refactoring API doesn't seem to properly support. 210 | func singleRenames(multiset map[symbolRenameReq]int) []symbolRenameReq { 211 | var res []symbolRenameReq 212 | for x, count := range multiset { 213 | if count == 1 { 214 | res = append(res, x) 215 | } 216 | } 217 | return res 218 | } 219 | 220 | // containsUnsupportedCode checks if a source directory 221 | // contains assembly or CGO code, neither of which are 222 | // supported by the refactoring API. 223 | func containsUnsupportedCode(dir string) bool { 224 | return containsAssembly(dir) || containsCGO(dir) 225 | } 226 | 227 | // containsAssembly checks if a source directory contains 228 | // any assembly files. 229 | // We cannot rename symbols in assembly-filled directories 230 | // because of limitations of the refactoring API. 231 | func containsAssembly(dir string) bool { 232 | contents, _ := ioutil.ReadDir(dir) 233 | for _, item := range contents { 234 | if filepath.Ext(item.Name()) == ".s" { 235 | return true 236 | } 237 | } 238 | return false 239 | } 240 | 241 | // containsCGO checks if a package relies on CGO. 242 | // We cannot rename symbols in packages that use CGO due 243 | // to limitations of the refactoring API. 244 | func containsCGO(dir string) bool { 245 | listing, err := ioutil.ReadDir(dir) 246 | if err != nil { 247 | return false 248 | } 249 | for _, item := range listing { 250 | if isGoFile(item.Name()) { 251 | path := filepath.Join(dir, item.Name()) 252 | set := token.NewFileSet() 253 | file, err := parser.ParseFile(set, path, nil, 0) 254 | if err != nil { 255 | return false 256 | } 257 | for _, spec := range file.Imports { 258 | if spec.Path.Value == `"C"` { 259 | return true 260 | } 261 | } 262 | } 263 | } 264 | return false 265 | } 266 | 267 | // removeDoNotEdit removes comments that prevent gorename 268 | // from working properly. 269 | func removeDoNotEdit(dir string) error { 270 | srcDir := filepath.Join(dir, "src") 271 | return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { 272 | if err != nil { 273 | return err 274 | } 275 | if info.IsDir() || !isGoFile(path) { 276 | return nil 277 | } 278 | 279 | f, err := os.OpenFile(path, os.O_RDWR, 0755) 280 | if err != nil { 281 | return err 282 | } 283 | defer f.Close() 284 | 285 | content, err := ioutil.ReadAll(f) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | set := token.NewFileSet() 291 | file, err := parser.ParseFile(set, path, content, parser.ParseComments) 292 | if err != nil { 293 | return err 294 | } 295 | 296 | for _, comment := range file.Comments { 297 | data := make([]byte, comment.End()-comment.Pos()) 298 | start := int(comment.Pos()) - 1 299 | end := start + len(data) 300 | data = content[start:end] 301 | commentStr := string(data) 302 | if strings.Contains(commentStr, "DO NOT EDIT") { 303 | commentStr = strings.Replace(commentStr, "DO NOT EDIT", "XXXXXXXXXXX", -1) 304 | if _, err := f.WriteAt([]byte(commentStr), int64(comment.Pos()-1)); err != nil { 305 | return err 306 | } 307 | } 308 | } 309 | return nil 310 | }) 311 | } 312 | 313 | // receiverString gets the string representation of a 314 | // method receiver so that the method can be renamed. 315 | func receiverString(prefix string, rec *ast.Field) string { 316 | if stringer, ok := rec.Type.(fmt.Stringer); ok { 317 | return prefix + stringer.String() 318 | } else if star, ok := rec.Type.(*ast.StarExpr); ok { 319 | if stringer, ok := star.X.(fmt.Stringer); ok { 320 | return "(*" + prefix + stringer.String() + ")" 321 | } 322 | } 323 | return "" 324 | } 325 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "path/filepath" 4 | 5 | func isGoFile(path string) bool { 6 | return filepath.Ext(path) == ".go" 7 | } 8 | --------------------------------------------------------------------------------