diff --git a/go.mod b/go.mod index d5793ea6..1cd9d2f4 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/uber/jaeger-lib v2.0.0+incompatible // indirect github.com/urfave/negroni v1.0.0 go.uber.org/goleak v1.0.0 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 // indirect google.golang.org/grpc v1.22.0 // indirect gopkg.in/yaml.v2 v2.3.0 diff --git a/go.sum b/go.sum index 34442ffe..03f3bf7a 100644 --- a/go.sum +++ b/go.sum @@ -197,6 +197,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180419222023-a2a45943ae67/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/crypto/hash.go b/pkg/crypto/hash.go new file mode 100644 index 00000000..88790d83 --- /dev/null +++ b/pkg/crypto/hash.go @@ -0,0 +1,90 @@ +package crypto + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "hash" + "io" + "strings" + + "golang.org/x/crypto/sha3" +) + +var ( + // DefaultHasher is the default implementation for hashing things + // It outputs 32 Bytes and uses a SHA3-256 hash in the current configuration. + // Its generic security strength is 256 bits against preimage attacks, + // and 128 bits against collision attacks. + defaultHasher = basicHasher{sha3.New256()} +) + +// Hash is a convenience function calling the default hasher +// WARNING: only pass in data that is json-marshalable. If not, the worst case scenario is that you passed in data with circular references and this will just blow up your CPU +func Hash(data ...interface{}) ([]byte, error) { + return defaultHasher.Hash(data...) +} + +// HashToString is a convenience function calling the default hasher and encoding the result as hex string +func HashToString(data ...interface{}) (string, error) { + hash, err := defaultHasher.Hash(data...) + if err != nil { + return "", err + } + return hex.EncodeToString(hash), nil +} + +// Hasher provides a method for hashing arbitary data types +type Hasher interface { + Hash(data ...interface{}) ([]byte, error) +} + +type basicHasher struct { + hash hash.Hash +} + +func (h basicHasher) Hash(args ...interface{}) ([]byte, error) { + h.hash.Reset() + + for _, data := range args { + var ( + reader io.Reader + encoderError error + ) + + // setup reader for the data + switch d := data.(type) { + case io.Reader: + reader = d + case []byte: + reader = bytes.NewReader(d) + case string: + reader = strings.NewReader(d) + case fmt.Stringer: + reader = strings.NewReader(d.String()) + default: + r, w := io.Pipe() + encoder := json.NewEncoder(w) + go func() { + defer w.Close() + encoderError = encoder.Encode(data) + }() + reader = r + } + + // hash all the data + _, err := io.Copy(h.hash, reader) + if err != nil { + return nil, err + } + + // check encoder error + if encoderError != nil { + return nil, encoderError + } + + } + + return h.hash.Sum(nil), nil +} diff --git a/pkg/crypto/hash_test.go b/pkg/crypto/hash_test.go new file mode 100644 index 00000000..ead81e70 --- /dev/null +++ b/pkg/crypto/hash_test.go @@ -0,0 +1,80 @@ +package crypto + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHash(t *testing.T) { + cases := []struct { + name string + input []interface{} + expectedOutput string + expectedError error + }{ + { + name: "hash a string", + input: []interface{}{"foobar"}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "reproducable results", + input: []interface{}{"foobar"}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "something else", + input: []interface{}{"foobarbaz"}, + expectedOutput: "369972cd3fda2b1e239bf114f4c2c65115b05fee2e4e5b2ae19ce7b5d757c572", + }, + { + name: "hash multiple strings", + input: []interface{}{"foo", "bar"}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "hash reader", + input: []interface{}{strings.NewReader("foobar")}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "hash bytes", + input: []interface{}{[]byte("foobar")}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "hash complex object", + input: []interface{}{struct{ foo map[string]interface{} }{foo: map[string]interface{}{"a": 123}}}, + expectedOutput: "d0a1b2af1705c1b8495b00145082ef7470384e62ac1c4d9b9cdbbe0476c28f8c", + }, + { + name: "hash multiple different things", + input: []interface{}{"f", strings.NewReader("oo"), []byte("bar")}, + expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5", + }, + { + name: "marshal error is forwarded", + input: []interface{}{map[float64]string{ + 1.: "lol", + }}, + expectedOutput: "", + expectedError: &json.UnsupportedTypeError{ + Type: reflect.MapOf( + reflect.TypeOf(1.), + reflect.TypeOf("lol"), + )}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + hash, err := HashToString(tc.input...) + require.Equal(t, tc.expectedOutput, hash) + require.Equal(t, tc.expectedError, err) + }) + } +}