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/internal/literals/literals.go

329 lines
7.9 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package literals
import (
"fmt"
"go/ast"
"go/token"
"go/types"
mathrand "math/rand"
"strconv"
"golang.org/x/tools/go/ast/astutil"
ah "mvdan.cc/garble/internal/asthelper"
)
// maxSizeBytes is the limit, in bytes, of the size of string-like literals
// which we will obfuscate. This is important, because otherwise garble can take
// a very long time to obfuscate huge code-generated literals, such as those
// corresponding to large assets.
//
// Note that this is the size of the literal in source code. For example, "\xab"
// counts as four bytes.
//
// If someone truly wants to obfuscate those, they should do that when they
// generate the code, not at build time. Plus, with Go 1.16 that technique
// should largely stop being used.
const maxSizeBytes = 2 << 10 // KiB
func randObfuscator() obfuscator {
randPos := mathrand.Intn(len(obfuscators))
return obfuscators[randPos]
}
// Obfuscate replaces literals with obfuscated anonymous functions.
func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, ignoreObj map[types.Object]bool) *ast.File {
pre := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.GenDecl:
if x.Tok != token.CONST {
return true
}
for _, spec := range x.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
return false
}
if len(spec.Values) == 0 {
// skip constants with inferred values
return false
}
for _, name := range spec.Names {
obj := info.ObjectOf(name)
basic, ok := obj.Type().(*types.Basic)
if !ok {
// skip the block if it contains non basic types
return false
}
if basic.Info()&types.IsUntyped != 0 {
// skip the block if it contains untyped constants
return false
}
// The object cannot be obfuscated, e.g. a value that needs to be constant
if ignoreObj[obj] {
return false
}
}
}
x.Tok = token.VAR
// constants are not possible if we want to obfuscate literals, therefore
// move all constant blocks which only contain strings to variables
}
return true
}
post := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.CompositeLit:
byteType := types.Universe.Lookup("byte").Type()
if len(x.Elts) == 0 {
return true
}
switch y := info.TypeOf(x.Type).(type) {
case *types.Array:
if y.Elem() != byteType {
return true
}
if y.Len() > maxSizeBytes {
return true
}
data := make([]byte, y.Len())
for i, el := range x.Elts {
lit, ok := el.(*ast.BasicLit)
if !ok {
return true
}
value, err := strconv.Atoi(lit.Value)
if err != nil {
return true
}
data[i] = byte(value)
}
cursor.Replace(withPos(obfuscateByteArray(data, y.Len()), x.Pos()))
case *types.Slice:
if y.Elem() != byteType {
return true
}
if len(x.Elts) > maxSizeBytes {
return true
}
data := make([]byte, 0, len(x.Elts))
for _, el := range x.Elts {
lit, ok := el.(*ast.BasicLit)
if !ok {
return true
}
value, err := strconv.Atoi(lit.Value)
if err != nil {
return true
}
data = append(data, byte(value))
}
cursor.Replace(withPos(obfuscateByteSlice(data), x.Pos()))
}
case *ast.BasicLit:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args", "X", "Y", "Results":
default:
return true // we don't want to obfuscate imports etc.
}
if x.Kind != token.STRING {
return true
}
if len(x.Value) > maxSizeBytes {
return true
}
typeInfo := info.TypeOf(x)
if typeInfo != types.Typ[types.String] && typeInfo != types.Typ[types.UntypedString] {
return true
}
value, err := strconv.Unquote(x.Value)
if err != nil {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
if len(value) == 0 {
return true
}
cursor.Replace(withPos(obfuscateString(value), x.Pos()))
}
return true
}
return astutil.Apply(file, pre, post).(*ast.File)
}
// withPos sets any token.Pos fields under node which affect printing to pos.
// Note that we can't set all token.Pos fields, since some affect the semantics.
//
// This function is useful so that go/printer doesn't try to estimate position
// offsets, which can end up in printing comment directives too early.
//
// We don't set any "end" or middle positions, because they seem irrelevant.
func withPos(node ast.Node, pos token.Pos) ast.Node {
ast.Inspect(node, func(node ast.Node) bool {
switch node := node.(type) {
case *ast.BasicLit:
node.ValuePos = pos
case *ast.Ident:
node.NamePos = pos
case *ast.CompositeLit:
node.Lbrace = pos
node.Rbrace = pos
case *ast.ArrayType:
node.Lbrack = pos
case *ast.FuncType:
node.Func = pos
case *ast.BinaryExpr:
node.OpPos = pos
case *ast.StarExpr:
node.Star = pos
case *ast.CallExpr:
node.Lparen = pos
node.Rparen = pos
case *ast.GenDecl:
node.TokPos = pos
case *ast.ReturnStmt:
node.Return = pos
case *ast.ForStmt:
node.For = pos
case *ast.RangeStmt:
node.For = pos
case *ast.BranchStmt:
node.TokPos = pos
}
return true
})
return node
}
func obfuscateString(data string) *ast.CallExpr {
obfuscator := randObfuscator()
block := obfuscator.obfuscate([]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(data []byte) *ast.CallExpr {
obfuscator := randObfuscator()
block := obfuscator.obfuscate(data)
block.List = append(block.List, ah.ReturnStmt(ast.NewIdent("data")))
return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block)
}
func obfuscateByteArray(data []byte, length int64) *ast.CallExpr {
obfuscator := randObfuscator()
block := obfuscator.obfuscate(data)
arrayType := &ast.ArrayType{
Len: ah.IntLit(int(length)),
Elt: ast.NewIdent("byte"),
}
sliceToArray := []ast.Stmt{
&ast.DeclStmt{
Decl: &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{
Names: []*ast.Ident{ast.NewIdent("newdata")},
Type: arrayType,
}},
},
},
&ast.RangeStmt{
Key: ast.NewIdent("i"),
Tok: token.DEFINE,
X: ast.NewIdent("newdata"),
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{ah.IndexExpr("newdata", ast.NewIdent("i"))},
Tok: token.ASSIGN,
Rhs: []ast.Expr{ah.IndexExpr("data", ast.NewIdent("i"))},
},
}},
},
ah.ReturnStmt(ast.NewIdent("newdata")),
}
block.List = append(block.List, sliceToArray...)
return ah.LambdaCall(arrayType, block)
}
// RecordUsedAsConstants records identifieres used in constant expressions.
func RecordUsedAsConstants(node ast.Node, info *types.Info, ignoreObj map[types.Object]bool) {
visit := func(node ast.Node) bool {
ident, ok := node.(*ast.Ident)
if !ok {
return true
}
// Only record *types.Const objects.
// Other objects, such as builtins or type names,
// must not be recorded as they would be false positives.
obj := info.ObjectOf(ident)
if _, ok := obj.(*types.Const); ok {
ignoreObj[obj] = true
}
return true
}
switch x := node.(type) {
// in a slice or array composite literal all explicit keys must be constant representable
case *ast.CompositeLit:
if _, ok := x.Type.(*ast.ArrayType); !ok {
break
}
for _, elt := range x.Elts {
if kv, ok := elt.(*ast.KeyValueExpr); ok {
ast.Inspect(kv.Key, visit)
}
}
// in an array type the length must be a constant representable
case *ast.ArrayType:
if x.Len != nil {
ast.Inspect(x.Len, visit)
}
// in a const declaration all values must be constant representable
case *ast.GenDecl:
if x.Tok != token.CONST {
break
}
for _, spec := range x.Specs {
spec := spec.(*ast.ValueSpec)
for _, val := range spec.Values {
ast.Inspect(val, visit)
}
}
}
}