diff --git a/ahash/ahash.go b/ahash/ahash.go new file mode 100644 index 0000000..908cb4d --- /dev/null +++ b/ahash/ahash.go @@ -0,0 +1,258 @@ +// Package ahash provides support for hashing data with a selectable +// hash function. +package ahash + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "errors" + "hash" + "hash/adler32" + "hash/crc32" + "hash/crc64" + "hash/fnv" + "io" + "sort" + + "github.com/kisom/goutils/assert" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/blake2s" + "golang.org/x/crypto/md4" + "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/sha3" +) + +func sha224Slicer(bs []byte) []byte { + sum := sha256.Sum224(bs) + return sum[:] +} + +func sha256Slicer(bs []byte) []byte { + sum := sha256.Sum256(bs) + return sum[:] +} + +func sha384Slicer(bs []byte) []byte { + sum := sha512.Sum384(bs) + return sum[:] +} + +func sha512Slicer(bs []byte) []byte { + sum := sha512.Sum512(bs) + return sum[:] +} + +var sliceFunctions = map[string]func([]byte) []byte{ + "sha224": sha224Slicer, + "sha256": sha256Slicer, + "sha384": sha384Slicer, + "sha512": sha512Slicer, +} + +// Hash represents a generic hash function that may or may not be secure. It +// satisfies the hash.Hash interface. +type Hash struct { + hash.Hash + secure bool + algo string +} + +// HashAlgo returns the name of the underlying hash algorithm. +func (h *Hash) HashAlgo() string { + return h.algo +} + +// IsSecure returns true if the Hash is a cryptographic hash. +func (h *Hash) IsSecure() bool { + return h.secure +} + +// Sum32 returns true if the underlying hash is a 32-bit hash; if is, the +// uint32 parameter will contain the hash. +func (h *Hash) Sum32() (uint32, bool) { + h32, ok := h.Hash.(hash.Hash32) + if !ok { + return 0, false + } + + return h32.Sum32(), true +} + +// IsHash32 returns true if the underlying hash is a 32-bit hash function. +func (h *Hash) IsHash32() bool { + _, ok := h.Hash.(hash.Hash32) + return ok +} + +// Sum64 returns true if the underlying hash is a 64-bit hash; if is, the +// uint64 parameter will contain the hash. +func (h *Hash) Sum64() (uint64, bool) { + h64, ok := h.Hash.(hash.Hash64) + if !ok { + return 0, false + } + + return h64.Sum64(), true +} + +// IsHash64 returns true if the underlying hash is a 64-bit hash function. +func (h *Hash) IsHash64() bool { + _, ok := h.Hash.(hash.Hash64) + return ok +} + +func blakeFunc(bf func(key []byte) (hash.Hash, error)) func() hash.Hash { + return func() hash.Hash { + h, err := bf(nil) + assert.NoError(err, "while constructing a BLAKE2 hash function") + return h + } +} + +var secureHashes = map[string]func() hash.Hash{ + "ripemd160": ripemd160.New, + "sha224": sha256.New224, + "sha256": sha256.New, + "sha384": sha512.New384, + "sha512": sha512.New, + "sha3-224": sha3.New224, + "sha3-256": sha3.New256, + "sha3-384": sha3.New384, + "sha3-512": sha3.New512, + "blake2s-256": blakeFunc(blake2s.New256), + "blake2b-256": blakeFunc(blake2b.New256), + "blake2b-384": blakeFunc(blake2b.New384), + "blake2b-512": blakeFunc(blake2b.New512), +} + +func newHash32(f func () hash.Hash32) func () hash.Hash { + return func() hash.Hash { + return f() + } +} + +func newHash64(f func () hash.Hash64) func () hash.Hash { + return func() hash.Hash { + return f() + } +} + +func newCRC64(tab uint64) func () hash.Hash { + return newHash64( + func() hash.Hash64 { + return crc64.New(crc64.MakeTable(tab)) + }) +} + +var insecureHashes = map[string]func() hash.Hash{ + "md4": md4.New, + "md5": md5.New, + "sha1": sha1.New, + "adler32": newHash32(adler32.New), + "crc32-ieee": newHash32(crc32.NewIEEE), + "crc64": newCRC64(crc64.ISO), + "crc64-ecma": newCRC64(crc64.ECMA), + "fnv1-32a": newHash32(fnv.New32a), + "fnv1-32": newHash32(fnv.New32), + "fnv1-64a": newHash64(fnv.New64a), + "fnv1-64": newHash64(fnv.New64), +} + +// New returns a new Hash for the specified algorithm. +func New(algo string) (*Hash, error) { + h := &Hash{algo: algo} + + hf, ok := secureHashes[algo] + if ok { + h.Hash = hf() + h.secure = true + return h, nil + } + + hf, ok = insecureHashes[algo] + if ok { + h.Hash = hf() + h.secure = false + return h, nil + } + + return nil, errors.New("chash: unsupport hash algorithm " + algo) +} + +// Sum returns the digest (not the hex digest) of the data using the given +// algorithm. +func Sum(algo string, data []byte) ([]byte, error) { + h, err := New(algo) + if err != nil { + return nil, err + } + + _, err = h.Write(data) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil +} + +// SumReader reads all the data from the given io.Reader and returns the +// digest (not the hex digest) from the specified algorithm. +func SumReader(algo string, r io.Reader) ([]byte, error) { + h, err := New(algo) + if err != nil { + return nil, err + } + + _, err = io.Copy(h, r) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil +} + +var insecureHashList, secureHashList, hashList []string + +func init() { + shl := len(secureHashes) // secure hash list length + ihl := len(insecureHashes) // insecure hash list length + ahl := shl + ihl // all hash list length + + insecureHashList = make([]string, 0, ihl) + secureHashList = make([]string, 0, shl) + hashList = make([]string, 0, ahl) + + for algo := range insecureHashes { + insecureHashList = append(insecureHashList, algo) + } + sort.Strings(insecureHashList) + + for algo := range secureHashes { + secureHashList = append(secureHashList, algo) + } + sort.Strings(secureHashList) + + hashList = append(hashList, insecureHashList...) + hashList = append(hashList, secureHashList...) + sort.Strings(hashList) +} + +// HashList returns a sorted list of all the hash algorithms supported by the +// package. +func HashList() []string { + return hashList[:] +} + +// SecureHashList returns a sorted list of all the secure (cryptographic) hash +// algorithms supported by the package. +func SecureHashList() []string { + return secureHashList[:] +} + +// InsecureHashList returns a sorted list of all the insecure hash algorithms +// supported by the package. +func InsecureHashList() []string { + return insecureHashList[:] +} diff --git a/ahash/ahash_test.go b/ahash/ahash_test.go new file mode 100644 index 0000000..ad1e069 --- /dev/null +++ b/ahash/ahash_test.go @@ -0,0 +1,141 @@ +package ahash + +import ( + "bytes" + "fmt" + "testing" + + "github.com/kisom/goutils/assert" +) + +func TestSecureHash(t *testing.T) { + algo := "sha256" + h, err := New(algo) + assert.NoErrorT(t, err) + assert.BoolT(t, h.IsSecure(), algo + " should be a secure hash") + assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo") + assert.BoolT(t, !h.IsHash32(), algo + " isn't actually a 32-bit hash") + assert.BoolT(t, !h.IsHash64(), algo + " isn't actually a 64-bit hash") + + var data []byte + var expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + sum, err := Sum(algo, data) + assert.NoErrorT(t, err) + assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum)) + + data = []byte("hello, world") + buf := bytes.NewBuffer(data) + expected = "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b" + sum, err = SumReader(algo, buf) + assert.NoErrorT(t, err) + assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum)) + + data = []byte("hello world") + _, err = h.Write(data) + assert.NoErrorT(t, err) + unExpected := "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b" + sum = h.Sum(nil) + assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected)) +} + +func TestInsecureHash(t *testing.T) { + algo := "md5" + h, err := New(algo) + assert.NoErrorT(t, err) + assert.BoolT(t, !h.IsSecure(), algo + " shouldn't be a secure hash") + assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo") + assert.BoolT(t, !h.IsHash32(), algo + " isn't actually a 32-bit hash") + assert.BoolT(t, !h.IsHash64(), algo + " isn't actually a 64-bit hash") + + var data []byte + var expected = "d41d8cd98f00b204e9800998ecf8427e" + sum, err := Sum(algo, data) + assert.NoErrorT(t, err) + assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum)) + + data = []byte("hello, world") + buf := bytes.NewBuffer(data) + expected = "e4d7f1b4ed2e42d15898f4b27b019da4" + sum, err = SumReader(algo, buf) + assert.NoErrorT(t, err) + assert.BoolT(t, fmt.Sprintf("%x", sum) == expected, fmt.Sprintf("expected hash %s but have %x", expected, sum)) + + data = []byte("hello world") + _, err = h.Write(data) + assert.NoErrorT(t, err) + unExpected := "e4d7f1b4ed2e42d15898f4b27b019da4" + sum = h.Sum(nil) + assert.BoolT(t, fmt.Sprintf("%x", sum) != unExpected, fmt.Sprintf("hash shouldn't have returned %x", unExpected)) +} + +func TestHash32(t *testing.T) { + algo := "crc32-ieee" + h, err := New(algo) + assert.NoErrorT(t, err) + assert.BoolT(t, !h.IsSecure(), algo + " shouldn't be a secure hash") + assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo") + assert.BoolT(t, h.IsHash32(), algo + " is actually a 32-bit hash") + assert.BoolT(t, !h.IsHash64(), algo + " isn't actually a 64-bit hash") + + var data []byte + var expected uint32 + + h.Write(data) + sum, ok := h.Sum32() + assert.BoolT(t, ok, algo + " should be able to return a Sum32") + assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected)) + + data = []byte("hello, world") + expected = 0xffab723a + h.Write(data) + sum, ok = h.Sum32() + assert.BoolT(t, ok, algo + " should be able to return a Sum32") + assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected)) + + h.Reset() + data = []byte("hello world") + h.Write(data) + sum, ok = h.Sum32() + assert.BoolT(t, ok, algo + " should be able to return a Sum32") + assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum, expected)) +} + +func TestHash64(t *testing.T) { + algo := "crc64" + h, err := New(algo) + assert.NoErrorT(t, err) + assert.BoolT(t, !h.IsSecure(), algo + " shouldn't be a secure hash") + assert.BoolT(t, h.HashAlgo() == algo, "hash returned the wrong HashAlgo") + assert.BoolT(t, h.IsHash64(), algo + " is actually a 64-bit hash") + assert.BoolT(t, !h.IsHash32(), algo + " isn't actually a 32-bit hash") + + var data []byte + var expected uint64 + + h.Write(data) + sum, ok := h.Sum64() + assert.BoolT(t, ok, algo + " should be able to return a Sum64") + assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected)) + + data = []byte("hello, world") + expected = 0x16c45c0eb1d9c2ec + h.Write(data) + sum, ok = h.Sum64() + assert.BoolT(t, ok, algo + " should be able to return a Sum64") + assert.BoolT(t, expected == sum, fmt.Sprintf("%s returned the %d but expected %d", algo, sum, expected)) + + h.Reset() + data = []byte("hello world") + h.Write(data) + sum, ok = h.Sum64() + assert.BoolT(t, ok, algo + " should be able to return a Sum64") + assert.BoolT(t, expected != sum, fmt.Sprintf("%s returned %d but shouldn't have", algo, sum, expected)) +} + +func TestListLengthSanity(t *testing.T) { + all := HashList() + secure := SecureHashList() + insecure := InsecureHashList() + + assert.BoolT(t, len(all) == len(secure) + len(insecure)) +} \ No newline at end of file