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

193 lines
5.1 KiB
Go

// Copyright (c) 2019, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"go/token"
"io"
"os"
"os/exec"
"strings"
"unicode"
)
const buildIDSeparator = "/"
// actionID returns the action ID half of a build ID, the first element.
func actionID(buildID string) string {
i := strings.Index(buildID, buildIDSeparator)
if i < 0 {
return buildID
}
return buildID[:i]
}
// contentID returns the content ID half of a build ID, the last element.
func contentID(buildID string) string {
return buildID[strings.LastIndex(buildID, buildIDSeparator)+1:]
}
// decodeHash isthe opposite of hashToString, but with a panic for error
// handling since it should never happen.
func decodeHash(str string) []byte {
h, err := base64.RawURLEncoding.DecodeString(str)
if err != nil {
panic(fmt.Sprintf("invalid hash %q: %v", str, err))
}
return h
}
func alterToolVersion(tool string, args []string) error {
cmd := exec.Command(args[0], args[1:]...)
out, err := cmd.Output()
if err != nil {
if err, _ := err.(*exec.ExitError); err != nil {
return fmt.Errorf("%v: %s", err, err.Stderr)
}
return err
}
line := string(bytes.TrimSpace(out)) // no trailing newline
f := strings.Fields(line)
if len(f) < 3 || f[0] != tool || f[1] != "version" || f[2] == "devel" && !strings.HasPrefix(f[len(f)-1], "buildID=") {
return fmt.Errorf("%s -V=full: unexpected output:\n\t%s", args[0], line)
}
var toolID []byte
if f[2] == "devel" {
// On the development branch, use the content ID part of the build ID.
toolID = decodeHash(contentID(f[len(f)-1]))
} else {
// For a release, the output is like: "compile version go1.9.1 X:framepointer".
// Use the whole line.
toolID = []byte(line)
}
contentID, err := ownContentID(toolID)
if err != nil {
return fmt.Errorf("cannot obtain garble's own version: %v", err)
}
// The part of the build ID that matters is the last, since it's the
// "content ID" which is used to work out whether there is a need to redo
// the action (build) or not. Since cmd/go parses the last word in the
// output as "buildID=...", we simply add "+garble buildID=_/_/_/${hash}".
// The slashes let us imitate a full binary build ID, but we assume that
// the other components such as the action ID are not necessary, since the
// only reader here is cmd/go and it only consumes the content ID.
fmt.Printf("%s +garble buildID=_/_/_/%s\n", line, contentID)
return nil
}
func ownContentID(toolID []byte) (string, error) {
// We can't rely on the module version to exist, because it's
// missing in local builds without 'go get'.
// For now, use 'go tool buildid' on the binary that's running. Just
// like Go's own cache, we use hex-encoded sha256 sums.
// Once https://github.com/golang/go/issues/37475 is fixed, we
// can likely just use that.
path, err := os.Executable()
if err != nil {
return "", err
}
buildID, err := buildidOf(path)
if err != nil {
return "", err
}
ownID := decodeHash(contentID(buildID))
// Join the two content IDs together into a single base64-encoded sha256
// sum. This includes the original tool's content ID, and garble's own
// content ID.
h := sha256.New()
h.Write(toolID)
h.Write(ownID)
// We also need to add the selected options to the full version string,
// because all of them result in different output. We use spaces to
// separate the env vars and flags, to reduce the chances of collisions.
if envGoPrivate != "" {
fmt.Fprintf(h, " GOPRIVATE=%s", envGoPrivate)
}
if opts.GarbleLiterals {
fmt.Fprintf(h, " -literals")
}
if opts.Tiny {
fmt.Fprintf(h, " -tiny")
}
if len(opts.Seed) > 0 {
fmt.Fprintf(h, " -seed=%x", opts.Seed)
}
return hashToString(h.Sum(nil)), nil
}
// hashToString encodes the first 120 bits of a sha256 sum in base64, the same
// format used for elements in a build ID.
func hashToString(h []byte) string {
return base64.RawURLEncoding.EncodeToString(h[:15])
}
func buildidOf(path string) (string, error) {
cmd := exec.Command("go", "tool", "buildid", path)
out, err := cmd.Output()
if err != nil {
if err, _ := err.(*exec.ExitError); err != nil {
return "", fmt.Errorf("%v: %s", err, err.Stderr)
}
return "", err
}
return string(out), nil
}
func hashWith(salt []byte, name string) string {
const length = 4
d := sha256.New()
d.Write(salt)
d.Write(opts.Seed)
io.WriteString(d, name)
sum := b64.EncodeToString(d.Sum(nil))
if token.IsExported(name) {
return "Z" + sum[:length]
}
return "z" + sum[:length]
}
func buildNameCharset() []rune {
var charset []rune
for _, r := range unicode.Letter.R16 {
for c := r.Lo; c <= r.Hi; c += r.Stride {
charset = append(charset, rune(c))
}
}
for _, r := range unicode.Digit.R16 {
for c := r.Lo; c <= r.Hi; c += r.Stride {
charset = append(charset, rune(c))
}
}
return charset
}
var privateNameCharset = buildNameCharset()
func encodeIntToName(i int) string {
builder := strings.Builder{}
for i > 0 {
charIdx := i % len(privateNameCharset)
i -= charIdx + 1
c := privateNameCharset[charIdx]
if builder.Len() == 0 && !unicode.IsLetter(c) {
builder.WriteByte('_')
}
builder.WriteRune(c)
}
return builder.String()
}