Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit ca6a1f6

Browse files
truschLucasRoesler
andauthored
add Hash() and HashToString() function to the crypto package (#54)
* add Hash() and HashToString() function to the crypto package to have one standard way of hashing across different projects. Co-authored-by: Lucas Roesler <[email protected]>
1 parent efef3b1 commit ca6a1f6

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
github.com/uber/jaeger-lib v2.0.0+incompatible // indirect
3131
github.com/urfave/negroni v1.0.0
3232
go.uber.org/goleak v1.0.0
33+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
3334
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 // indirect
3435
google.golang.org/grpc v1.22.0 // indirect
3536
gopkg.in/yaml.v2 v2.3.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh
197197
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
198198
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
199199
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
200+
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
200201
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
201202
golang.org/x/sys v0.0.0-20180419222023-a2a45943ae67/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
202203
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

pkg/crypto/hash.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package crypto
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"encoding/json"
7+
"fmt"
8+
"hash"
9+
"io"
10+
"strings"
11+
12+
"golang.org/x/crypto/sha3"
13+
)
14+
15+
var (
16+
// DefaultHasher is the default implementation for hashing things
17+
// It outputs 32 Bytes and uses a SHA3-256 hash in the current configuration.
18+
// Its generic security strength is 256 bits against preimage attacks,
19+
// and 128 bits against collision attacks.
20+
defaultHasher = basicHasher{sha3.New256()}
21+
)
22+
23+
// Hash is a convenience function calling the default hasher
24+
// 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
25+
func Hash(data ...interface{}) ([]byte, error) {
26+
return defaultHasher.Hash(data...)
27+
}
28+
29+
// HashToString is a convenience function calling the default hasher and encoding the result as hex string
30+
func HashToString(data ...interface{}) (string, error) {
31+
hash, err := defaultHasher.Hash(data...)
32+
if err != nil {
33+
return "", err
34+
}
35+
return hex.EncodeToString(hash), nil
36+
}
37+
38+
// Hasher provides a method for hashing arbitary data types
39+
type Hasher interface {
40+
Hash(data ...interface{}) ([]byte, error)
41+
}
42+
43+
type basicHasher struct {
44+
hash hash.Hash
45+
}
46+
47+
func (h basicHasher) Hash(args ...interface{}) ([]byte, error) {
48+
h.hash.Reset()
49+
50+
for _, data := range args {
51+
var (
52+
reader io.Reader
53+
encoderError error
54+
)
55+
56+
// setup reader for the data
57+
switch d := data.(type) {
58+
case io.Reader:
59+
reader = d
60+
case []byte:
61+
reader = bytes.NewReader(d)
62+
case string:
63+
reader = strings.NewReader(d)
64+
case fmt.Stringer:
65+
reader = strings.NewReader(d.String())
66+
default:
67+
r, w := io.Pipe()
68+
encoder := json.NewEncoder(w)
69+
go func() {
70+
defer w.Close()
71+
encoderError = encoder.Encode(data)
72+
}()
73+
reader = r
74+
}
75+
76+
// hash all the data
77+
_, err := io.Copy(h.hash, reader)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
// check encoder error
83+
if encoderError != nil {
84+
return nil, encoderError
85+
}
86+
87+
}
88+
89+
return h.hash.Sum(nil), nil
90+
}

pkg/crypto/hash_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package crypto
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestHash(t *testing.T) {
13+
cases := []struct {
14+
name string
15+
input []interface{}
16+
expectedOutput string
17+
expectedError error
18+
}{
19+
{
20+
name: "hash a string",
21+
input: []interface{}{"foobar"},
22+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
23+
},
24+
{
25+
name: "reproducable results",
26+
input: []interface{}{"foobar"},
27+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
28+
},
29+
{
30+
name: "something else",
31+
input: []interface{}{"foobarbaz"},
32+
expectedOutput: "369972cd3fda2b1e239bf114f4c2c65115b05fee2e4e5b2ae19ce7b5d757c572",
33+
},
34+
{
35+
name: "hash multiple strings",
36+
input: []interface{}{"foo", "bar"},
37+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
38+
},
39+
{
40+
name: "hash reader",
41+
input: []interface{}{strings.NewReader("foobar")},
42+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
43+
},
44+
{
45+
name: "hash bytes",
46+
input: []interface{}{[]byte("foobar")},
47+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
48+
},
49+
{
50+
name: "hash complex object",
51+
input: []interface{}{struct{ foo map[string]interface{} }{foo: map[string]interface{}{"a": 123}}},
52+
expectedOutput: "d0a1b2af1705c1b8495b00145082ef7470384e62ac1c4d9b9cdbbe0476c28f8c",
53+
},
54+
{
55+
name: "hash multiple different things",
56+
input: []interface{}{"f", strings.NewReader("oo"), []byte("bar")},
57+
expectedOutput: "09234807e4af85f17c66b48ee3bca89dffd1f1233659f9f940a2b17b0b8c6bc5",
58+
},
59+
{
60+
name: "marshal error is forwarded",
61+
input: []interface{}{map[float64]string{
62+
1.: "lol",
63+
}},
64+
expectedOutput: "",
65+
expectedError: &json.UnsupportedTypeError{
66+
Type: reflect.MapOf(
67+
reflect.TypeOf(1.),
68+
reflect.TypeOf("lol"),
69+
)},
70+
},
71+
}
72+
73+
for _, tc := range cases {
74+
t.Run(tc.name, func(t *testing.T) {
75+
hash, err := HashToString(tc.input...)
76+
require.Equal(t, tc.expectedOutput, hash)
77+
require.Equal(t, tc.expectedError, err)
78+
})
79+
}
80+
}

0 commit comments

Comments
 (0)