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/strings.go

257 lines
5.7 KiB
Go

package main
import (
"encoding/hex"
"fmt"
"go/ast"
"go/token"
"strconv"
"strings"
"golang.org/x/tools/go/ast/astutil"
)
func obfuscateLiterals(files []*ast.File) []*ast.File {
pre := func(cursor *astutil.Cursor) bool {
decl, ok := cursor.Node().(*ast.GenDecl)
if !ok || decl.Tok != token.CONST {
return true
}
for _, spec := range decl.Specs {
for _, val := range spec.(*ast.ValueSpec).Values {
if v, ok := val.(*ast.BasicLit); !ok || v.Kind != token.STRING {
return false // skip the block if it contains non basic literals
}
}
}
// constants are not possible if we want to obfuscate literals, therefore
// move all constant blocks which only contain strings to variables
decl.Tok = token.VAR
return true
}
key := genAesKey()
addedToPkg := false // we only want to inject the code and imports once
post := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.File:
if addedToPkg {
break
}
x.Decls = append(x.Decls, funcStmt)
x.Decls = append(x.Decls, keyStmt(key))
astutil.AddImport(fset, x, "crypto/aes")
astutil.AddImport(fset, x, "crypto/cipher")
addedToPkg = true
case *ast.BasicLit:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args":
default:
return true // we don't want to obfuscate imports etc.
}
if x.Kind != token.STRING {
return true // TODO: garble literals other than strings
}
value, err := strconv.Unquote(x.Value)
if err != nil {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
ciphertext, err := encAES([]byte(value), key)
if err != nil {
panic(fmt.Sprintf("cannot encrypt string: %v", err))
}
cursor.Replace(ciphertextStmt(ciphertext))
}
return true
}
for i := range files {
files[i] = astutil.Apply(files[i], pre, post).(*ast.File)
}
return files
}
// AST definitions for injection
var (
aesCipherStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "block"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "aes"},
Sel: &ast.Ident{Name: "NewCipher"},
},
Args: []ast.Expr{&ast.Ident{Name: "garbleKey"}},
}},
}
aesGcmCipherStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "aesgcm"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "cipher"},
Sel: &ast.Ident{Name: "NewGCM"},
},
Args: []ast.Expr{&ast.Ident{Name: "block"}},
}},
}
plaintextStmt = &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "plaintext"},
&ast.Ident{Name: "err"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "aesgcm"},
Sel: &ast.Ident{Name: "Open"},
},
Args: []ast.Expr{
&ast.Ident{Name: "nil"},
&ast.SliceExpr{
X: &ast.Ident{Name: "ciphertext"},
High: &ast.BasicLit{
Kind: token.INT,
Value: "12",
},
},
&ast.SliceExpr{
X: &ast.Ident{Name: "ciphertext"},
Low: &ast.BasicLit{
Kind: token.INT,
Value: "12",
},
},
&ast.Ident{Name: "nil"},
},
}},
}
returnStmt = &ast.ReturnStmt{Results: []ast.Expr{
&ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{&ast.Ident{Name: "plaintext"}},
},
}}
)
func decErrStmt() *ast.IfStmt {
return &ast.IfStmt{
Cond: &ast.BinaryExpr{
X: &ast.Ident{Name: "err"},
Op: token.NEQ,
Y: &ast.Ident{Name: "nil"},
},
Body: &ast.BlockStmt{List: []ast.Stmt{
&ast.ExprStmt{X: &ast.CallExpr{
Fun: &ast.Ident{Name: "panic"},
Args: []ast.Expr{&ast.BinaryExpr{
X: &ast.BasicLit{
Kind: token.STRING,
Value: `"garble: literal couldn't be decrypted: "`,
},
Op: token.ADD,
Y: &ast.CallExpr{Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "err"},
Sel: &ast.Ident{Name: "Error"},
}},
}},
}},
}},
}
}
var funcStmt = &ast.FuncDecl{
Name: &ast.Ident{Name: "garbleDecrypt"},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{{Name: "ciphertext"}},
Type: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
}}},
Results: &ast.FieldList{List: []*ast.Field{{
Type: &ast.Ident{Name: "string"},
}}},
},
Body: &ast.BlockStmt{List: []ast.Stmt{
aesCipherStmt,
decErrStmt(),
aesGcmCipherStmt,
decErrStmt(),
plaintextStmt,
decErrStmt(),
returnStmt,
}},
}
func ciphertextStmt(ciphertext []byte) *ast.CallExpr {
ciphertextLit := dataAsByteSlice(ciphertext)
return &ast.CallExpr{
Fun: &ast.Ident{Name: "garbleDecrypt"},
Args: []ast.Expr{ciphertextLit},
}
}
// dataAsByteSlice turns a byte slice like []byte{1, 2, 3} into an AST
// expression which encodes it, such as []byte("\x01\x02\x03").
func dataAsByteSlice(data []byte) *ast.CallExpr {
var b strings.Builder
b.WriteByte('"')
hexstr := hex.EncodeToString(data)
for i := 0; i < len(hexstr); i += 2 {
b.WriteString("\\x" + hexstr[i:i+2])
}
b.WriteByte('"')
return &ast.CallExpr{
Fun: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.STRING,
Value: b.String(),
}},
}
}
func keyStmt(key []byte) *ast.GenDecl {
keyLit := dataAsByteSlice(key)
return &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{
Names: []*ast.Ident{{Name: "garbleKey"}},
Values: []ast.Expr{keyLit},
}},
}
}
var cryptoAesImportSpec = &ast.GenDecl{
Tok: token.IMPORT,
Specs: []ast.Spec{
&ast.ImportSpec{Path: &ast.BasicLit{
Kind: token.STRING,
Value: `"crypto/aes"`,
}},
&ast.ImportSpec{Path: &ast.BasicLit{
Kind: token.STRING,
Value: `"crypto/cipher"`,
}},
},
}